From a054bbc04b0b3d7783e2a33ead6b906f5902421a Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Wed, 11 Dec 2024 11:51:42 +0000 Subject: [PATCH 01/77] Remove 7.2 transport versions (#118434) --- .../org/elasticsearch/TransportVersions.java | 2 - .../cluster/metadata/IndexMetadata.java | 18 ++----- .../query/DistanceFeatureQueryBuilder.java | 2 +- .../index/query/IntervalsSourceProvider.java | 10 +--- .../query/MatchBoolPrefixQueryBuilder.java | 2 +- .../index/refresh/RefreshStats.java | 13 ++--- .../elasticsearch/TransportVersionTests.java | 42 ++++++++-------- .../indices/close/CloseIndexRequestTests.java | 16 ++---- .../termvectors/TermVectorsUnitTests.java | 49 ------------------- .../rolemapping/PutRoleMappingRequest.java | 9 +--- .../pivot/DateHistogramGroupSourceTests.java | 21 -------- .../authc/RoleMappingMetadataTests.java | 23 --------- .../xpack/sql/common/io/SqlStreamTests.java | 2 +- 13 files changed, 39 insertions(+), 170 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index b22df6c374b4d..d61afbdf98587 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -52,8 +52,6 @@ static TransportVersion def(int id) { @UpdateForV9(owner = UpdateForV9.Owner.CORE_INFRA) // remove the transport versions with which v9 will not need to interact public static final TransportVersion ZERO = def(0); public static final TransportVersion V_7_0_0 = def(7_00_00_99); - public static final TransportVersion V_7_2_0 = def(7_02_00_99); - public static final TransportVersion V_7_2_1 = def(7_02_01_99); public static final TransportVersion V_7_3_0 = def(7_03_00_99); public static final TransportVersion V_7_4_0 = def(7_04_00_99); public static final TransportVersion V_7_5_0 = def(7_05_00_99); 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 681ea84513088..952789e1bf746 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java @@ -1618,11 +1618,7 @@ private static class IndexMetadataDiff implements Diff { version = in.readLong(); mappingVersion = in.readVLong(); settingsVersion = in.readVLong(); - if (in.getTransportVersion().onOrAfter(TransportVersions.V_7_2_0)) { - aliasesVersion = in.readVLong(); - } else { - aliasesVersion = 1; - } + aliasesVersion = in.readVLong(); state = State.fromId(in.readByte()); if (in.getTransportVersion().onOrAfter(SETTING_DIFF_VERSION)) { settings = null; @@ -1688,9 +1684,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeLong(version); out.writeVLong(mappingVersion); out.writeVLong(settingsVersion); - if (out.getTransportVersion().onOrAfter(TransportVersions.V_7_2_0)) { - out.writeVLong(aliasesVersion); - } + out.writeVLong(aliasesVersion); out.writeByte(state.id); assert settings != null : "settings should always be non-null since this instance is not expected to have been read from another node"; @@ -1776,9 +1770,7 @@ public static IndexMetadata readFrom(StreamInput in, @Nullable Function randomRoleMapping(true))); - TransportVersion version = TransportVersionUtils.randomVersionBetween(random(), TransportVersions.V_7_2_0, null); - BytesStreamOutput output = new BytesStreamOutput(); - output.setTransportVersion(version); - original.writeTo(output); - StreamInput streamInput = new NamedWriteableAwareStreamInput( - ByteBufferStreamInput.wrap(BytesReference.toBytes(output.bytes())), - new NamedWriteableRegistry(new XPackClientPlugin().getNamedWriteables()) - ); - streamInput.setTransportVersion(version); - RoleMappingMetadata deserialized = new RoleMappingMetadata(streamInput); - assertEquals(original, deserialized); - } - public void testEquals() { Set roleMappings1 = randomSet(0, 3, () -> randomRoleMapping(true)); Set roleMappings2 = randomSet(0, 3, () -> randomRoleMapping(true)); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/common/io/SqlStreamTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/common/io/SqlStreamTests.java index b5f23f6ab7abb..7b3a20a7d56e4 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/common/io/SqlStreamTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/common/io/SqlStreamTests.java @@ -69,7 +69,7 @@ public void testOldCursorProducesVersionMismatchError() { } public void testVersionCanBeReadByOldNodes() throws IOException { - TransportVersion version = randomFrom(TransportVersions.V_7_0_0, TransportVersions.V_7_2_1, TransportVersions.V_8_1_0); + TransportVersion version = TransportVersions.V_8_1_0; SqlStreamOutput out = SqlStreamOutput.create(version, randomZone()); out.writeString("payload"); out.close(); From b40a52035f24895625ed76a13e05184b4b38557f Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Wed, 11 Dec 2024 13:17:19 +0000 Subject: [PATCH 02/77] Add Optional Source Filtering to Source Loaders (#113827) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change introduces optional source filtering directly within source loaders (both synthetic and stored). The main benefit is seen in synthetic source loaders, as synthetic fields are stored independently. By filtering while loading the synthetic source, generating the source becomes linear in the number of fields that match the filter. This update also modifies the get document API to apply source filters earlier—directly through the source loader. The search API, however, is not affected in this change, since the loaded source is still used by other features (e.g., highlighting, fields, nested hits), and source filtering is always applied as the final step. A follow-up will be required to ensure careful handling of all search-related scenarios. --- .../subphase/FetchSourcePhaseBenchmark.java | 2 +- .../xcontent/FilterContentBenchmark.java | 2 +- docs/changelog/113827.yaml | 5 + .../XContentParserConfigurationImpl.java | 35 ++- .../xcontent/XContentParserConfiguration.java | 17 ++ .../AbstractXContentFilteringTestCase.java | 42 +++- .../action/update/UpdateHelper.java | 6 +- .../cluster/metadata/DataStream.java | 1 + .../cluster/routing/IndexRouting.java | 2 +- .../common/xcontent/XContentHelper.java | 9 +- .../xcontent/support/XContentMapValues.java | 29 +-- .../LuceneSyntheticSourceChangesSnapshot.java | 2 +- .../index/fieldvisitor/StoredFieldLoader.java | 15 +- .../index/get/ShardGetService.java | 13 +- .../index/mapper/DocumentMapper.java | 2 +- .../index/mapper/FieldAliasMapper.java | 5 - .../index/mapper/FieldMapper.java | 4 +- .../elasticsearch/index/mapper/Mapper.java | 12 - .../elasticsearch/index/mapper/Mapping.java | 9 +- .../index/mapper/MappingLookup.java | 10 +- .../index/mapper/NestedObjectMapper.java | 15 +- .../index/mapper/ObjectMapper.java | 69 ++++-- .../index/mapper/SourceFieldMapper.java | 10 - .../index/mapper/SourceLoader.java | 33 ++- .../index/mapper/XContentDataHelper.java | 96 +++++--- .../index/query/SearchExecutionContext.java | 10 +- .../fetch/subphase/FetchSourceContext.java | 7 +- .../fetch/subphase/FetchSourcePhase.java | 12 +- .../search/lookup/SourceFilter.java | 52 +++++ .../search/lookup/SourceProvider.java | 4 +- .../mapper/DocCountFieldMapperTests.java | 4 +- .../mapper/IgnoredSourceFieldMapperTests.java | 214 +++++++++++++++++- .../index/mapper/ObjectMapperTests.java | 8 +- .../index/mapper/RangeFieldMapperTests.java | 2 +- .../index/mapper/SourceFieldMetricsTests.java | 1 + .../index/mapper/SourceLoaderTests.java | 4 +- .../index/mapper/TextFieldMapperTests.java | 2 +- .../index/mapper/XContentDataHelperTests.java | 67 +++++- .../index/mapper/MapperServiceTestCase.java | 26 ++- .../index/mapper/MapperTestCase.java | 43 +++- .../action/TransportPutRollupJobAction.java | 1 + 41 files changed, 736 insertions(+), 166 deletions(-) create mode 100644 docs/changelog/113827.yaml diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/search/fetch/subphase/FetchSourcePhaseBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/search/fetch/subphase/FetchSourcePhaseBenchmark.java index 848ee6e556dc1..55b8c18138f46 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/search/fetch/subphase/FetchSourcePhaseBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/search/fetch/subphase/FetchSourcePhaseBenchmark.java @@ -63,7 +63,7 @@ public void setup() throws IOException { ); includesSet = Set.of(fetchContext.includes()); excludesSet = Set.of(fetchContext.excludes()); - parserConfig = XContentParserConfiguration.EMPTY.withFiltering(includesSet, excludesSet, false); + parserConfig = XContentParserConfiguration.EMPTY.withFiltering(null, includesSet, excludesSet, false); } private BytesReference read300BytesExample() throws IOException { diff --git a/benchmarks/src/main/java/org/elasticsearch/benchmark/xcontent/FilterContentBenchmark.java b/benchmarks/src/main/java/org/elasticsearch/benchmark/xcontent/FilterContentBenchmark.java index 334f5ef153048..aa9236e9f314f 100644 --- a/benchmarks/src/main/java/org/elasticsearch/benchmark/xcontent/FilterContentBenchmark.java +++ b/benchmarks/src/main/java/org/elasticsearch/benchmark/xcontent/FilterContentBenchmark.java @@ -170,7 +170,7 @@ private XContentParserConfiguration buildParseConfig(boolean matchDotsInFieldNam includes = null; excludes = filters; } - return XContentParserConfiguration.EMPTY.withFiltering(includes, excludes, matchDotsInFieldNames); + return XContentParserConfiguration.EMPTY.withFiltering(null, includes, excludes, matchDotsInFieldNames); } private BytesReference filter(XContentParserConfiguration contentParserConfiguration) throws IOException { diff --git a/docs/changelog/113827.yaml b/docs/changelog/113827.yaml new file mode 100644 index 0000000000000..2c05f3eeb5d6a --- /dev/null +++ b/docs/changelog/113827.yaml @@ -0,0 +1,5 @@ +pr: 113827 +summary: Add Optional Source Filtering to Source Loaders +area: Mapping +type: enhancement +issues: [] diff --git a/libs/x-content/impl/src/main/java/org/elasticsearch/xcontent/provider/XContentParserConfigurationImpl.java b/libs/x-content/impl/src/main/java/org/elasticsearch/xcontent/provider/XContentParserConfigurationImpl.java index a8e039deb38be..70adc59b9c6a9 100644 --- a/libs/x-content/impl/src/main/java/org/elasticsearch/xcontent/provider/XContentParserConfigurationImpl.java +++ b/libs/x-content/impl/src/main/java/org/elasticsearch/xcontent/provider/XContentParserConfigurationImpl.java @@ -19,6 +19,8 @@ import org.elasticsearch.xcontent.provider.filtering.FilterPathBasedFilter; import org.elasticsearch.xcontent.support.filtering.FilterPath; +import java.util.ArrayList; +import java.util.List; import java.util.Set; public class XContentParserConfigurationImpl implements XContentParserConfiguration { @@ -106,12 +108,41 @@ public XContentParserConfiguration withFiltering( Set excludeStrings, boolean filtersMatchFieldNamesWithDots ) { + return withFiltering(null, includeStrings, excludeStrings, filtersMatchFieldNamesWithDots); + } + + public XContentParserConfiguration withFiltering( + String prefixPath, + Set includeStrings, + Set excludeStrings, + boolean filtersMatchFieldNamesWithDots + ) { + FilterPath[] includePaths = FilterPath.compile(includeStrings); + FilterPath[] excludePaths = FilterPath.compile(excludeStrings); + + if (prefixPath != null) { + if (includePaths != null) { + List includeFilters = new ArrayList<>(); + for (var incl : includePaths) { + incl.matches(prefixPath, includeFilters, true); + } + includePaths = includeFilters.isEmpty() ? null : includeFilters.toArray(FilterPath[]::new); + } + + if (excludePaths != null) { + List excludeFilters = new ArrayList<>(); + for (var excl : excludePaths) { + excl.matches(prefixPath, excludeFilters, true); + } + excludePaths = excludeFilters.isEmpty() ? null : excludeFilters.toArray(FilterPath[]::new); + } + } return new XContentParserConfigurationImpl( registry, deprecationHandler, restApiVersion, - FilterPath.compile(includeStrings), - FilterPath.compile(excludeStrings), + includePaths, + excludePaths, filtersMatchFieldNamesWithDots ); } diff --git a/libs/x-content/src/main/java/org/elasticsearch/xcontent/XContentParserConfiguration.java b/libs/x-content/src/main/java/org/elasticsearch/xcontent/XContentParserConfiguration.java index a8e45e821c220..59e5cd5d6485c 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/xcontent/XContentParserConfiguration.java +++ b/libs/x-content/src/main/java/org/elasticsearch/xcontent/XContentParserConfiguration.java @@ -49,10 +49,27 @@ public interface XContentParserConfiguration { RestApiVersion restApiVersion(); + // TODO: Remove when serverless uses the new API + XContentParserConfiguration withFiltering( + Set includeStrings, + Set excludeStrings, + boolean filtersMatchFieldNamesWithDots + ); + /** * Replace the configured filtering. + * + * @param prefixPath The path to be prepended to each sub-path before applying the include/exclude rules. + * Specify {@code null} if parsing starts from the root. + * @param includeStrings A set of strings representing paths to include during filtering. + * If specified, only these paths will be included in parsing. + * @param excludeStrings A set of strings representing paths to exclude during filtering. + * If specified, these paths will be excluded from parsing. + * @param filtersMatchFieldNamesWithDots Indicates whether filters should match field names containing dots ('.') + * as part of the field name. */ XContentParserConfiguration withFiltering( + String prefixPath, Set includeStrings, Set excludeStrings, boolean filtersMatchFieldNamesWithDots diff --git a/libs/x-content/src/test/java/org/elasticsearch/xcontent/support/filtering/AbstractXContentFilteringTestCase.java b/libs/x-content/src/test/java/org/elasticsearch/xcontent/support/filtering/AbstractXContentFilteringTestCase.java index 453a4473e54c8..481a62a2cd7b9 100644 --- a/libs/x-content/src/test/java/org/elasticsearch/xcontent/support/filtering/AbstractXContentFilteringTestCase.java +++ b/libs/x-content/src/test/java/org/elasticsearch/xcontent/support/filtering/AbstractXContentFilteringTestCase.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.Set; import java.util.stream.IntStream; @@ -332,6 +333,24 @@ protected final void testFilter(Builder expected, Builder sample, Collection includes, Set excludes, boolean matchFieldNamesWithDots) throws IOException { assertFilterResult(expected.apply(createBuilder()), filter(sample, includes, excludes, matchFieldNamesWithDots)); + + String rootPrefix = "root.path.random"; + if (includes != null) { + Set rootIncludes = new HashSet<>(); + for (var incl : includes) { + rootIncludes.add(rootPrefix + (randomBoolean() ? "." : "*.") + incl); + } + includes = rootIncludes; + } + + if (excludes != null) { + Set rootExcludes = new HashSet<>(); + for (var excl : excludes) { + rootExcludes.add(rootPrefix + (randomBoolean() ? "." : "*.") + excl); + } + excludes = rootExcludes; + } + assertFilterResult(expected.apply(createBuilder()), filterSub(sample, rootPrefix, includes, excludes, matchFieldNamesWithDots)); } public void testArrayWithEmptyObjectInInclude() throws IOException { @@ -413,21 +432,36 @@ private XContentBuilder filter(Builder sample, Set includes, Set && matchFieldNamesWithDots == false) { return filterOnBuilder(sample, includes, excludes); } - return filterOnParser(sample, includes, excludes, matchFieldNamesWithDots); + return filterOnParser(sample, null, includes, excludes, matchFieldNamesWithDots); + } + + private XContentBuilder filterSub( + Builder sample, + String root, + Set includes, + Set excludes, + boolean matchFieldNamesWithDots + ) throws IOException { + return filterOnParser(sample, root, includes, excludes, matchFieldNamesWithDots); } private XContentBuilder filterOnBuilder(Builder sample, Set includes, Set excludes) throws IOException { return sample.apply(XContentBuilder.builder(getXContentType(), includes, excludes)); } - private XContentBuilder filterOnParser(Builder sample, Set includes, Set excludes, boolean matchFieldNamesWithDots) - throws IOException { + private XContentBuilder filterOnParser( + Builder sample, + String rootPath, + Set includes, + Set excludes, + boolean matchFieldNamesWithDots + ) throws IOException { try (XContentBuilder builtSample = sample.apply(createBuilder())) { BytesReference sampleBytes = BytesReference.bytes(builtSample); try ( XContentParser parser = getXContentType().xContent() .createParser( - XContentParserConfiguration.EMPTY.withFiltering(includes, excludes, matchFieldNamesWithDots), + XContentParserConfiguration.EMPTY.withFiltering(rootPath, includes, excludes, matchFieldNamesWithDots), sampleBytes.streamInput() ) ) { diff --git a/server/src/main/java/org/elasticsearch/action/update/UpdateHelper.java b/server/src/main/java/org/elasticsearch/action/update/UpdateHelper.java index d32e102b2e18b..a645c156b63c7 100644 --- a/server/src/main/java/org/elasticsearch/action/update/UpdateHelper.java +++ b/server/src/main/java/org/elasticsearch/action/update/UpdateHelper.java @@ -35,6 +35,7 @@ import org.elasticsearch.script.UpdateScript; import org.elasticsearch.script.UpsertCtxMap; import org.elasticsearch.search.lookup.Source; +import org.elasticsearch.search.lookup.SourceFilter; import org.elasticsearch.xcontent.XContentType; import java.io.IOException; @@ -340,8 +341,9 @@ public static GetResult extractGetResult( return null; } BytesReference sourceFilteredAsBytes = sourceAsBytes; - if (request.fetchSource().hasFilter()) { - sourceFilteredAsBytes = Source.fromMap(source, sourceContentType).filter(request.fetchSource().filter()).internalSourceRef(); + SourceFilter sourceFilter = request.fetchSource().filter(); + if (sourceFilter != null) { + sourceFilteredAsBytes = Source.fromMap(source, sourceContentType).filter(sourceFilter).internalSourceRef(); } // TODO when using delete/none, we can still return the source as bytes by generating it (using the sourceContentType) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java index 1c6206a4815eb..7745ec9cc75b2 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java @@ -1361,6 +1361,7 @@ public DataStream getParentDataStream() { } public static final XContentParserConfiguration TS_EXTRACT_CONFIG = XContentParserConfiguration.EMPTY.withFiltering( + null, Set.of(TIMESTAMP_FIELD_NAME), null, false diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java b/server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java index d9343909f779f..f42252df4ab7b 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/IndexRouting.java @@ -283,7 +283,7 @@ public static class ExtractFromSource extends IndexRouting { trackTimeSeriesRoutingHash = metadata.getCreationVersion().onOrAfter(IndexVersions.TIME_SERIES_ROUTING_HASH_IN_ID); List routingPaths = metadata.getRoutingPaths(); isRoutingPath = Regex.simpleMatcher(routingPaths.toArray(String[]::new)); - this.parserConfig = XContentParserConfiguration.EMPTY.withFiltering(Set.copyOf(routingPaths), null, true); + this.parserConfig = XContentParserConfiguration.EMPTY.withFiltering(null, Set.copyOf(routingPaths), null, true); } public boolean matchesField(String fieldName) { diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java b/server/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java index 9464ccbcc7aa3..c0eaee071b76c 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java @@ -192,7 +192,7 @@ public static Tuple> convertToMap( ) throws ElasticsearchParseException { XContentParserConfiguration config = XContentParserConfiguration.EMPTY; if (include != null || exclude != null) { - config = config.withFiltering(include, exclude, false); + config = config.withFiltering(null, include, exclude, false); } return parseToType(ordered ? XContentParser::mapOrdered : XContentParser::map, bytes, xContentType, config); } @@ -267,7 +267,10 @@ public static Map convertToMap( @Nullable Set exclude ) throws ElasticsearchParseException { try ( - XContentParser parser = xContent.createParser(XContentParserConfiguration.EMPTY.withFiltering(include, exclude, false), input) + XContentParser parser = xContent.createParser( + XContentParserConfiguration.EMPTY.withFiltering(null, include, exclude, false), + input + ) ) { return ordered ? parser.mapOrdered() : parser.map(); } catch (IOException e) { @@ -302,7 +305,7 @@ public static Map convertToMap( ) throws ElasticsearchParseException { try ( XContentParser parser = xContent.createParser( - XContentParserConfiguration.EMPTY.withFiltering(include, exclude, false), + XContentParserConfiguration.EMPTY.withFiltering(null, include, exclude, false), bytes, offset, length diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/support/XContentMapValues.java b/server/src/main/java/org/elasticsearch/common/xcontent/support/XContentMapValues.java index c4b03c712c272..a1ba3759c7854 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/support/XContentMapValues.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/support/XContentMapValues.java @@ -274,24 +274,8 @@ public static Map filter(Map map, String[] inclu */ public static Function, Map> filter(String[] includes, String[] excludes) { CharacterRunAutomaton matchAllAutomaton = new CharacterRunAutomaton(Automata.makeAnyString()); - - CharacterRunAutomaton include; - if (includes == null || includes.length == 0) { - include = matchAllAutomaton; - } else { - Automaton includeA = Regex.simpleMatchToAutomaton(includes); - includeA = Operations.determinize(makeMatchDotsInFieldNames(includeA), MAX_DETERMINIZED_STATES); - include = new CharacterRunAutomaton(includeA); - } - - Automaton excludeA; - if (excludes == null || excludes.length == 0) { - excludeA = Automata.makeEmpty(); - } else { - excludeA = Regex.simpleMatchToAutomaton(excludes); - excludeA = Operations.determinize(makeMatchDotsInFieldNames(excludeA), MAX_DETERMINIZED_STATES); - } - CharacterRunAutomaton exclude = new CharacterRunAutomaton(excludeA); + CharacterRunAutomaton include = compileAutomaton(includes, matchAllAutomaton); + CharacterRunAutomaton exclude = compileAutomaton(excludes, new CharacterRunAutomaton(Automata.makeEmpty())); // NOTE: We cannot use Operations.minus because of the special case that // we want all sub properties to match as soon as an object matches @@ -299,6 +283,15 @@ public static Function, Map> filter(String[] return (map) -> filter(map, include, 0, exclude, 0, matchAllAutomaton); } + public static CharacterRunAutomaton compileAutomaton(String[] patterns, CharacterRunAutomaton defaultValue) { + if (patterns == null || patterns.length == 0) { + return defaultValue; + } + var aut = Regex.simpleMatchToAutomaton(patterns); + aut = Operations.determinize(makeMatchDotsInFieldNames(aut), MAX_DETERMINIZED_STATES); + return new CharacterRunAutomaton(aut); + } + /** Make matches on objects also match dots in field names. * For instance, if the original simple regex is `foo`, this will translate * it into `foo` OR `foo.*`. */ diff --git a/server/src/main/java/org/elasticsearch/index/engine/LuceneSyntheticSourceChangesSnapshot.java b/server/src/main/java/org/elasticsearch/index/engine/LuceneSyntheticSourceChangesSnapshot.java index 3d3d2f6f66d56..f21a3c06ab015 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/LuceneSyntheticSourceChangesSnapshot.java +++ b/server/src/main/java/org/elasticsearch/index/engine/LuceneSyntheticSourceChangesSnapshot.java @@ -80,7 +80,7 @@ public LuceneSyntheticSourceChangesSnapshot( assert mappingLookup.isSourceSynthetic(); // ensure we can buffer at least one document this.maxMemorySizeInBytes = maxMemorySizeInBytes > 0 ? maxMemorySizeInBytes : 1; - this.sourceLoader = mappingLookup.newSourceLoader(SourceFieldMetrics.NOOP); + this.sourceLoader = mappingLookup.newSourceLoader(null, SourceFieldMetrics.NOOP); Set storedFields = sourceLoader.requiredStoredFields(); assert mappingLookup.isSourceSynthetic() : "synthetic source must be enabled for proper functionality."; this.storedFieldLoader = StoredFieldLoader.create(false, storedFields); diff --git a/server/src/main/java/org/elasticsearch/index/fieldvisitor/StoredFieldLoader.java b/server/src/main/java/org/elasticsearch/index/fieldvisitor/StoredFieldLoader.java index d41f65fd68fc2..52e9830037832 100644 --- a/server/src/main/java/org/elasticsearch/index/fieldvisitor/StoredFieldLoader.java +++ b/server/src/main/java/org/elasticsearch/index/fieldvisitor/StoredFieldLoader.java @@ -53,17 +53,24 @@ public static StoredFieldLoader fromSpec(StoredFieldsSpec spec) { return create(spec.requiresSource(), spec.requiredStoredFields()); } + public static StoredFieldLoader create(boolean loadSource, Set fields) { + return create(loadSource, fields, false); + } + /** * Creates a new StoredFieldLoader - * @param loadSource should this loader load the _source field - * @param fields a set of additional fields the loader should load + * + * @param loadSource indicates whether this loader should load the {@code _source} field. + * @param fields a set of additional fields that the loader should load. + * @param forceSequentialReader if {@code true}, forces the use of a sequential leaf reader; + * otherwise, uses the heuristic defined in {@link StoredFieldLoader#reader(LeafReaderContext, int[])}. */ - public static StoredFieldLoader create(boolean loadSource, Set fields) { + public static StoredFieldLoader create(boolean loadSource, Set fields, boolean forceSequentialReader) { List fieldsToLoad = fieldsToLoad(loadSource, fields); return new StoredFieldLoader() { @Override public LeafStoredFieldLoader getLoader(LeafReaderContext ctx, int[] docs) throws IOException { - return new ReaderStoredFieldLoader(reader(ctx, docs), loadSource, fields); + return new ReaderStoredFieldLoader(forceSequentialReader ? sequentialReader(ctx) : reader(ctx, docs), loadSource, fields); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/get/ShardGetService.java b/server/src/main/java/org/elasticsearch/index/get/ShardGetService.java index 41879c64c3338..43b5d2c7d3f78 100644 --- a/server/src/main/java/org/elasticsearch/index/get/ShardGetService.java +++ b/server/src/main/java/org/elasticsearch/index/get/ShardGetService.java @@ -306,9 +306,14 @@ private GetResult innerGetFetch( Map documentFields = null; Map metadataFields = null; DocIdAndVersion docIdAndVersion = get.docIdAndVersion(); + var sourceFilter = fetchSourceContext.filter(); SourceLoader loader = forceSyntheticSource - ? new SourceLoader.Synthetic(mappingLookup.getMapping()::syntheticFieldLoader, mapperMetrics.sourceFieldMetrics()) - : mappingLookup.newSourceLoader(mapperMetrics.sourceFieldMetrics()); + ? new SourceLoader.Synthetic( + sourceFilter, + () -> mappingLookup.getMapping().syntheticFieldLoader(sourceFilter), + mapperMetrics.sourceFieldMetrics() + ) + : mappingLookup.newSourceLoader(fetchSourceContext.filter(), mapperMetrics.sourceFieldMetrics()); StoredFieldLoader storedFieldLoader = buildStoredFieldLoader(storedFields, fetchSourceContext, loader); LeafStoredFieldLoader leafStoredFieldLoader = storedFieldLoader.getLoader(docIdAndVersion.reader.getContext(), null); try { @@ -367,10 +372,6 @@ private GetResult innerGetFetch( if (mapperService.mappingLookup().isSourceEnabled() && fetchSourceContext.fetchSource()) { Source source = loader.leaf(docIdAndVersion.reader, new int[] { docIdAndVersion.docId }) .source(leafStoredFieldLoader, docIdAndVersion.docId); - - if (fetchSourceContext.hasFilter()) { - source = source.filter(fetchSourceContext.filter()); - } sourceBytes = source.internalSourceRef(); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java index ecc4b92f369d6..a99fa3f93679b 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java @@ -155,7 +155,7 @@ public void validate(IndexSettings settings, boolean checkLimits) { * with the source loading strategy declared on the source field mapper. */ try { - sourceMapper().newSourceLoader(mapping(), mapperMetrics.sourceFieldMetrics()); + mappingLookup.newSourceLoader(null, mapperMetrics.sourceFieldMetrics()); } catch (IllegalArgumentException e) { mapperMetrics.sourceFieldMetrics().recordSyntheticSourceIncompatibleMapping(); throw e; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldAliasMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldAliasMapper.java index 57e1ffa322302..7ce955a441f6d 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldAliasMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldAliasMapper.java @@ -156,9 +156,4 @@ public FieldAliasMapper build(MapperBuilderContext context) { return new FieldAliasMapper(leafName(), fullName, path); } } - - @Override - public SourceLoader.SyntheticFieldLoader syntheticFieldLoader() { - return SourceLoader.SyntheticFieldLoader.NOTHING; - } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java index 4802fb5a28b58..7238127571fed 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java @@ -31,6 +31,7 @@ import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.lookup.SearchLookup; +import org.elasticsearch.search.lookup.SourceFilter; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.ToXContentFragment; import org.elasticsearch.xcontent.XContentBuilder; @@ -484,7 +485,7 @@ final SyntheticSourceMode syntheticSourceMode() { /** * Returns synthetic field loader for the mapper. * If mapper does not support synthetic source, it is handled using generic implementation - * in {@link DocumentParser#parseObjectOrField} and {@link ObjectMapper#syntheticFieldLoader()}. + * in {@link DocumentParser#parseObjectOrField} and {@link ObjectMapper#syntheticFieldLoader(SourceFilter)}. *
* * This method is final in order to support common use cases like fallback synthetic source. @@ -492,7 +493,6 @@ final SyntheticSourceMode syntheticSourceMode() { * * @return implementation of {@link SourceLoader.SyntheticFieldLoader} */ - @Override public final SourceLoader.SyntheticFieldLoader syntheticFieldLoader() { if (hasScript()) { return SourceLoader.SyntheticFieldLoader.NOTHING; diff --git a/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java b/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java index 0ecd3cc588d5b..6bc63bdbcceaf 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java @@ -172,18 +172,6 @@ public final String leafName() { */ public abstract void validate(MappingLookup mappers); - /** - * Create a {@link SourceLoader.SyntheticFieldLoader} to populate synthetic source. - * - * @throws IllegalArgumentException if the field is configured in a way that doesn't - * support synthetic source. This translates nicely into a 400 error when - * users configure synthetic source in the mapping without configuring all - * fields properly. - */ - public SourceLoader.SyntheticFieldLoader syntheticFieldLoader() { - throw new IllegalArgumentException("field [" + fullPath() + "] of type [" + typeName() + "] doesn't support synthetic source"); - } - @Override public String toString() { return Strings.toString(this); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/Mapping.java b/server/src/main/java/org/elasticsearch/index/mapper/Mapping.java index 52bc48004ccda..1278ebf0a393a 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/Mapping.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/Mapping.java @@ -13,7 +13,9 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.core.Nullable; import org.elasticsearch.index.mapper.MapperService.MergeReason; +import org.elasticsearch.search.lookup.SourceFilter; import org.elasticsearch.xcontent.ToXContentFragment; import org.elasticsearch.xcontent.XContentBuilder; @@ -22,6 +24,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -126,9 +129,9 @@ private boolean isSourceSynthetic() { return sfm != null && sfm.isSynthetic(); } - public SourceLoader.SyntheticFieldLoader syntheticFieldLoader() { - var stream = Stream.concat(Stream.of(metadataMappers), root.mappers.values().stream()); - return root.syntheticFieldLoader(stream); + public SourceLoader.SyntheticFieldLoader syntheticFieldLoader(@Nullable SourceFilter filter) { + var mappers = Stream.concat(Stream.of(metadataMappers), root.mappers.values().stream()).collect(Collectors.toList()); + return root.syntheticFieldLoader(filter, mappers, false); } /** diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java b/server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java index ce3f8cfb53184..ed02e5fc29617 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MappingLookup.java @@ -11,10 +11,12 @@ import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.InferenceFieldMetadata; +import org.elasticsearch.core.Nullable; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.analysis.IndexAnalyzers; import org.elasticsearch.index.analysis.NamedAnalyzer; import org.elasticsearch.inference.InferenceService; +import org.elasticsearch.search.lookup.SourceFilter; import java.util.ArrayList; import java.util.Collection; @@ -480,9 +482,11 @@ public boolean isSourceSynthetic() { /** * Build something to load source {@code _source}. */ - public SourceLoader newSourceLoader(SourceFieldMetrics metrics) { - SourceFieldMapper sfm = mapping.getMetadataMapperByClass(SourceFieldMapper.class); - return sfm == null ? SourceLoader.FROM_STORED_SOURCE : sfm.newSourceLoader(mapping, metrics); + public SourceLoader newSourceLoader(@Nullable SourceFilter filter, SourceFieldMetrics metrics) { + if (isSourceSynthetic()) { + return new SourceLoader.Synthetic(filter, () -> mapping.syntheticFieldLoader(filter), metrics); + } + return filter == null ? SourceLoader.FROM_STORED_SOURCE : new SourceLoader.Stored(filter); } /** diff --git a/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java index d0e0dcb6b97ba..03818f7b5c83f 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java @@ -23,10 +23,12 @@ import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.fieldvisitor.LeafStoredFieldLoader; +import org.elasticsearch.search.lookup.SourceFilter; import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; @@ -403,16 +405,18 @@ protected MapperMergeContext createChildContext(MapperMergeContext mapperMergeCo } @Override - public SourceLoader.SyntheticFieldLoader syntheticFieldLoader() { + SourceLoader.SyntheticFieldLoader syntheticFieldLoader(SourceFilter filter, Collection mappers, boolean isFragment) { + // IgnoredSourceFieldMapper integration takes care of writing the source for nested objects that enabled store_array_source. if (sourceKeepMode.orElse(SourceKeepMode.NONE) == SourceKeepMode.ALL) { // IgnoredSourceFieldMapper integration takes care of writing the source for the nested object. return SourceLoader.SyntheticFieldLoader.NOTHING; } - SourceLoader sourceLoader = new SourceLoader.Synthetic(() -> super.syntheticFieldLoader(mappers.values().stream(), true), NOOP); + SourceLoader sourceLoader = new SourceLoader.Synthetic(filter, () -> super.syntheticFieldLoader(filter, mappers, true), NOOP); // Some synthetic source use cases require using _ignored_source field var requiredStoredFields = IgnoredSourceFieldMapper.ensureLoaded(sourceLoader.requiredStoredFields(), indexSettings); - var storedFieldLoader = org.elasticsearch.index.fieldvisitor.StoredFieldLoader.create(false, requiredStoredFields); + // force sequential access since nested fields are indexed per block + var storedFieldLoader = org.elasticsearch.index.fieldvisitor.StoredFieldLoader.create(false, requiredStoredFields, true); return new NestedSyntheticFieldLoader( storedFieldLoader, sourceLoader, @@ -504,5 +508,10 @@ public void write(XContentBuilder b) throws IOException { public String fieldName() { return NestedObjectMapper.this.fullPath(); } + + @Override + public void reset() { + children.clear(); + } } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java index 023f6fcea0bfe..46b70193ba0e8 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java @@ -24,8 +24,10 @@ import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.mapper.MapperService.MergeReason; +import org.elasticsearch.search.lookup.SourceFilter; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParserConfiguration; import java.io.IOException; import java.util.ArrayList; @@ -39,6 +41,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.TreeMap; import java.util.stream.Stream; @@ -888,24 +891,40 @@ ObjectMapper findParentMapper(String leafFieldPath) { return null; } - protected SourceLoader.SyntheticFieldLoader syntheticFieldLoader(Stream mappers, boolean isFragment) { - var fields = mappers.sorted(Comparator.comparing(Mapper::fullPath)) - .map(Mapper::syntheticFieldLoader) + SourceLoader.SyntheticFieldLoader syntheticFieldLoader(SourceFilter filter, Collection mappers, boolean isFragment) { + var fields = mappers.stream() + .sorted(Comparator.comparing(Mapper::fullPath)) + .map(m -> innerSyntheticFieldLoader(filter, m)) .filter(l -> l != SourceLoader.SyntheticFieldLoader.NOTHING) .toList(); - return new SyntheticSourceFieldLoader(fields, isFragment); + return new SyntheticSourceFieldLoader(filter, fields, isFragment); } - public SourceLoader.SyntheticFieldLoader syntheticFieldLoader(Stream mappers) { - return syntheticFieldLoader(mappers, false); + final SourceLoader.SyntheticFieldLoader syntheticFieldLoader(@Nullable SourceFilter filter) { + return syntheticFieldLoader(filter, mappers.values(), false); } - @Override - public SourceLoader.SyntheticFieldLoader syntheticFieldLoader() { - return syntheticFieldLoader(mappers.values().stream()); + private SourceLoader.SyntheticFieldLoader innerSyntheticFieldLoader(SourceFilter filter, Mapper mapper) { + if (mapper instanceof MetadataFieldMapper metaMapper) { + return metaMapper.syntheticFieldLoader(); + } + if (filter != null && filter.isPathFiltered(mapper.fullPath(), mapper instanceof ObjectMapper)) { + return SourceLoader.SyntheticFieldLoader.NOTHING; + } + + if (mapper instanceof ObjectMapper objectMapper) { + return objectMapper.syntheticFieldLoader(filter); + } + + if (mapper instanceof FieldMapper fieldMapper) { + return fieldMapper.syntheticFieldLoader(); + } + return SourceLoader.SyntheticFieldLoader.NOTHING; } private class SyntheticSourceFieldLoader implements SourceLoader.SyntheticFieldLoader { + private final SourceFilter filter; + private final XContentParserConfiguration parserConfig; private final List fields; private final boolean isFragment; @@ -921,9 +940,19 @@ private class SyntheticSourceFieldLoader implements SourceLoader.SyntheticFieldL // Use an ordered map between field names and writers to order writing by field name. private TreeMap currentWriters; - private SyntheticSourceFieldLoader(List fields, boolean isFragment) { + private SyntheticSourceFieldLoader(SourceFilter filter, List fields, boolean isFragment) { this.fields = fields; this.isFragment = isFragment; + this.filter = filter; + String fullPath = ObjectMapper.this.isRoot() ? null : fullPath(); + this.parserConfig = filter == null + ? XContentParserConfiguration.EMPTY + : XContentParserConfiguration.EMPTY.withFiltering( + fullPath, + filter.getIncludes() != null ? Set.of(filter.getIncludes()) : null, + filter.getExcludes() != null ? Set.of(filter.getExcludes()) : null, + true + ); } @Override @@ -994,7 +1023,7 @@ public void prepare() { var existing = currentWriters.get(value.name()); if (existing == null) { - currentWriters.put(value.name(), new FieldWriter.IgnoredSource(value)); + currentWriters.put(value.name(), new FieldWriter.IgnoredSource(filter, value)); } else if (existing instanceof FieldWriter.IgnoredSource isw) { isw.mergeWith(value); } @@ -1031,7 +1060,10 @@ public void write(XContentBuilder b) throws IOException { // If the root object mapper is disabled, it is expected to contain // the source encapsulated within a single ignored source value. assert ignoredValues.size() == 1 : ignoredValues.size(); - XContentDataHelper.decodeAndWrite(b, ignoredValues.get(0).value()); + var value = ignoredValues.get(0).value(); + var type = XContentDataHelper.decodeType(value); + assert type.isPresent(); + XContentDataHelper.decodeAndWriteXContent(parserConfig, b, type.get(), ignoredValues.get(0).value()); softReset(); return; } @@ -1109,11 +1141,20 @@ public boolean hasValue() { } class IgnoredSource implements FieldWriter { + private final XContentParserConfiguration parserConfig; private final String fieldName; private final String leafName; private final List encodedValues; - IgnoredSource(IgnoredSourceFieldMapper.NameValue initialValue) { + IgnoredSource(SourceFilter filter, IgnoredSourceFieldMapper.NameValue initialValue) { + parserConfig = filter == null + ? XContentParserConfiguration.EMPTY + : XContentParserConfiguration.EMPTY.withFiltering( + initialValue.name(), + filter.getIncludes() != null ? Set.of(filter.getIncludes()) : null, + filter.getExcludes() != null ? Set.of(filter.getExcludes()) : null, + true + ); this.fieldName = initialValue.name(); this.leafName = initialValue.getFieldName(); this.encodedValues = new ArrayList<>(); @@ -1124,7 +1165,7 @@ class IgnoredSource implements FieldWriter { @Override public void writeTo(XContentBuilder builder) throws IOException { - XContentDataHelper.writeMerged(builder, leafName, encodedValues); + XContentDataHelper.writeMerged(parserConfig, builder, leafName, encodedValues); } @Override diff --git a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java index d491eb9de5886..85f4217811a84 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/SourceFieldMapper.java @@ -454,16 +454,6 @@ public FieldMapper.Builder getMergeBuilder() { return new Builder(null, Settings.EMPTY, false, serializeMode).init(this); } - /** - * Build something to load source {@code _source}. - */ - public SourceLoader newSourceLoader(Mapping mapping, SourceFieldMetrics metrics) { - if (mode == Mode.SYNTHETIC) { - return new SourceLoader.Synthetic(mapping::syntheticFieldLoader, metrics); - } - return SourceLoader.FROM_STORED_SOURCE; - } - public boolean isSynthetic() { return mode == Mode.SYNTHETIC; } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/SourceLoader.java b/server/src/main/java/org/elasticsearch/index/mapper/SourceLoader.java index ec255a53e7c5a..27b4f4eb0ae76 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/SourceLoader.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/SourceLoader.java @@ -11,9 +11,11 @@ import org.apache.lucene.index.LeafReader; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.fieldvisitor.LeafStoredFieldLoader; import org.elasticsearch.search.lookup.Source; +import org.elasticsearch.search.lookup.SourceFilter; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.json.JsonXContent; @@ -71,7 +73,15 @@ interface Leaf { /** * Load {@code _source} from a stored field. */ - SourceLoader FROM_STORED_SOURCE = new SourceLoader() { + SourceLoader FROM_STORED_SOURCE = new Stored(null); + + class Stored implements SourceLoader { + final SourceFilter filter; + + public Stored(@Nullable SourceFilter filter) { + this.filter = filter; + } + @Override public boolean reordersFieldValues() { return false; @@ -82,7 +92,8 @@ public Leaf leaf(LeafReader reader, int[] docIdsInLeaf) { return new Leaf() { @Override public Source source(LeafStoredFieldLoader storedFields, int docId) throws IOException { - return Source.fromBytes(storedFields.source()); + var res = Source.fromBytes(storedFields.source()); + return filter == null ? res : res.filter(filter); } @Override @@ -97,28 +108,31 @@ public void write(LeafStoredFieldLoader storedFields, int docId, XContentBuilder public Set requiredStoredFields() { return Set.of(); } - }; + } /** * Reconstructs {@code _source} from doc values anf stored fields. */ class Synthetic implements SourceLoader { + private final SourceFilter filter; private final Supplier syntheticFieldLoaderLeafSupplier; private final Set requiredStoredFields; private final SourceFieldMetrics metrics; /** * Creates a {@link SourceLoader} to reconstruct {@code _source} from doc values anf stored fields. + * @param filter An optional filter to include/exclude fields. * @param fieldLoaderSupplier A supplier to create {@link SyntheticFieldLoader}, one for each leaf. * @param metrics Metrics for profiling. */ - public Synthetic(Supplier fieldLoaderSupplier, SourceFieldMetrics metrics) { + public Synthetic(@Nullable SourceFilter filter, Supplier fieldLoaderSupplier, SourceFieldMetrics metrics) { this.syntheticFieldLoaderLeafSupplier = fieldLoaderSupplier; this.requiredStoredFields = syntheticFieldLoaderLeafSupplier.get() .storedFieldLoaders() .map(Map.Entry::getKey) .collect(Collectors.toSet()); this.metrics = metrics; + this.filter = filter; } @Override @@ -134,7 +148,7 @@ public Set requiredStoredFields() { @Override public Leaf leaf(LeafReader reader, int[] docIdsInLeaf) throws IOException { SyntheticFieldLoader loader = syntheticFieldLoaderLeafSupplier.get(); - return new LeafWithMetrics(new SyntheticLeaf(loader, loader.docValuesLoader(reader, docIdsInLeaf)), metrics); + return new LeafWithMetrics(new SyntheticLeaf(filter, loader, loader.docValuesLoader(reader, docIdsInLeaf)), metrics); } private record LeafWithMetrics(Leaf leaf, SourceFieldMetrics metrics) implements Leaf { @@ -163,11 +177,13 @@ public void write(LeafStoredFieldLoader storedFields, int docId, XContentBuilder } private static class SyntheticLeaf implements Leaf { + private final SourceFilter filter; private final SyntheticFieldLoader loader; private final SyntheticFieldLoader.DocValuesLoader docValuesLoader; private final Map storedFieldLoaders; - private SyntheticLeaf(SyntheticFieldLoader loader, SyntheticFieldLoader.DocValuesLoader docValuesLoader) { + private SyntheticLeaf(SourceFilter filter, SyntheticFieldLoader loader, SyntheticFieldLoader.DocValuesLoader docValuesLoader) { + this.filter = filter; this.loader = loader; this.docValuesLoader = docValuesLoader; this.storedFieldLoaders = Map.copyOf( @@ -199,6 +215,11 @@ public void write(LeafStoredFieldLoader storedFieldLoader, int docId, XContentBu objectsWithIgnoredFields = new HashMap<>(); } IgnoredSourceFieldMapper.NameValue nameValue = IgnoredSourceFieldMapper.decode(value); + if (filter != null + && filter.isPathFiltered(nameValue.name(), XContentDataHelper.isEncodedObject(nameValue.value()))) { + // This path is filtered by the include/exclude rules + continue; + } objectsWithIgnoredFields.computeIfAbsent(nameValue.getParentFieldName(), k -> new ArrayList<>()).add(nameValue); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/XContentDataHelper.java b/server/src/main/java/org/elasticsearch/index/mapper/XContentDataHelper.java index dee5ff92040a9..646368b96a4c5 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/XContentDataHelper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/XContentDataHelper.java @@ -16,6 +16,7 @@ import org.elasticsearch.common.util.ByteUtils; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.CheckedFunction; +import org.elasticsearch.core.CheckedRunnable; import org.elasticsearch.core.Tuple; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; @@ -109,29 +110,53 @@ static void decodeAndWrite(XContentBuilder b, BytesRef r) throws IOException { } } + /** + * Determines if the given {@link BytesRef}, encoded with {@link XContentDataHelper#encodeToken(XContentParser)}, + * is an encoded object. + */ + static boolean isEncodedObject(BytesRef encoded) { + return switch ((char) encoded.bytes[encoded.offset]) { + case CBOR_OBJECT_ENCODING, YAML_OBJECT_ENCODING, JSON_OBJECT_ENCODING, SMILE_OBJECT_ENCODING -> true; + default -> false; + }; + } + + static Optional decodeType(BytesRef encodedValue) { + return switch ((char) encodedValue.bytes[encodedValue.offset]) { + case CBOR_OBJECT_ENCODING, JSON_OBJECT_ENCODING, YAML_OBJECT_ENCODING, SMILE_OBJECT_ENCODING -> Optional.of( + getXContentType(encodedValue) + ); + default -> Optional.empty(); + }; + } + /** * Writes encoded values to provided builder. If there are multiple values they are merged into * a single resulting array. * * Note that this method assumes all encoded parts have values that need to be written (are not VOID encoded). + * @param parserConfig The configuration for the parsing of the provided {@code encodedParts}. * @param b destination * @param fieldName name of the field that is written * @param encodedParts subset of field data encoded using methods of this class. Can contain arrays which will be flattened. * @throws IOException */ - static void writeMerged(XContentBuilder b, String fieldName, List encodedParts) throws IOException { + static void writeMerged(XContentParserConfiguration parserConfig, XContentBuilder b, String fieldName, List encodedParts) + throws IOException { if (encodedParts.isEmpty()) { return; } - if (encodedParts.size() == 1) { - b.field(fieldName); - XContentDataHelper.decodeAndWrite(b, encodedParts.get(0)); - return; - } - - b.startArray(fieldName); + boolean isArray = encodedParts.size() > 1; + // xcontent filtering can remove all values so we delay the start of the field until we have an actual value to write. + CheckedRunnable startField = () -> { + if (isArray) { + b.startArray(fieldName); + } else { + b.field(fieldName); + } + }; for (var encodedValue : encodedParts) { Optional encodedXContentType = switch ((char) encodedValue.bytes[encodedValue.offset]) { case CBOR_OBJECT_ENCODING, JSON_OBJECT_ENCODING, YAML_OBJECT_ENCODING, SMILE_OBJECT_ENCODING -> Optional.of( @@ -140,27 +165,33 @@ static void writeMerged(XContentBuilder b, String fieldName, List enco default -> Optional.empty(); }; if (encodedXContentType.isEmpty()) { + if (startField != null) { + // first value to write + startField.run(); + startField = null; + } // This is a plain value, we can just write it XContentDataHelper.decodeAndWrite(b, encodedValue); } else { - // Encoded value could be an array which needs to be flattened - // since we are already inside an array. + // Encoded value could be an object or an array of objects that needs + // to be filtered or flattened. try ( XContentParser parser = encodedXContentType.get() .xContent() - .createParser( - XContentParserConfiguration.EMPTY, - encodedValue.bytes, - encodedValue.offset + 1, - encodedValue.length - 1 - ) + .createParser(parserConfig, encodedValue.bytes, encodedValue.offset + 1, encodedValue.length - 1) ) { - if (parser.currentToken() == null) { - parser.nextToken(); + if ((parser.currentToken() == null) && (parser.nextToken() == null)) { + // the entire content is filtered by include/exclude rules + continue; } - // It's an array, we will flatten it. - if (parser.currentToken() == XContentParser.Token.START_ARRAY) { + if (startField != null) { + // first value to write + startField.run(); + startField = null; + } + if (isArray && parser.currentToken() == XContentParser.Token.START_ARRAY) { + // Encoded value is an array which needs to be flattened since we are already inside an array. while (parser.nextToken() != XContentParser.Token.END_ARRAY) { b.copyCurrentStructure(parser); } @@ -171,8 +202,9 @@ static void writeMerged(XContentBuilder b, String fieldName, List enco } } } - - b.endArray(); + if (isArray) { + b.endArray(); + } } public static boolean isDataPresent(BytesRef encoded) { @@ -509,10 +541,10 @@ byte[] encode(XContentParser parser) throws IOException { @Override void decodeAndWrite(XContentBuilder b, BytesRef r) throws IOException { switch ((char) r.bytes[r.offset]) { - case CBOR_OBJECT_ENCODING -> decodeAndWriteXContent(b, XContentType.CBOR, r); - case JSON_OBJECT_ENCODING -> decodeAndWriteXContent(b, XContentType.JSON, r); - case SMILE_OBJECT_ENCODING -> decodeAndWriteXContent(b, XContentType.SMILE, r); - case YAML_OBJECT_ENCODING -> decodeAndWriteXContent(b, XContentType.YAML, r); + case CBOR_OBJECT_ENCODING -> decodeAndWriteXContent(XContentParserConfiguration.EMPTY, b, XContentType.CBOR, r); + case JSON_OBJECT_ENCODING -> decodeAndWriteXContent(XContentParserConfiguration.EMPTY, b, XContentType.JSON, r); + case SMILE_OBJECT_ENCODING -> decodeAndWriteXContent(XContentParserConfiguration.EMPTY, b, XContentType.SMILE, r); + case YAML_OBJECT_ENCODING -> decodeAndWriteXContent(XContentParserConfiguration.EMPTY, b, XContentType.YAML, r); default -> throw new IllegalArgumentException("Can't decode " + r); } } @@ -606,11 +638,15 @@ static byte[] encode(XContentBuilder builder) throws IOException { assert position == encoded.length; return encoded; } + } - static void decodeAndWriteXContent(XContentBuilder b, XContentType type, BytesRef r) throws IOException { - try ( - XContentParser parser = type.xContent().createParser(XContentParserConfiguration.EMPTY, r.bytes, r.offset + 1, r.length - 1) - ) { + public static void decodeAndWriteXContent(XContentParserConfiguration parserConfig, XContentBuilder b, XContentType type, BytesRef r) + throws IOException { + try (XContentParser parser = type.xContent().createParser(parserConfig, r.bytes, r.offset + 1, r.length - 1)) { + if ((parser.currentToken() == null) && (parser.nextToken() == null)) { + // This can occur when all fields in a sub-object or all entries in an array of objects have been filtered out. + b.startObject().endObject(); + } else { b.copyCurrentStructure(parser); } } diff --git a/server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java b/server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java index d5e48a6a54daa..fbc3696d40221 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java +++ b/server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java @@ -439,9 +439,13 @@ public boolean isSourceSynthetic() { */ public SourceLoader newSourceLoader(boolean forceSyntheticSource) { if (forceSyntheticSource) { - return new SourceLoader.Synthetic(mappingLookup.getMapping()::syntheticFieldLoader, mapperMetrics.sourceFieldMetrics()); + return new SourceLoader.Synthetic( + null, + () -> mappingLookup.getMapping().syntheticFieldLoader(null), + mapperMetrics.sourceFieldMetrics() + ); } - return mappingLookup.newSourceLoader(mapperMetrics.sourceFieldMetrics()); + return mappingLookup.newSourceLoader(null, mapperMetrics.sourceFieldMetrics()); } /** @@ -501,7 +505,7 @@ public SearchLookup lookup() { public SourceProvider createSourceProvider() { return isSourceSynthetic() - ? SourceProvider.fromSyntheticSource(mappingLookup.getMapping(), mapperMetrics.sourceFieldMetrics()) + ? SourceProvider.fromSyntheticSource(mappingLookup.getMapping(), null, mapperMetrics.sourceFieldMetrics()) : SourceProvider.fromStoredFields(); } diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/FetchSourceContext.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/FetchSourceContext.java index 126c7aa28f4d1..0594fa4909783 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/FetchSourceContext.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/FetchSourceContext.java @@ -85,12 +85,15 @@ public String[] excludes() { return this.excludes; } - public boolean hasFilter() { + private boolean hasFilter() { return this.includes.length > 0 || this.excludes.length > 0; } + /** + * Returns a {@link SourceFilter} if filtering is enabled, {@code null} otherwise. + */ public SourceFilter filter() { - return new SourceFilter(includes, excludes); + return hasFilter() ? new SourceFilter(includes, excludes) : null; } public static FetchSourceContext parseFromRestRequest(RestRequest request) { diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/FetchSourcePhase.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/FetchSourcePhase.java index e151f0fc2e090..79e51036a91be 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/FetchSourcePhase.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/FetchSourcePhase.java @@ -29,7 +29,7 @@ public FetchSubPhaseProcessor getProcessor(FetchContext fetchContext) { } assert fetchSourceContext.fetchSource(); SourceFilter sourceFilter = fetchSourceContext.filter(); - final boolean filterExcludesAll = sourceFilter.excludesAll(); + final boolean filterExcludesAll = sourceFilter != null && sourceFilter.excludesAll(); return new FetchSubPhaseProcessor() { private int fastPath; @@ -47,22 +47,22 @@ public StoredFieldsSpec storedFieldsSpec() { public void process(HitContext hitContext) { String index = fetchContext.getIndexName(); if (fetchContext.getSearchExecutionContext().isSourceEnabled() == false) { - if (fetchSourceContext.hasFilter()) { + if (sourceFilter != null) { throw new IllegalArgumentException( "unable to fetch fields from _source field: _source is disabled in the mappings for index [" + index + "]" ); } return; } - hitExecute(fetchSourceContext, hitContext); + hitExecute(hitContext); } - private void hitExecute(FetchSourceContext fetchSourceContext, HitContext hitContext) { + private void hitExecute(HitContext hitContext) { final boolean nestedHit = hitContext.hit().getNestedIdentity() != null; Source source = hitContext.source(); // If this is a parent document and there are no source filters, then add the source as-is. - if (nestedHit == false && fetchSourceContext.hasFilter() == false) { + if (nestedHit == false && sourceFilter == null) { hitContext.hit().sourceRef(source.internalSourceRef()); fastPath++; return; @@ -73,7 +73,7 @@ private void hitExecute(FetchSourceContext fetchSourceContext, HitContext hitCon source = Source.empty(source.sourceContentType()); } else { // Otherwise, filter the source and add it to the hit. - source = source.filter(sourceFilter); + source = sourceFilter != null ? source.filter(sourceFilter) : source; } if (nestedHit) { source = extractNested(source, hitContext.hit().getNestedIdentity()); diff --git a/server/src/main/java/org/elasticsearch/search/lookup/SourceFilter.java b/server/src/main/java/org/elasticsearch/search/lookup/SourceFilter.java index d3951700f3c9f..90034ef447c92 100644 --- a/server/src/main/java/org/elasticsearch/search/lookup/SourceFilter.java +++ b/server/src/main/java/org/elasticsearch/search/lookup/SourceFilter.java @@ -9,6 +9,8 @@ package org.elasticsearch.search.lookup; +import org.apache.lucene.util.automaton.Automata; +import org.apache.lucene.util.automaton.CharacterRunAutomaton; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; @@ -40,6 +42,8 @@ public final class SourceFilter { private final boolean empty; private final String[] includes; private final String[] excludes; + private CharacterRunAutomaton includeAut; + private CharacterRunAutomaton excludeAut; /** * Construct a new filter based on a list of includes and excludes @@ -56,6 +60,53 @@ public SourceFilter(String[] includes, String[] excludes) { this.empty = CollectionUtils.isEmpty(this.includes) && CollectionUtils.isEmpty(this.excludes); } + public String[] getIncludes() { + return includes; + } + + public String[] getExcludes() { + return excludes; + } + + /** + * Determines whether the given full path should be filtered out. + * + * @param fullPath The full path to evaluate. + * @param isObject Indicates if the path represents an object. + * @return {@code true} if the path should be filtered out, {@code false} otherwise. + */ + public boolean isPathFiltered(String fullPath, boolean isObject) { + final boolean included; + if (includes != null) { + if (includeAut == null) { + includeAut = XContentMapValues.compileAutomaton(includes, new CharacterRunAutomaton(Automata.makeAnyString())); + } + int state = step(includeAut, fullPath, 0); + included = state != -1 && (isObject || includeAut.isAccept(state)); + } else { + included = true; + } + + if (excludes != null) { + if (excludeAut == null) { + excludeAut = XContentMapValues.compileAutomaton(excludes, new CharacterRunAutomaton(Automata.makeEmpty())); + } + int state = step(excludeAut, fullPath, 0); + if (state != -1 && excludeAut.isAccept(state)) { + return true; + } + } + + return included == false; + } + + private static int step(CharacterRunAutomaton automaton, String key, int state) { + for (int i = 0; state != -1 && i < key.length(); ++i) { + state = automaton.step(state, key.charAt(i)); + } + return state; + } + /** * Filter a Source using its map representation */ @@ -87,6 +138,7 @@ private Function buildBytesFilter() { return this::filterMap; } final XContentParserConfiguration parserConfig = XContentParserConfiguration.EMPTY.withFiltering( + null, Set.copyOf(Arrays.asList(includes)), Set.copyOf(Arrays.asList(excludes)), true diff --git a/server/src/main/java/org/elasticsearch/search/lookup/SourceProvider.java b/server/src/main/java/org/elasticsearch/search/lookup/SourceProvider.java index e232aec5d1f6c..4696ef2299fd7 100644 --- a/server/src/main/java/org/elasticsearch/search/lookup/SourceProvider.java +++ b/server/src/main/java/org/elasticsearch/search/lookup/SourceProvider.java @@ -48,7 +48,7 @@ static SourceProvider fromStoredFields() { * but it is not safe to use this to access documents from the same segment across * multiple threads. */ - static SourceProvider fromSyntheticSource(Mapping mapping, SourceFieldMetrics metrics) { - return new SyntheticSourceProvider(new SourceLoader.Synthetic(mapping::syntheticFieldLoader, metrics)); + static SourceProvider fromSyntheticSource(Mapping mapping, SourceFilter filter, SourceFieldMetrics metrics) { + return new SyntheticSourceProvider(new SourceLoader.Synthetic(filter, () -> mapping.syntheticFieldLoader(filter), metrics)); } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DocCountFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DocCountFieldMapperTests.java index 4101828d4cd24..84f17c2fc3d6a 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DocCountFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DocCountFieldMapperTests.java @@ -98,7 +98,7 @@ public void testSyntheticSourceMany() throws IOException { iw.addDocument(mapper.documentMapper().parse(source(b -> b.field("doc", doc).field(CONTENT_TYPE, c))).rootDoc()); } }, reader -> { - SourceLoader loader = mapper.mappingLookup().newSourceLoader(SourceFieldMetrics.NOOP); + SourceLoader loader = mapper.mappingLookup().newSourceLoader(null, SourceFieldMetrics.NOOP); assertThat(loader.requiredStoredFields(), Matchers.contains("_ignored_source")); for (LeafReaderContext leaf : reader.leaves()) { int[] docIds = IntStream.range(0, leaf.reader().maxDoc()).toArray(); @@ -130,7 +130,7 @@ public void testSyntheticSourceManyDoNotHave() throws IOException { })).rootDoc()); } }, reader -> { - SourceLoader loader = mapper.mappingLookup().newSourceLoader(SourceFieldMetrics.NOOP); + SourceLoader loader = mapper.mappingLookup().newSourceLoader(null, SourceFieldMetrics.NOOP); assertThat(loader.requiredStoredFields(), Matchers.contains("_ignored_source")); for (LeafReaderContext leaf : reader.leaves()) { int[] docIds = IntStream.range(0, leaf.reader().maxDoc()).toArray(); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/IgnoredSourceFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/IgnoredSourceFieldMapperTests.java index b43371594d57b..14902aa419b9f 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/IgnoredSourceFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/IgnoredSourceFieldMapperTests.java @@ -12,6 +12,8 @@ import org.apache.lucene.index.DirectoryReader; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.CheckedConsumer; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.search.lookup.SourceFilter; import org.elasticsearch.test.FieldMaskingReader; import org.elasticsearch.xcontent.XContentBuilder; import org.hamcrest.Matchers; @@ -46,8 +48,15 @@ private ParsedDocument getParsedDocumentWithFieldLimit(CheckedConsumer build) throws IOException { + return getSyntheticSourceWithFieldLimit(null, build); + } + + private String getSyntheticSourceWithFieldLimit( + @Nullable SourceFilter sourceFilter, + CheckedConsumer build + ) throws IOException { DocumentMapper documentMapper = getDocumentMapperWithFieldLimit(); - return syntheticSource(documentMapper, build); + return syntheticSource(documentMapper, sourceFilter, build); } private MapperService createMapperServiceWithStoredArraySource(XContentBuilder mappings) throws IOException { @@ -62,36 +71,120 @@ private MapperService createMapperServiceWithStoredArraySource(XContentBuilder m public void testIgnoredBoolean() throws IOException { boolean value = randomBoolean(); assertEquals("{\"my_value\":" + value + "}", getSyntheticSourceWithFieldLimit(b -> b.field("my_value", value))); + assertEquals( + "{\"my_value\":" + value + "}", + getSyntheticSourceWithFieldLimit(new SourceFilter(new String[] { "my_value" }, null), b -> b.field("my_value", value)) + ); + assertEquals( + "{}", + getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_value" }), b -> b.field("my_value", value)) + ); + } + + public void testIgnoredBooleanArray() throws IOException { + assertEquals( + "{\"my_value\":[false,true,false]}", + getSyntheticSourceWithFieldLimit(b -> b.field("my_value", new boolean[] { false, true, false })) + ); + assertEquals( + "{\"my_value\":[false,true,false]}", + getSyntheticSourceWithFieldLimit( + new SourceFilter(new String[] { "my_value" }, null), + b -> b.array("my_value", new boolean[] { false, true, false }) + ) + ); + assertEquals( + "{}", + getSyntheticSourceWithFieldLimit( + new SourceFilter(null, new String[] { "my_value" }), + b -> b.field("my_value", new boolean[] { false, true, false }) + ) + ); + assertEquals( + "{}", + getSyntheticSourceWithFieldLimit( + new SourceFilter(new String[] { "my_value.object" }, null), + b -> b.array("my_value", new boolean[] { false, true, false }) + ) + ); } public void testIgnoredString() throws IOException { String value = randomAlphaOfLength(5); assertEquals("{\"my_value\":\"" + value + "\"}", getSyntheticSourceWithFieldLimit(b -> b.field("my_value", value))); + assertEquals( + "{\"my_value\":\"" + value + "\"}", + getSyntheticSourceWithFieldLimit(new SourceFilter(new String[] { "my_value" }, null), b -> b.field("my_value", value)) + ); + assertEquals( + "{}", + getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_value" }), b -> b.field("my_value", value)) + ); } public void testIgnoredInt() throws IOException { int value = randomInt(); assertEquals("{\"my_value\":" + value + "}", getSyntheticSourceWithFieldLimit(b -> b.field("my_value", value))); + assertEquals( + "{\"my_value\":" + value + "}", + getSyntheticSourceWithFieldLimit(new SourceFilter(new String[] { "my_value" }, null), b -> b.field("my_value", value)) + ); + assertEquals( + "{}", + getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_value" }), b -> b.field("my_value", value)) + ); } public void testIgnoredLong() throws IOException { long value = randomLong(); assertEquals("{\"my_value\":" + value + "}", getSyntheticSourceWithFieldLimit(b -> b.field("my_value", value))); + assertEquals( + "{\"my_value\":" + value + "}", + getSyntheticSourceWithFieldLimit(new SourceFilter(new String[] { "my_value" }, null), b -> b.field("my_value", value)) + ); + assertEquals( + "{}", + getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_value" }), b -> b.field("my_value", value)) + ); } public void testIgnoredFloat() throws IOException { float value = randomFloat(); assertEquals("{\"my_value\":" + value + "}", getSyntheticSourceWithFieldLimit(b -> b.field("my_value", value))); + assertEquals( + "{\"my_value\":" + value + "}", + getSyntheticSourceWithFieldLimit(new SourceFilter(new String[] { "my_value" }, null), b -> b.field("my_value", value)) + ); + assertEquals( + "{}", + getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_value" }), b -> b.field("my_value", value)) + ); } public void testIgnoredDouble() throws IOException { double value = randomDouble(); assertEquals("{\"my_value\":" + value + "}", getSyntheticSourceWithFieldLimit(b -> b.field("my_value", value))); + assertEquals( + "{\"my_value\":" + value + "}", + getSyntheticSourceWithFieldLimit(new SourceFilter(new String[] { "my_value" }, null), b -> b.field("my_value", value)) + ); + assertEquals( + "{}", + getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_value" }), b -> b.field("my_value", value)) + ); } public void testIgnoredBigInteger() throws IOException { BigInteger value = randomBigInteger(); assertEquals("{\"my_value\":" + value + "}", getSyntheticSourceWithFieldLimit(b -> b.field("my_value", value))); + assertEquals( + "{\"my_value\":" + value + "}", + getSyntheticSourceWithFieldLimit(new SourceFilter(new String[] { "my_value" }, null), b -> b.field("my_value", value)) + ); + assertEquals( + "{}", + getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_value" }), b -> b.field("my_value", value)) + ); } public void testIgnoredBytes() throws IOException { @@ -100,6 +193,14 @@ public void testIgnoredBytes() throws IOException { "{\"my_value\":\"" + Base64.getEncoder().encodeToString(value) + "\"}", getSyntheticSourceWithFieldLimit(b -> b.field("my_value", value)) ); + assertEquals( + "{\"my_value\":\"" + Base64.getEncoder().encodeToString(value) + "\"}", + getSyntheticSourceWithFieldLimit(new SourceFilter(new String[] { "my_value" }, null), b -> b.field("my_value", value)) + ); + assertEquals( + "{}", + getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_value" }), b -> b.field("my_value", value)) + ); } public void testIgnoredObjectBoolean() throws IOException { @@ -107,15 +208,124 @@ public void testIgnoredObjectBoolean() throws IOException { assertEquals("{\"my_object\":{\"my_value\":" + value + "}}", getSyntheticSourceWithFieldLimit(b -> { b.startObject("my_object").field("my_value", value).endObject(); })); + + assertEquals( + "{\"my_object\":{\"my_value\":" + value + "}}", + getSyntheticSourceWithFieldLimit(new SourceFilter(new String[] { "my_object" }, null), b -> { + b.startObject("my_object").field("my_value", value).endObject(); + }) + ); + + assertEquals( + "{\"my_object\":{\"my_value\":" + value + "}}", + getSyntheticSourceWithFieldLimit(new SourceFilter(new String[] { "my_object.my_value" }, null), b -> { + b.startObject("my_object").field("my_value", value).endObject(); + }) + ); + + assertEquals("{}", getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_object" }), b -> { + b.startObject("my_object").field("my_value", value).endObject(); + })); + + assertEquals("{}", getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_object.my_value" }), b -> { + b.startObject("my_object").field("my_value", value).endObject(); + })); + + assertEquals( + "{\"my_object\":{\"another_value\":\"0\"}}", + getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_object.my_value" }), b -> { + b.startObject("my_object").field("my_value", value).field("another_value", "0").endObject(); + }) + ); + } + + public void testIgnoredArrayOfObjects() throws IOException { + boolean value = randomBoolean(); + int another_value = randomInt(); + + assertEquals( + "{\"my_object\":[{\"my_value\":" + value + "},{\"another_value\":" + another_value + "}]}", + getSyntheticSourceWithFieldLimit(b -> { + b.startArray("my_object"); + b.startObject().field("my_value", value).endObject(); + b.startObject().field("another_value", another_value).endObject(); + b.endArray(); + }) + ); + + assertEquals( + "{\"my_object\":[{\"another_value\":" + another_value + "}]}", + getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_object.my_value" }), b -> { + b.startArray("my_object"); + b.startObject().field("my_value", value).endObject(); + b.startObject().field("another_value", another_value).endObject(); + b.endArray(); + }) + ); + + assertEquals( + "{\"my_object\":[{\"my_value\":" + value + "}]}", + getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_object.another_value" }), b -> { + b.startArray("my_object"); + b.startObject().field("my_value", value).endObject(); + b.startObject().field("another_value", another_value).endObject(); + b.endArray(); + }) + ); + + assertEquals( + "{}", + getSyntheticSourceWithFieldLimit( + new SourceFilter(null, new String[] { "my_object.another_value", "my_object.my_value" }), + b -> { + b.startArray("my_object"); + b.startObject().field("my_value", value).endObject(); + b.startObject().field("another_value", another_value).endObject(); + b.endArray(); + } + ) + ); + + assertEquals( + "{\"my_object\":[{\"another_field2\":2}]}", + getSyntheticSourceWithFieldLimit( + new SourceFilter(null, new String[] { "my_object.another_field1", "my_object.my_value" }), + b -> { + b.startArray("my_object"); + b.startObject().field("my_value", value).endObject(); + b.startObject().field("another_field1", 1).endObject(); + b.startObject().field("another_field2", 2).endObject(); + b.endArray(); + } + ) + ); + + assertEquals("{}", getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_object" }), b -> { + b.startArray("my_object"); + b.startObject().field("my_value", value).endObject(); + b.startObject().field("another_value", another_value).endObject(); + b.endArray(); + })); } public void testIgnoredArray() throws IOException { - assertEquals("{\"my_array\":[{\"int_value\":10},{\"int_value\":20}]}", getSyntheticSourceWithFieldLimit(b -> { + assertEquals( + "{\"my_array\":[{\"int_value\":10},{\"int_value\":20}]}", + getSyntheticSourceWithFieldLimit(new SourceFilter(new String[] { "my_array" }, null), b -> { + b.startArray("my_array"); + b.startObject().field("int_value", 10).endObject(); + b.startObject().field("int_value", 20).endObject(); + b.endArray(); + }) + ); + + assertEquals("{}", getSyntheticSourceWithFieldLimit(new SourceFilter(null, new String[] { "my_array" }), b -> { b.startArray("my_array"); b.startObject().field("int_value", 10).endObject(); b.startObject().field("int_value", 20).endObject(); b.endArray(); })); + } public void testEncodeFieldToMap() throws IOException { diff --git a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java index 527d7497a8418..911fe6d4b9337 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java @@ -654,8 +654,8 @@ public void testSubobjectsAutoRootWithInnerNested() throws IOException { public void testSyntheticSourceDocValuesEmpty() throws IOException { DocumentMapper mapper = createDocumentMapper(mapping(b -> b.startObject("o").field("type", "object").endObject())); ObjectMapper o = (ObjectMapper) mapper.mapping().getRoot().getMapper("o"); - assertThat(o.syntheticFieldLoader().docValuesLoader(null, null), nullValue()); - assertThat(mapper.mapping().getRoot().syntheticFieldLoader().docValuesLoader(null, null), nullValue()); + assertThat(o.syntheticFieldLoader(null).docValuesLoader(null, null), nullValue()); + assertThat(mapper.mapping().getRoot().syntheticFieldLoader(null).docValuesLoader(null, null), nullValue()); } /** @@ -680,8 +680,8 @@ public void testSyntheticSourceDocValuesFieldWithout() throws IOException { b.endObject().endObject(); })); ObjectMapper o = (ObjectMapper) mapper.mapping().getRoot().getMapper("o"); - assertThat(o.syntheticFieldLoader().docValuesLoader(null, null), nullValue()); - assertThat(mapper.mapping().getRoot().syntheticFieldLoader().docValuesLoader(null, null), nullValue()); + assertThat(o.syntheticFieldLoader(null).docValuesLoader(null, null), nullValue()); + assertThat(mapper.mapping().getRoot().syntheticFieldLoader(null).docValuesLoader(null, null), nullValue()); } public void testStoreArraySourceinSyntheticSourceMode() throws IOException { diff --git a/server/src/test/java/org/elasticsearch/index/mapper/RangeFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/RangeFieldMapperTests.java index 3a091bf539229..c36a126479e87 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/RangeFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/RangeFieldMapperTests.java @@ -408,7 +408,7 @@ protected Source getSourceFor(CheckedConsumer mapp iw.addDocument(doc); iw.close(); try (DirectoryReader reader = DirectoryReader.open(directory)) { - SourceProvider provider = SourceProvider.fromSyntheticSource(mapper.mapping(), SourceFieldMetrics.NOOP); + SourceProvider provider = SourceProvider.fromSyntheticSource(mapper.mapping(), null, SourceFieldMetrics.NOOP); Source syntheticSource = provider.getSource(getOnlyLeafReader(reader).getContext(), 0); return syntheticSource; diff --git a/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMetricsTests.java b/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMetricsTests.java index c640cea16487b..ea9f8f6ae28a7 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMetricsTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMetricsTests.java @@ -47,6 +47,7 @@ public void testSyntheticSourceLoadLatency() throws IOException { try (DirectoryReader reader = DirectoryReader.open(directory)) { SourceProvider provider = SourceProvider.fromSyntheticSource( mapper.mapping(), + null, createTestMapperMetrics().sourceFieldMetrics() ); Source synthetic = provider.getSource(getOnlyLeafReader(reader).getContext(), 0); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/SourceLoaderTests.java b/server/src/test/java/org/elasticsearch/index/mapper/SourceLoaderTests.java index c2e49759cdfde..8b4176d6b9631 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/SourceLoaderTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/SourceLoaderTests.java @@ -21,7 +21,7 @@ public void testNonSynthetic() throws IOException { b.startObject("o").field("type", "object").endObject(); b.startObject("kwd").field("type", "keyword").endObject(); })); - assertFalse(mapper.mappers().newSourceLoader(SourceFieldMetrics.NOOP).reordersFieldValues()); + assertFalse(mapper.mappers().newSourceLoader(null, SourceFieldMetrics.NOOP).reordersFieldValues()); } public void testEmptyObject() throws IOException { @@ -29,7 +29,7 @@ public void testEmptyObject() throws IOException { b.startObject("o").field("type", "object").endObject(); b.startObject("kwd").field("type", "keyword").endObject(); })).documentMapper(); - assertTrue(mapper.mappers().newSourceLoader(SourceFieldMetrics.NOOP).reordersFieldValues()); + assertTrue(mapper.mappers().newSourceLoader(null, SourceFieldMetrics.NOOP).reordersFieldValues()); assertThat(syntheticSource(mapper, b -> b.field("kwd", "foo")), equalTo(""" {"kwd":"foo"}""")); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java index 7f9474f5bab83..32cbcfc2441a1 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/TextFieldMapperTests.java @@ -1354,7 +1354,7 @@ private void testBlockLoaderFromParent(boolean columnReader, boolean syntheticSo XContentBuilder mapping = mapping(buildFields); MapperService mapper = syntheticSource ? createSytheticSourceMapperService(mapping) : createMapperService(mapping); BlockReaderSupport blockReaderSupport = getSupportedReaders(mapper, "field.sub"); - var sourceLoader = mapper.mappingLookup().newSourceLoader(SourceFieldMetrics.NOOP); + var sourceLoader = mapper.mappingLookup().newSourceLoader(null, SourceFieldMetrics.NOOP); testBlockLoader(columnReader, example, blockReaderSupport, sourceLoader); } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/XContentDataHelperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/XContentDataHelperTests.java index f4e114da1fa51..ecf59b611080b 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/XContentDataHelperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/XContentDataHelperTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xcontent.json.JsonXContent; @@ -29,6 +30,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import static org.hamcrest.Matchers.equalTo; @@ -57,6 +59,7 @@ private String encodeAndDecodeCustom(XContentType type, Object value) throws IOE parser.nextToken(); var encoded = XContentDataHelper.encodeToken(parser); + assertThat(XContentDataHelper.isEncodedObject(encoded), equalTo(value instanceof Map)); var decoded = XContentFactory.jsonBuilder(); XContentDataHelper.decodeAndWrite(decoded, encoded); @@ -124,6 +127,7 @@ public void testEmbeddedObject() throws IOException { assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); parser.nextToken(); var encoded = XContentDataHelper.encodeToken(parser); + assertFalse(XContentDataHelper.isEncodedObject(encoded)); var decoded = XContentFactory.jsonBuilder(); XContentDataHelper.decodeAndWrite(decoded, encoded); @@ -132,6 +136,7 @@ public void testEmbeddedObject() throws IOException { } var encoded = XContentDataHelper.encodeXContentBuilder(builder); + assertTrue(XContentDataHelper.isEncodedObject(encoded)); var decoded = XContentFactory.jsonBuilder(); XContentDataHelper.decodeAndWrite(decoded, encoded); @@ -147,7 +152,9 @@ public void testObject() throws IOException { XContentBuilder builder = XContentFactory.jsonBuilder(); builder.humanReadable(true); - XContentDataHelper.decodeAndWrite(builder, XContentDataHelper.encodeToken(p)); + var encoded = XContentDataHelper.encodeToken(p); + assertTrue(XContentDataHelper.isEncodedObject(encoded)); + XContentDataHelper.decodeAndWrite(builder, encoded); assertEquals(object, Strings.toString(builder)); XContentBuilder builder2 = XContentFactory.jsonBuilder(); @@ -156,6 +163,62 @@ public void testObject() throws IOException { assertEquals(object, Strings.toString(builder2)); } + public void testObjectWithFilter() throws IOException { + String object = "{\"name\":\"foo\",\"path\":{\"filter\":{\"keep\":[0],\"field\":\"value\"}}}"; + String filterObject = "{\"name\":\"foo\",\"path\":{\"filter\":{\"keep\":[0]}}}"; + + XContentParser p = createParser(JsonXContent.jsonXContent, object); + assertThat(p.nextToken(), equalTo(XContentParser.Token.START_OBJECT)); + XContentParserConfiguration parserConfig = XContentParserConfiguration.EMPTY.withFiltering( + null, + null, + Set.of("path.filter.field"), + true + ); + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.humanReadable(true); + XContentDataHelper.decodeAndWriteXContent(parserConfig, builder, XContentType.JSON, XContentDataHelper.encodeToken(p)); + assertEquals(filterObject, Strings.toString(builder)); + + XContentBuilder builder2 = XContentFactory.jsonBuilder(); + builder2.humanReadable(true); + XContentDataHelper.decodeAndWriteXContent( + parserConfig, + builder2, + XContentType.JSON, + XContentDataHelper.encodeXContentBuilder(builder) + ); + assertEquals(filterObject, Strings.toString(builder2)); + } + + public void testObjectWithFilterRootPath() throws IOException { + String object = "{\"name\":\"foo\",\"path\":{\"filter\":{\"keep\":[0],\"field\":\"value\"}}}"; + String filterObject = "{\"path\":{\"filter\":{\"keep\":[0]}}}"; + + XContentParser p = createParser(JsonXContent.jsonXContent, object); + assertThat(p.nextToken(), equalTo(XContentParser.Token.START_OBJECT)); + XContentParserConfiguration parserConfig = XContentParserConfiguration.EMPTY.withFiltering( + "root.obj.sub_obj", + Set.of("root.obj.sub_obj.path"), + Set.of("root.obj.sub_obj.path.filter.field"), + true + ); + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.humanReadable(true); + XContentDataHelper.decodeAndWriteXContent(parserConfig, builder, XContentType.JSON, XContentDataHelper.encodeToken(p)); + assertEquals(filterObject, Strings.toString(builder)); + + XContentBuilder builder2 = XContentFactory.jsonBuilder(); + builder2.humanReadable(true); + XContentDataHelper.decodeAndWriteXContent( + parserConfig, + builder2, + XContentType.JSON, + XContentDataHelper.encodeXContentBuilder(builder) + ); + assertEquals(filterObject, Strings.toString(builder2)); + } + public void testArrayInt() throws IOException { String values = "[" + String.join(",", List.of(Integer.toString(randomInt()), Integer.toString(randomInt()), Integer.toString(randomInt()))) @@ -252,7 +315,7 @@ private Map executeWriteMergedOnTwoEncodedValues(Object first, O var destination = XContentFactory.contentBuilder(xContentType); destination.startObject(); - XContentDataHelper.writeMerged(destination, "foo", List.of(firstEncoded, secondEncoded)); + XContentDataHelper.writeMerged(XContentParserConfiguration.EMPTY, destination, "foo", List.of(firstEncoded, secondEncoded)); destination.endObject(); return XContentHelper.convertToMap(BytesReference.bytes(destination), false, xContentType).v2(); diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java index 66d87f3532cbd..b9356bc4b5633 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperServiceTestCase.java @@ -70,6 +70,7 @@ import org.elasticsearch.search.aggregations.support.ValuesSourceRegistry; import org.elasticsearch.search.internal.SubSearchContext; import org.elasticsearch.search.lookup.SearchLookup; +import org.elasticsearch.search.lookup.SourceFilter; import org.elasticsearch.search.lookup.SourceProvider; import org.elasticsearch.search.sort.BucketedSort; import org.elasticsearch.search.sort.BucketedSort.ExtraData; @@ -798,6 +799,14 @@ protected RandomIndexWriter indexWriterForSyntheticSource(Directory directory) t } protected final String syntheticSource(DocumentMapper mapper, CheckedConsumer build) throws IOException { + return syntheticSource(mapper, null, build); + } + + protected final String syntheticSource( + DocumentMapper mapper, + @Nullable SourceFilter sourceFilter, + CheckedConsumer build + ) throws IOException { try (Directory directory = newDirectory()) { RandomIndexWriter iw = indexWriterForSyntheticSource(directory); ParsedDocument doc = mapper.parse(source(build)); @@ -806,9 +815,10 @@ protected final String syntheticSource(DocumentMapper mapper, CheckedConsumer mapper.mapping().syntheticFieldLoader(filter), + SourceFieldMetrics.NOOP + ); var sourceLeafLoader = sourceLoader.leaf(getOnlyLeafReader(reader), docIds); var storedFieldLoader = StoredFieldLoader.create(false, sourceLoader.requiredStoredFields()) .getLoader(leafReader.getContext(), docIds); diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java index 7dcbbce9fa8e0..2da2c5a08c177 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/MapperTestCase.java @@ -64,6 +64,7 @@ import org.elasticsearch.search.lookup.LeafStoredFieldsLookup; import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.search.lookup.Source; +import org.elasticsearch.search.lookup.SourceFilter; import org.elasticsearch.search.lookup.SourceProvider; import org.elasticsearch.test.ListMatcher; import org.elasticsearch.xcontent.ToXContent; @@ -1169,6 +1170,11 @@ private void assertSyntheticSource(SyntheticSourceExample example) throws IOExce b.endObject(); })).documentMapper(); assertThat(syntheticSource(mapper, example::buildInput), equalTo(example.expected())); + assertThat( + syntheticSource(mapper, new SourceFilter(new String[] { "field" }, null), example::buildInput), + equalTo(example.expected()) + ); + assertThat(syntheticSource(mapper, new SourceFilter(null, new String[] { "field" }), example::buildInput), equalTo("{}")); } private void assertSyntheticSourceWithTranslogSnapshot(SyntheticSourceSupport support, boolean doIndexSort) throws IOException { @@ -1292,7 +1298,7 @@ public final void testSyntheticSourceMany() throws IOException { } try (DirectoryReader reader = DirectoryReader.open(directory)) { int i = 0; - SourceLoader loader = mapper.sourceMapper().newSourceLoader(mapper.mapping(), SourceFieldMetrics.NOOP); + SourceLoader loader = mapper.mappers().newSourceLoader(null, SourceFieldMetrics.NOOP); StoredFieldLoader storedFieldLoader = loader.requiredStoredFields().isEmpty() ? StoredFieldLoader.empty() : StoredFieldLoader.create(false, loader.requiredStoredFields()); @@ -1335,6 +1341,18 @@ public final void testSyntheticSourceInObject() throws IOException { syntheticSourceExample.buildInput(b); b.endObject(); }), equalTo("{\"obj\":" + syntheticSourceExample.expected() + "}")); + + assertThat(syntheticSource(mapper, new SourceFilter(new String[] { "obj.field" }, null), b -> { + b.startObject("obj"); + syntheticSourceExample.buildInput(b); + b.endObject(); + }), equalTo("{\"obj\":" + syntheticSourceExample.expected() + "}")); + + assertThat(syntheticSource(mapper, new SourceFilter(null, new String[] { "obj.field" }), b -> { + b.startObject("obj"); + syntheticSourceExample.buildInput(b); + b.endObject(); + }), equalTo("{}")); } public final void testSyntheticEmptyList() throws IOException { @@ -1465,7 +1483,7 @@ private void testBlockLoader(boolean syntheticSource, boolean columnReader) thro blockReaderSupport.syntheticSource ); } - var sourceLoader = mapper.mappingLookup().newSourceLoader(SourceFieldMetrics.NOOP); + var sourceLoader = mapper.mappingLookup().newSourceLoader(null, SourceFieldMetrics.NOOP); testBlockLoader(columnReader, example, blockReaderSupport, sourceLoader); } @@ -1592,7 +1610,8 @@ private void assertNoDocValueLoader(CheckedConsumer { + b.startObject("obj"); + syntheticSourceExample.buildInput(b); + b.endObject(); + }), equalTo("{\"obj\":" + syntheticSourceExample.expected() + "}")); + + assertThat(syntheticSource(mapper, new SourceFilter(null, new String[] { "obj.field" }), b -> { + b.startObject("obj"); + syntheticSourceExample.buildInput(b); + b.endObject(); + }), equalTo("{\"obj\":{}}")); + + assertThat(syntheticSource(mapper, new SourceFilter(null, new String[] { "obj" }), b -> { + b.startObject("obj"); + syntheticSourceExample.buildInput(b); + b.endObject(); + }), equalTo("{}")); } protected SyntheticSourceSupport syntheticSourceSupportForKeepTests(boolean ignoreMalformed) { diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportPutRollupJobAction.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportPutRollupJobAction.java index 6618f3199debf..3035bb98a3a93 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportPutRollupJobAction.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/action/TransportPutRollupJobAction.java @@ -68,6 +68,7 @@ public class TransportPutRollupJobAction extends AcknowledgedTransportMasterNode private static final Logger LOGGER = LogManager.getLogger(TransportPutRollupJobAction.class); private static final XContentParserConfiguration PARSER_CONFIGURATION = XContentParserConfiguration.EMPTY.withFiltering( + null, Set.of("_doc._meta._rollup"), null, false From bcba5bf591403d60ee68be4ec3edae019f7e68c4 Mon Sep 17 00:00:00 2001 From: Mike Pellegrini Date: Wed, 11 Dec 2024 08:20:26 -0500 Subject: [PATCH 03/77] Restore original "is within leaf" value in SparseVectorFieldMapper (#118380) --- docs/changelog/118380.yaml | 5 +++++ .../index/mapper/vectors/SparseVectorFieldMapper.java | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/118380.yaml diff --git a/docs/changelog/118380.yaml b/docs/changelog/118380.yaml new file mode 100644 index 0000000000000..8b26c871fb172 --- /dev/null +++ b/docs/changelog/118380.yaml @@ -0,0 +1,5 @@ +pr: 118380 +summary: Restore original "is within leaf" value in `SparseVectorFieldMapper` +area: Mapping +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/SparseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/SparseVectorFieldMapper.java index 552e66336005d..b4de73e3b62ce 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/SparseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/SparseVectorFieldMapper.java @@ -200,6 +200,7 @@ public void parse(DocumentParserContext context) throws IOException { ); } + final boolean isWithinLeaf = context.path().isWithinLeafObject(); String feature = null; try { // make sure that we don't expand dots in field names while parsing @@ -234,7 +235,7 @@ public void parse(DocumentParserContext context) throws IOException { context.addToFieldNames(fieldType().name()); } } finally { - context.path().setWithinLeafObject(false); + context.path().setWithinLeafObject(isWithinLeaf); } } From 052f46ae49d532d6ca14c9f823944d96ce4745e5 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Wed, 11 Dec 2024 13:21:26 +0000 Subject: [PATCH 04/77] =?UTF-8?q?[ML]=20Reinstate=20default=20endpoint=20f?= =?UTF-8?q?or=20Elastic=20Rerank=20behind=20a=20feature=20flag(#117939)"?= =?UTF-8?q?=E2=80=A6=20(#118253)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "Revert "Adding default endpoint for Elastic Rerank (#117939)" (#118221)" --- docs/changelog/117939.yaml | 5 ++ .../xpack/inference/DefaultEndPointsIT.java | 40 ++++++++++++++ .../inference/InferenceBaseRestTest.java | 47 ++++++++++++---- .../xpack/inference/InferenceCrudIT.java | 4 +- .../InferenceNamedWriteablesProvider.java | 6 +- .../elasticsearch/CustomElandRerankModel.java | 4 +- .../elasticsearch/ElasticRerankerModel.java | 5 +- .../ElasticsearchInternalService.java | 55 +++++++++++++------ ...kSettings.java => RerankTaskSettings.java} | 25 ++++----- .../ElasticsearchInternalServiceTests.java | 42 +++++++------- ...ests.java => RerankTaskSettingsTests.java} | 48 ++++++++-------- 11 files changed, 185 insertions(+), 96 deletions(-) create mode 100644 docs/changelog/117939.yaml rename x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/{CustomElandRerankTaskSettings.java => RerankTaskSettings.java} (79%) rename x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/{CustomElandRerankTaskSettingsTests.java => RerankTaskSettingsTests.java} (53%) diff --git a/docs/changelog/117939.yaml b/docs/changelog/117939.yaml new file mode 100644 index 0000000000000..d41111f099f97 --- /dev/null +++ b/docs/changelog/117939.yaml @@ -0,0 +1,5 @@ +pr: 117939 +summary: Adding default endpoint for Elastic Rerank +area: Machine Learning +type: enhancement +issues: [] diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/DefaultEndPointsIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/DefaultEndPointsIT.java index ba3e48e11928d..068b3e1f4ce04 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/DefaultEndPointsIT.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/DefaultEndPointsIT.java @@ -57,6 +57,9 @@ public void testGet() throws IOException { var e5Model = getModel(ElasticsearchInternalService.DEFAULT_E5_ID); assertDefaultE5Config(e5Model); + + var rerankModel = getModel(ElasticsearchInternalService.DEFAULT_RERANK_ID); + assertDefaultRerankConfig(rerankModel); } @SuppressWarnings("unchecked") @@ -125,6 +128,42 @@ private static void assertDefaultE5Config(Map modelConfig) { assertDefaultChunkingSettings(modelConfig); } + @SuppressWarnings("unchecked") + public void testInferDeploysDefaultRerank() throws IOException { + var model = getModel(ElasticsearchInternalService.DEFAULT_RERANK_ID); + assertDefaultRerankConfig(model); + + var inputs = List.of("Hello World", "Goodnight moon"); + var query = "but why"; + var queryParams = Map.of("timeout", "120s"); + var results = infer(ElasticsearchInternalService.DEFAULT_RERANK_ID, TaskType.RERANK, inputs, query, queryParams); + var embeddings = (List>) results.get("rerank"); + assertThat(results.toString(), embeddings, hasSize(2)); + } + + @SuppressWarnings("unchecked") + private static void assertDefaultRerankConfig(Map modelConfig) { + assertEquals(modelConfig.toString(), ElasticsearchInternalService.DEFAULT_RERANK_ID, modelConfig.get("inference_id")); + assertEquals(modelConfig.toString(), ElasticsearchInternalService.NAME, modelConfig.get("service")); + assertEquals(modelConfig.toString(), TaskType.RERANK.toString(), modelConfig.get("task_type")); + + var serviceSettings = (Map) modelConfig.get("service_settings"); + assertThat(modelConfig.toString(), serviceSettings.get("model_id"), is(".rerank-v1")); + assertEquals(modelConfig.toString(), 1, serviceSettings.get("num_threads")); + + var adaptiveAllocations = (Map) serviceSettings.get("adaptive_allocations"); + assertThat( + modelConfig.toString(), + adaptiveAllocations, + Matchers.is(Map.of("enabled", true, "min_number_of_allocations", 0, "max_number_of_allocations", 32)) + ); + + var chunkingSettings = (Map) modelConfig.get("chunking_settings"); + assertNull(chunkingSettings); + var taskSettings = (Map) modelConfig.get("task_settings"); + assertThat(modelConfig.toString(), taskSettings, Matchers.is(Map.of("return_documents", true))); + } + @SuppressWarnings("unchecked") private static void assertDefaultChunkingSettings(Map modelConfig) { var chunkingSettings = (Map) modelConfig.get("chunking_settings"); @@ -159,6 +198,7 @@ public void onFailure(Exception exception) { var request = createInferenceRequest( Strings.format("_inference/%s", ElasticsearchInternalService.DEFAULT_ELSER_ID), inputs, + null, queryParams ); client().performRequestAsync(request, listener); diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceBaseRestTest.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceBaseRestTest.java index 5b7394e89bc43..5e6c4d53f4c58 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceBaseRestTest.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceBaseRestTest.java @@ -338,7 +338,7 @@ private List getInternalAsList(String endpoint) throws IOException { protected Map infer(String modelId, List input) throws IOException { var endpoint = Strings.format("_inference/%s", modelId); - return inferInternal(endpoint, input, Map.of()); + return inferInternal(endpoint, input, null, Map.of()); } protected Deque streamInferOnMockService(String modelId, TaskType taskType, List input) throws Exception { @@ -354,7 +354,7 @@ protected Deque unifiedCompletionInferOnMockService(String mode private Deque callAsync(String endpoint, List input) throws Exception { var request = new Request("POST", endpoint); - request.setJsonEntity(jsonBody(input)); + request.setJsonEntity(jsonBody(input, null)); return execAsyncCall(request); } @@ -396,33 +396,60 @@ private String createUnifiedJsonBody(List input, String role) throws IOE protected Map infer(String modelId, TaskType taskType, List input) throws IOException { var endpoint = Strings.format("_inference/%s/%s", taskType, modelId); - return inferInternal(endpoint, input, Map.of()); + return inferInternal(endpoint, input, null, Map.of()); } protected Map infer(String modelId, TaskType taskType, List input, Map queryParameters) throws IOException { var endpoint = Strings.format("_inference/%s/%s?error_trace", taskType, modelId); - return inferInternal(endpoint, input, queryParameters); + return inferInternal(endpoint, input, null, queryParameters); } - protected Request createInferenceRequest(String endpoint, List input, Map queryParameters) { + protected Map infer( + String modelId, + TaskType taskType, + List input, + String query, + Map queryParameters + ) throws IOException { + var endpoint = Strings.format("_inference/%s/%s?error_trace", taskType, modelId); + return inferInternal(endpoint, input, query, queryParameters); + } + + protected Request createInferenceRequest( + String endpoint, + List input, + @Nullable String query, + Map queryParameters + ) { var request = new Request("POST", endpoint); - request.setJsonEntity(jsonBody(input)); + request.setJsonEntity(jsonBody(input, query)); if (queryParameters.isEmpty() == false) { request.addParameters(queryParameters); } return request; } - private Map inferInternal(String endpoint, List input, Map queryParameters) throws IOException { - var request = createInferenceRequest(endpoint, input, queryParameters); + private Map inferInternal( + String endpoint, + List input, + @Nullable String query, + Map queryParameters + ) throws IOException { + var request = createInferenceRequest(endpoint, input, query, queryParameters); var response = client().performRequest(request); assertOkOrCreated(response); return entityAsMap(response); } - private String jsonBody(List input) { - var bodyBuilder = new StringBuilder("{\"input\": ["); + private String jsonBody(List input, @Nullable String query) { + final StringBuilder bodyBuilder = new StringBuilder("{"); + + if (query != null) { + bodyBuilder.append("\"query\":\"").append(query).append("\","); + } + + bodyBuilder.append("\"input\": ["); for (var in : input) { bodyBuilder.append('"').append(in).append('"').append(','); } diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java index da1d10db4da8b..90d4f3a8eb33b 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java @@ -49,7 +49,7 @@ public void testCRUD() throws IOException { } var getAllModels = getAllModels(); - int numModels = 11; + int numModels = 12; assertThat(getAllModels, hasSize(numModels)); var getSparseModels = getModels("_all", TaskType.SPARSE_EMBEDDING); @@ -537,7 +537,7 @@ private static String expectedResult(String input) { } public void testGetZeroModels() throws IOException { - var models = getModels("_all", TaskType.RERANK); + var models = getModels("_all", TaskType.COMPLETION); assertThat(models, empty()); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceNamedWriteablesProvider.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceNamedWriteablesProvider.java index b83c098ca808c..a4187f4c4fa90 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceNamedWriteablesProvider.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceNamedWriteablesProvider.java @@ -63,12 +63,12 @@ import org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceServiceSparseEmbeddingsServiceSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.CustomElandInternalServiceSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.CustomElandInternalTextEmbeddingServiceSettings; -import org.elasticsearch.xpack.inference.services.elasticsearch.CustomElandRerankTaskSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.ElasticRerankerServiceSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.ElasticsearchInternalServiceSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.ElserInternalServiceSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.ElserMlNodeTaskSettings; import org.elasticsearch.xpack.inference.services.elasticsearch.MultilingualE5SmallInternalServiceSettings; +import org.elasticsearch.xpack.inference.services.elasticsearch.RerankTaskSettings; import org.elasticsearch.xpack.inference.services.googleaistudio.completion.GoogleAiStudioCompletionServiceSettings; import org.elasticsearch.xpack.inference.services.googleaistudio.embeddings.GoogleAiStudioEmbeddingsServiceSettings; import org.elasticsearch.xpack.inference.services.googlevertexai.GoogleVertexAiSecretSettings; @@ -518,9 +518,7 @@ private static void addCustomElandWriteables(final List namedWriteables) { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankModel.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankModel.java index f620b15680c8d..6388bb33bb78d 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankModel.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankModel.java @@ -17,7 +17,7 @@ import java.util.HashMap; import java.util.Map; -import static org.elasticsearch.xpack.inference.services.elasticsearch.CustomElandRerankTaskSettings.RETURN_DOCUMENTS; +import static org.elasticsearch.xpack.inference.services.elasticsearch.RerankTaskSettings.RETURN_DOCUMENTS; public class CustomElandRerankModel extends CustomElandModel { @@ -26,7 +26,7 @@ public CustomElandRerankModel( TaskType taskType, String service, CustomElandInternalServiceSettings serviceSettings, - CustomElandRerankTaskSettings taskSettings + RerankTaskSettings taskSettings ) { super(inferenceEntityId, taskType, service, serviceSettings, taskSettings); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticRerankerModel.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticRerankerModel.java index 115cc9f05599a..276bce6dbe8f8 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticRerankerModel.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticRerankerModel.java @@ -9,7 +9,6 @@ import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.inference.ChunkingSettings; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.TaskType; import org.elasticsearch.xpack.core.ml.action.CreateTrainedModelAssignmentAction; @@ -22,9 +21,9 @@ public ElasticRerankerModel( TaskType taskType, String service, ElasticRerankerServiceSettings serviceSettings, - ChunkingSettings chunkingSettings + RerankTaskSettings taskSettings ) { - super(inferenceEntityId, taskType, service, serviceSettings, chunkingSettings); + super(inferenceEntityId, taskType, service, serviceSettings, taskSettings); } @Override diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalService.java index 8cb91782e238e..5f613d6be5869 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalService.java @@ -103,6 +103,7 @@ public class ElasticsearchInternalService extends BaseElasticsearchInternalServi public static final int EMBEDDING_MAX_BATCH_SIZE = 10; public static final String DEFAULT_ELSER_ID = ".elser-2-elasticsearch"; public static final String DEFAULT_E5_ID = ".multilingual-e5-small-elasticsearch"; + public static final String DEFAULT_RERANK_ID = ".rerank-v1-elasticsearch"; private static final EnumSet supportedTaskTypes = EnumSet.of( TaskType.RERANK, @@ -227,7 +228,7 @@ public void parseRequestConfig( ) ); } else if (RERANKER_ID.equals(modelId)) { - rerankerCase(inferenceEntityId, taskType, config, serviceSettingsMap, chunkingSettings, modelListener); + rerankerCase(inferenceEntityId, taskType, config, serviceSettingsMap, taskSettingsMap, modelListener); } else { customElandCase(inferenceEntityId, taskType, serviceSettingsMap, taskSettingsMap, chunkingSettings, modelListener); } @@ -310,7 +311,7 @@ private static CustomElandModel createCustomElandModel( taskType, NAME, elandServiceSettings(serviceSettings, context), - CustomElandRerankTaskSettings.fromMap(taskSettings) + RerankTaskSettings.fromMap(taskSettings) ); default -> throw new ElasticsearchStatusException(TaskType.unsupportedTaskTypeErrorMsg(taskType, NAME), RestStatus.BAD_REQUEST); }; @@ -333,7 +334,7 @@ private void rerankerCase( TaskType taskType, Map config, Map serviceSettingsMap, - ChunkingSettings chunkingSettings, + Map taskSettingsMap, ActionListener modelListener ) { @@ -348,7 +349,7 @@ private void rerankerCase( taskType, NAME, new ElasticRerankerServiceSettings(esServiceSettingsBuilder.build()), - chunkingSettings + RerankTaskSettings.fromMap(taskSettingsMap) ) ); } @@ -514,6 +515,14 @@ public Model parsePersistedConfig(String inferenceEntityId, TaskType taskType, M ElserMlNodeTaskSettings.DEFAULT, chunkingSettings ); + } else if (modelId.equals(RERANKER_ID)) { + return new ElasticRerankerModel( + inferenceEntityId, + taskType, + NAME, + new ElasticRerankerServiceSettings(ElasticsearchInternalServiceSettings.fromPersistedMap(serviceSettingsMap)), + RerankTaskSettings.fromMap(taskSettingsMap) + ); } else { return createCustomElandModel( inferenceEntityId, @@ -665,21 +674,23 @@ public void inferRerank( ) { var request = buildInferenceRequest(model.mlNodeDeploymentId(), new TextSimilarityConfigUpdate(query), inputs, inputType, timeout); - var modelSettings = (CustomElandRerankTaskSettings) model.getTaskSettings(); - var requestSettings = CustomElandRerankTaskSettings.fromMap(requestTaskSettings); - Boolean returnDocs = CustomElandRerankTaskSettings.of(modelSettings, requestSettings).returnDocuments(); + var returnDocs = Boolean.TRUE; + if (model.getTaskSettings() instanceof RerankTaskSettings modelSettings) { + var requestSettings = RerankTaskSettings.fromMap(requestTaskSettings); + returnDocs = RerankTaskSettings.of(modelSettings, requestSettings).returnDocuments(); + } Function inputSupplier = returnDocs == Boolean.TRUE ? inputs::get : i -> null; - client.execute( - InferModelAction.INSTANCE, - request, - listener.delegateFailureAndWrap( - (l, inferenceResult) -> l.onResponse( - textSimilarityResultsToRankedDocs(inferenceResult.getInferenceResults(), inputSupplier) - ) - ) + ActionListener mlResultsListener = listener.delegateFailureAndWrap( + (l, inferenceResult) -> l.onResponse(textSimilarityResultsToRankedDocs(inferenceResult.getInferenceResults(), inputSupplier)) + ); + + var maybeDeployListener = mlResultsListener.delegateResponse( + (l, exception) -> maybeStartDeployment(model, exception, request, mlResultsListener) ); + + client.execute(InferModelAction.INSTANCE, request, maybeDeployListener); } public void chunkedInfer( @@ -823,7 +834,8 @@ private RankedDocsResults textSimilarityResultsToRankedDocs( public List defaultConfigIds() { return List.of( new DefaultConfigId(DEFAULT_ELSER_ID, TaskType.SPARSE_EMBEDDING, this), - new DefaultConfigId(DEFAULT_E5_ID, TaskType.TEXT_EMBEDDING, this) + new DefaultConfigId(DEFAULT_E5_ID, TaskType.TEXT_EMBEDDING, this), + new DefaultConfigId(DEFAULT_RERANK_ID, TaskType.RERANK, this) ); } @@ -916,12 +928,19 @@ private List defaultConfigs(boolean useLinuxOptimizedModel) { ), ChunkingSettingsBuilder.DEFAULT_SETTINGS ); - return List.of(defaultElser, defaultE5); + var defaultRerank = new ElasticRerankerModel( + DEFAULT_RERANK_ID, + TaskType.RERANK, + NAME, + new ElasticRerankerServiceSettings(null, 1, RERANKER_ID, new AdaptiveAllocationsSettings(Boolean.TRUE, 0, 32)), + RerankTaskSettings.DEFAULT_SETTINGS + ); + return List.of(defaultElser, defaultE5, defaultRerank); } @Override boolean isDefaultId(String inferenceId) { - return DEFAULT_ELSER_ID.equals(inferenceId) || DEFAULT_E5_ID.equals(inferenceId); + return DEFAULT_ELSER_ID.equals(inferenceId) || DEFAULT_E5_ID.equals(inferenceId) || DEFAULT_RERANK_ID.equals(inferenceId); } static EmbeddingRequestChunker.EmbeddingType embeddingTypeFromTaskTypeAndSettings( diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankTaskSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/RerankTaskSettings.java similarity index 79% rename from x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankTaskSettings.java rename to x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/RerankTaskSettings.java index a0be1661b860d..3c25f7a6a9016 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankTaskSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/RerankTaskSettings.java @@ -26,14 +26,14 @@ /** * Defines the task settings for internal rerank service. */ -public class CustomElandRerankTaskSettings implements TaskSettings { +public class RerankTaskSettings implements TaskSettings { public static final String NAME = "custom_eland_rerank_task_settings"; public static final String RETURN_DOCUMENTS = "return_documents"; - static final CustomElandRerankTaskSettings DEFAULT_SETTINGS = new CustomElandRerankTaskSettings(Boolean.TRUE); + static final RerankTaskSettings DEFAULT_SETTINGS = new RerankTaskSettings(Boolean.TRUE); - public static CustomElandRerankTaskSettings defaultsFromMap(Map map) { + public static RerankTaskSettings defaultsFromMap(Map map) { ValidationException validationException = new ValidationException(); if (map == null || map.isEmpty()) { @@ -49,7 +49,7 @@ public static CustomElandRerankTaskSettings defaultsFromMap(Map returnDocuments = true; } - return new CustomElandRerankTaskSettings(returnDocuments); + return new RerankTaskSettings(returnDocuments); } /** @@ -57,13 +57,13 @@ public static CustomElandRerankTaskSettings defaultsFromMap(Map * @param map source map * @return Task settings */ - public static CustomElandRerankTaskSettings fromMap(Map map) { + public static RerankTaskSettings fromMap(Map map) { if (map == null || map.isEmpty()) { return DEFAULT_SETTINGS; } Boolean returnDocuments = extractOptionalBoolean(map, RETURN_DOCUMENTS, new ValidationException()); - return new CustomElandRerankTaskSettings(returnDocuments); + return new RerankTaskSettings(returnDocuments); } /** @@ -74,20 +74,17 @@ public static CustomElandRerankTaskSettings fromMap(Map map) { * @param requestTaskSettings the settings passed in within the task_settings field of the request * @return Either {@code originalSettings} or {@code requestTaskSettings} */ - public static CustomElandRerankTaskSettings of( - CustomElandRerankTaskSettings originalSettings, - CustomElandRerankTaskSettings requestTaskSettings - ) { + public static RerankTaskSettings of(RerankTaskSettings originalSettings, RerankTaskSettings requestTaskSettings) { return requestTaskSettings.returnDocuments() != null ? requestTaskSettings : originalSettings; } private final Boolean returnDocuments; - public CustomElandRerankTaskSettings(StreamInput in) throws IOException { + public RerankTaskSettings(StreamInput in) throws IOException { this(in.readOptionalBoolean()); } - public CustomElandRerankTaskSettings(@Nullable Boolean doReturnDocuments) { + public RerankTaskSettings(@Nullable Boolean doReturnDocuments) { if (doReturnDocuments == null) { this.returnDocuments = true; } else { @@ -133,7 +130,7 @@ public Boolean returnDocuments() { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - CustomElandRerankTaskSettings that = (CustomElandRerankTaskSettings) o; + RerankTaskSettings that = (RerankTaskSettings) o; return Objects.equals(returnDocuments, that.returnDocuments); } @@ -144,7 +141,7 @@ public int hashCode() { @Override public TaskSettings updatedTaskSettings(Map newSettings) { - CustomElandRerankTaskSettings updatedSettings = CustomElandRerankTaskSettings.fromMap(new HashMap<>(newSettings)); + RerankTaskSettings updatedSettings = RerankTaskSettings.fromMap(new HashMap<>(newSettings)); return of(this, updatedSettings); } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalServiceTests.java index 306509ea60cfc..17e6583f11c8f 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalServiceTests.java @@ -534,16 +534,13 @@ public void testParseRequestConfig_Rerank() { ) ); var returnDocs = randomBoolean(); - settings.put( - ModelConfigurations.TASK_SETTINGS, - new HashMap<>(Map.of(CustomElandRerankTaskSettings.RETURN_DOCUMENTS, returnDocs)) - ); + settings.put(ModelConfigurations.TASK_SETTINGS, new HashMap<>(Map.of(RerankTaskSettings.RETURN_DOCUMENTS, returnDocs))); ActionListener modelListener = ActionListener.wrap(model -> { assertThat(model, instanceOf(CustomElandRerankModel.class)); - assertThat(model.getTaskSettings(), instanceOf(CustomElandRerankTaskSettings.class)); + assertThat(model.getTaskSettings(), instanceOf(RerankTaskSettings.class)); assertThat(model.getServiceSettings(), instanceOf(CustomElandInternalServiceSettings.class)); - assertEquals(returnDocs, ((CustomElandRerankTaskSettings) model.getTaskSettings()).returnDocuments()); + assertEquals(returnDocs, ((RerankTaskSettings) model.getTaskSettings()).returnDocuments()); }, e -> { fail("Model parsing failed " + e.getMessage()); }); service.parseRequestConfig(randomInferenceEntityId, TaskType.RERANK, settings, modelListener); @@ -583,9 +580,9 @@ public void testParseRequestConfig_Rerank_DefaultTaskSettings() { ActionListener modelListener = ActionListener.wrap(model -> { assertThat(model, instanceOf(CustomElandRerankModel.class)); - assertThat(model.getTaskSettings(), instanceOf(CustomElandRerankTaskSettings.class)); + assertThat(model.getTaskSettings(), instanceOf(RerankTaskSettings.class)); assertThat(model.getServiceSettings(), instanceOf(CustomElandInternalServiceSettings.class)); - assertEquals(Boolean.TRUE, ((CustomElandRerankTaskSettings) model.getTaskSettings()).returnDocuments()); + assertEquals(Boolean.TRUE, ((RerankTaskSettings) model.getTaskSettings()).returnDocuments()); }, e -> { fail("Model parsing failed " + e.getMessage()); }); service.parseRequestConfig(randomInferenceEntityId, TaskType.RERANK, settings, modelListener); @@ -1249,14 +1246,11 @@ public void testParsePersistedConfig_Rerank() { ); settings.put(ElasticsearchInternalServiceSettings.MODEL_ID, "foo"); var returnDocs = randomBoolean(); - settings.put( - ModelConfigurations.TASK_SETTINGS, - new HashMap<>(Map.of(CustomElandRerankTaskSettings.RETURN_DOCUMENTS, returnDocs)) - ); + settings.put(ModelConfigurations.TASK_SETTINGS, new HashMap<>(Map.of(RerankTaskSettings.RETURN_DOCUMENTS, returnDocs))); var model = service.parsePersistedConfig(randomInferenceEntityId, TaskType.RERANK, settings); - assertThat(model.getTaskSettings(), instanceOf(CustomElandRerankTaskSettings.class)); - assertEquals(returnDocs, ((CustomElandRerankTaskSettings) model.getTaskSettings()).returnDocuments()); + assertThat(model.getTaskSettings(), instanceOf(RerankTaskSettings.class)); + assertEquals(returnDocs, ((RerankTaskSettings) model.getTaskSettings()).returnDocuments()); } // without task settings @@ -1279,8 +1273,8 @@ public void testParsePersistedConfig_Rerank() { settings.put(ElasticsearchInternalServiceSettings.MODEL_ID, "foo"); var model = service.parsePersistedConfig(randomInferenceEntityId, TaskType.RERANK, settings); - assertThat(model.getTaskSettings(), instanceOf(CustomElandRerankTaskSettings.class)); - assertTrue(((CustomElandRerankTaskSettings) model.getTaskSettings()).returnDocuments()); + assertThat(model.getTaskSettings(), instanceOf(RerankTaskSettings.class)); + assertTrue(((RerankTaskSettings) model.getTaskSettings()).returnDocuments()); } } @@ -1335,7 +1329,7 @@ private CustomElandModel getCustomElandModel(TaskType taskType) { taskType, ElasticsearchInternalService.NAME, new CustomElandInternalServiceSettings(1, 4, "custom-model", null), - CustomElandRerankTaskSettings.DEFAULT_SETTINGS + RerankTaskSettings.DEFAULT_SETTINGS ); } else if (taskType == TaskType.TEXT_EMBEDDING) { var serviceSettings = new CustomElandInternalTextEmbeddingServiceSettings(1, 4, "custom-model", null); @@ -1528,20 +1522,30 @@ public void testEmbeddingTypeFromTaskTypeAndSettings() { ) ); - var e = expectThrows( + var e1 = expectThrows( ElasticsearchStatusException.class, () -> ElasticsearchInternalService.embeddingTypeFromTaskTypeAndSettings( TaskType.COMPLETION, new ElasticsearchInternalServiceSettings(1, 1, "foo", null) ) ); - assertThat(e.getMessage(), containsString("Chunking is not supported for task type [completion]")); + assertThat(e1.getMessage(), containsString("Chunking is not supported for task type [completion]")); + + var e2 = expectThrows( + ElasticsearchStatusException.class, + () -> ElasticsearchInternalService.embeddingTypeFromTaskTypeAndSettings( + TaskType.RERANK, + new ElasticsearchInternalServiceSettings(1, 1, "foo", null) + ) + ); + assertThat(e2.getMessage(), containsString("Chunking is not supported for task type [rerank]")); } public void testIsDefaultId() { var service = createService(mock(Client.class)); assertTrue(service.isDefaultId(".elser-2-elasticsearch")); assertTrue(service.isDefaultId(".multilingual-e5-small-elasticsearch")); + assertTrue(service.isDefaultId(".rerank-v1-elasticsearch")); assertFalse(service.isDefaultId("foo")); } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankTaskSettingsTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/RerankTaskSettingsTests.java similarity index 53% rename from x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankTaskSettingsTests.java rename to x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/RerankTaskSettingsTests.java index 4207896fc54f3..255454a1ed62b 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandRerankTaskSettingsTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/RerankTaskSettingsTests.java @@ -22,7 +22,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.sameInstance; -public class CustomElandRerankTaskSettingsTests extends AbstractWireSerializingTestCase { +public class RerankTaskSettingsTests extends AbstractWireSerializingTestCase { public void testIsEmpty() { var randomSettings = createRandom(); @@ -35,9 +35,9 @@ public void testUpdatedTaskSettings() { var newSettings = createRandom(); Map newSettingsMap = new HashMap<>(); if (newSettings.returnDocuments() != null) { - newSettingsMap.put(CustomElandRerankTaskSettings.RETURN_DOCUMENTS, newSettings.returnDocuments()); + newSettingsMap.put(RerankTaskSettings.RETURN_DOCUMENTS, newSettings.returnDocuments()); } - CustomElandRerankTaskSettings updatedSettings = (CustomElandRerankTaskSettings) initialSettings.updatedTaskSettings( + RerankTaskSettings updatedSettings = (RerankTaskSettings) initialSettings.updatedTaskSettings( Collections.unmodifiableMap(newSettingsMap) ); if (newSettings.returnDocuments() == null) { @@ -48,37 +48,37 @@ public void testUpdatedTaskSettings() { } public void testDefaultsFromMap_MapIsNull_ReturnsDefaultSettings() { - var customElandRerankTaskSettings = CustomElandRerankTaskSettings.defaultsFromMap(null); + var rerankTaskSettings = RerankTaskSettings.defaultsFromMap(null); - assertThat(customElandRerankTaskSettings, sameInstance(CustomElandRerankTaskSettings.DEFAULT_SETTINGS)); + assertThat(rerankTaskSettings, sameInstance(RerankTaskSettings.DEFAULT_SETTINGS)); } public void testDefaultsFromMap_MapIsEmpty_ReturnsDefaultSettings() { - var customElandRerankTaskSettings = CustomElandRerankTaskSettings.defaultsFromMap(new HashMap<>()); + var rerankTaskSettings = RerankTaskSettings.defaultsFromMap(new HashMap<>()); - assertThat(customElandRerankTaskSettings, sameInstance(CustomElandRerankTaskSettings.DEFAULT_SETTINGS)); + assertThat(rerankTaskSettings, sameInstance(RerankTaskSettings.DEFAULT_SETTINGS)); } public void testDefaultsFromMap_ExtractedReturnDocumentsNull_SetsReturnDocumentToTrue() { - var customElandRerankTaskSettings = CustomElandRerankTaskSettings.defaultsFromMap(new HashMap<>()); + var rerankTaskSettings = RerankTaskSettings.defaultsFromMap(new HashMap<>()); - assertThat(customElandRerankTaskSettings.returnDocuments(), is(Boolean.TRUE)); + assertThat(rerankTaskSettings.returnDocuments(), is(Boolean.TRUE)); } public void testFromMap_MapIsNull_ReturnsDefaultSettings() { - var customElandRerankTaskSettings = CustomElandRerankTaskSettings.fromMap(null); + var rerankTaskSettings = RerankTaskSettings.fromMap(null); - assertThat(customElandRerankTaskSettings, sameInstance(CustomElandRerankTaskSettings.DEFAULT_SETTINGS)); + assertThat(rerankTaskSettings, sameInstance(RerankTaskSettings.DEFAULT_SETTINGS)); } public void testFromMap_MapIsEmpty_ReturnsDefaultSettings() { - var customElandRerankTaskSettings = CustomElandRerankTaskSettings.fromMap(new HashMap<>()); + var rerankTaskSettings = RerankTaskSettings.fromMap(new HashMap<>()); - assertThat(customElandRerankTaskSettings, sameInstance(CustomElandRerankTaskSettings.DEFAULT_SETTINGS)); + assertThat(rerankTaskSettings, sameInstance(RerankTaskSettings.DEFAULT_SETTINGS)); } public void testToXContent_WritesAllValues() throws IOException { - var serviceSettings = new CustomElandRerankTaskSettings(Boolean.TRUE); + var serviceSettings = new RerankTaskSettings(Boolean.TRUE); XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON); serviceSettings.toXContent(builder, null); @@ -89,30 +89,30 @@ public void testToXContent_WritesAllValues() throws IOException { } public void testOf_PrefersNonNullRequestTaskSettings() { - var originalSettings = new CustomElandRerankTaskSettings(Boolean.FALSE); - var requestTaskSettings = new CustomElandRerankTaskSettings(Boolean.TRUE); + var originalSettings = new RerankTaskSettings(Boolean.FALSE); + var requestTaskSettings = new RerankTaskSettings(Boolean.TRUE); - var taskSettings = CustomElandRerankTaskSettings.of(originalSettings, requestTaskSettings); + var taskSettings = RerankTaskSettings.of(originalSettings, requestTaskSettings); assertThat(taskSettings, sameInstance(requestTaskSettings)); } - private static CustomElandRerankTaskSettings createRandom() { - return new CustomElandRerankTaskSettings(randomOptionalBoolean()); + private static RerankTaskSettings createRandom() { + return new RerankTaskSettings(randomOptionalBoolean()); } @Override - protected Writeable.Reader instanceReader() { - return CustomElandRerankTaskSettings::new; + protected Writeable.Reader instanceReader() { + return RerankTaskSettings::new; } @Override - protected CustomElandRerankTaskSettings createTestInstance() { + protected RerankTaskSettings createTestInstance() { return createRandom(); } @Override - protected CustomElandRerankTaskSettings mutateInstance(CustomElandRerankTaskSettings instance) throws IOException { - return randomValueOtherThan(instance, CustomElandRerankTaskSettingsTests::createRandom); + protected RerankTaskSettings mutateInstance(RerankTaskSettings instance) throws IOException { + return randomValueOtherThan(instance, RerankTaskSettingsTests::createRandom); } } From 6209b4fa260f9827d5266709de7ac8557da161e1 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Wed, 11 Dec 2024 13:28:23 +0000 Subject: [PATCH 05/77] Add the `USE_SYNTHETIC_SOURCE_FOR_RECOVERY_BACKPORT` Index Version (#118450) This PR introduces the backported `USE_SYNTHETIC_SOURCE_FOR_RECOVERY_BACKPORT` index version and updates the index version check to accommodate this addition. --- .../elasticsearch/index/IndexSettings.java | 6 +++- .../elasticsearch/index/IndexVersions.java | 1 + .../index/mapper/SourceFieldMapperTests.java | 31 ++++++++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/IndexSettings.java b/server/src/main/java/org/elasticsearch/index/IndexSettings.java index 8f0373d951319..b15828c5594ae 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexSettings.java +++ b/server/src/main/java/org/elasticsearch/index/IndexSettings.java @@ -688,7 +688,11 @@ public void validate(Boolean enabled, Map, Object> settings) { // Verify that all nodes can handle this setting var version = (IndexVersion) settings.get(SETTING_INDEX_VERSION_CREATED); - if (version.before(IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY)) { + if (version.before(IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY) + && version.between( + IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY_BACKPORT, + IndexVersions.UPGRADE_TO_LUCENE_10_0_0 + ) == false) { throw new IllegalArgumentException( String.format( Locale.ROOT, diff --git a/server/src/main/java/org/elasticsearch/index/IndexVersions.java b/server/src/main/java/org/elasticsearch/index/IndexVersions.java index 56f40309a825b..5589508507aec 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexVersions.java +++ b/server/src/main/java/org/elasticsearch/index/IndexVersions.java @@ -132,6 +132,7 @@ private static Version parseUnchecked(String version) { public static final IndexVersion LOGSDB_DEFAULT_IGNORE_DYNAMIC_BEYOND_LIMIT_BACKPORT = def(8_519_00_0, Version.LUCENE_9_12_0); public static final IndexVersion TIME_BASED_K_ORDERED_DOC_ID_BACKPORT = def(8_520_00_0, Version.LUCENE_9_12_0); public static final IndexVersion V8_DEPRECATE_SOURCE_MODE_MAPPER = def(8_521_00_0, Version.LUCENE_9_12_0); + public static final IndexVersion USE_SYNTHETIC_SOURCE_FOR_RECOVERY_BACKPORT = def(8_522_00_0, Version.LUCENE_9_12_0); public static final IndexVersion UPGRADE_TO_LUCENE_10_0_0 = def(9_000_00_0, Version.LUCENE_10_0_0); public static final IndexVersion LOGSDB_DEFAULT_IGNORE_DYNAMIC_BEYOND_LIMIT = def(9_001_00_0, Version.LUCENE_10_0_0); public static final IndexVersion TIME_BASED_K_ORDERED_DOC_ID = def(9_002_00_0, Version.LUCENE_10_0_0); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java index bec9cb5fa9be0..378920d0e6db5 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/SourceFieldMapperTests.java @@ -473,7 +473,36 @@ public void testRecoverySourceWitInvalidSettings() { IllegalArgumentException exc = expectThrows( IllegalArgumentException.class, () -> createMapperService( - IndexVersionUtils.randomPreviousCompatibleVersion(random(), IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY), + IndexVersionUtils.randomPreviousCompatibleVersion(random(), IndexVersions.USE_SYNTHETIC_SOURCE_FOR_RECOVERY_BACKPORT), + settings, + () -> false, + topMapping(b -> {}) + ) + ); + assertThat( + exc.getMessage(), + containsString( + String.format( + Locale.ROOT, + "The setting [%s] is unavailable on this cluster", + IndexSettings.RECOVERY_USE_SYNTHETIC_SOURCE_SETTING.getKey() + ) + ) + ); + } + { + Settings settings = Settings.builder() + .put(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(), SourceFieldMapper.Mode.SYNTHETIC.toString()) + .put(IndexSettings.RECOVERY_USE_SYNTHETIC_SOURCE_SETTING.getKey(), true) + .build(); + IllegalArgumentException exc = expectThrows( + IllegalArgumentException.class, + () -> createMapperService( + IndexVersionUtils.randomVersionBetween( + random(), + IndexVersions.UPGRADE_TO_LUCENE_10_0_0, + IndexVersions.DEPRECATE_SOURCE_MODE_MAPPER + ), settings, () -> false, topMapping(b -> {}) From 5e467bfa04bc6f8e13f2837635955ade93962ead Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 11 Dec 2024 13:44:36 +0000 Subject: [PATCH 06/77] Drop unused `@ThirdParty` tests (#118432) We don't run any such tests in CI, and probably haven't even run them manually for years. The only such test with this annotation is a trivial smoke test for `discovery-ec2` that is equally well covered by the Java REST test suite. --- plugins/discovery-ec2/build.gradle | 1 - .../discovery/ec2/AbstractAwsTestCase.java | 63 ------------------- .../ec2/Ec2DiscoveryUpdateSettingsTests.java | 42 ------------- .../elasticsearch/test/ESIntegTestCase.java | 21 ------- 4 files changed, 127 deletions(-) delete mode 100644 plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/AbstractAwsTestCase.java delete mode 100644 plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryUpdateSettingsTests.java diff --git a/plugins/discovery-ec2/build.gradle b/plugins/discovery-ec2/build.gradle index 169b4388d464f..2335577225340 100644 --- a/plugins/discovery-ec2/build.gradle +++ b/plugins/discovery-ec2/build.gradle @@ -6,7 +6,6 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ -apply plugin: 'elasticsearch.internal-cluster-test' apply plugin: 'elasticsearch.internal-java-rest-test' esplugin { diff --git a/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/AbstractAwsTestCase.java b/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/AbstractAwsTestCase.java deleted file mode 100644 index 6225fcb52df5d..0000000000000 --- a/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/AbstractAwsTestCase.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.discovery.ec2; - -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.settings.SettingsException; -import org.elasticsearch.core.PathUtils; -import org.elasticsearch.env.Environment; -import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.test.ESIntegTestCase; -import org.elasticsearch.test.ESIntegTestCase.ThirdParty; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; - -/** - * Base class for AWS tests that require credentials. - *

- * You must specify {@code -Dtests.thirdparty=true -Dtests.config=/path/to/config} - * in order to run these tests. - */ -@ThirdParty -public abstract class AbstractAwsTestCase extends ESIntegTestCase { - - @Override - protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { - Settings.Builder settings = Settings.builder() - .put(super.nodeSettings(nodeOrdinal, otherSettings)) - .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()); - - // if explicit, just load it and don't load from env - try { - if (Strings.hasText(System.getProperty("tests.config"))) { - try { - settings.loadFromPath(PathUtils.get(System.getProperty("tests.config"))); - } catch (IOException e) { - throw new IllegalArgumentException("could not load aws tests config", e); - } - } else { - throw new IllegalStateException( - "to run integration tests, you need to set -Dtests.thirdparty=true and -Dtests.config=/path/to/elasticsearch.yml" - ); - } - } catch (SettingsException exception) { - throw new IllegalStateException("your test configuration file is incorrect: " + System.getProperty("tests.config"), exception); - } - return settings.build(); - } - - @Override - protected Collection> nodePlugins() { - return Arrays.asList(Ec2DiscoveryPlugin.class); - } -} diff --git a/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryUpdateSettingsTests.java b/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryUpdateSettingsTests.java deleted file mode 100644 index b43f4afb8145f..0000000000000 --- a/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/Ec2DiscoveryUpdateSettingsTests.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.discovery.ec2; - -import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; -import org.elasticsearch.common.UUIDs; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.discovery.DiscoveryModule; -import org.elasticsearch.test.ESIntegTestCase.ClusterScope; -import org.elasticsearch.test.ESIntegTestCase.Scope; - -import static org.hamcrest.CoreMatchers.is; - -/** - * Just an empty Node Start test to check eveything if fine when - * starting. - * This test requires AWS to run. - */ -@ClusterScope(scope = Scope.TEST, numDataNodes = 0, numClientNodes = 0) -public class Ec2DiscoveryUpdateSettingsTests extends AbstractAwsTestCase { - public void testMinimumMasterNodesStart() { - Settings nodeSettings = Settings.builder().put(DiscoveryModule.DISCOVERY_SEED_PROVIDERS_SETTING.getKey(), "ec2").build(); - internalCluster().startNode(nodeSettings); - - // We try to update a setting now - final String expectedValue = UUIDs.randomBase64UUID(random()); - final String settingName = "cluster.routing.allocation.exclude.any_attribute"; - final ClusterUpdateSettingsResponse response = clusterAdmin().prepareUpdateSettings(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT) - .setPersistentSettings(Settings.builder().put(settingName, expectedValue)) - .get(); - - final String value = response.getPersistentSettings().get(settingName); - assertThat(value, is(expectedValue)); - } -} diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java index af92eae8c8a19..b9a097b4e76f3 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java @@ -13,7 +13,6 @@ import io.netty.util.concurrent.GlobalEventExecutor; import com.carrotsearch.randomizedtesting.RandomizedContext; -import com.carrotsearch.randomizedtesting.annotations.TestGroup; import com.carrotsearch.randomizedtesting.generators.RandomNumbers; import com.carrotsearch.randomizedtesting.generators.RandomPicks; @@ -273,26 +272,6 @@ @LuceneTestCase.SuppressFileSystems("ExtrasFS") // doesn't work with potential multi data path from test cluster yet public abstract class ESIntegTestCase extends ESTestCase { - /** - * Property that controls whether ThirdParty Integration tests are run (not the default). - */ - public static final String SYSPROP_THIRDPARTY = "tests.thirdparty"; - - /** - * Annotation for third-party integration tests. - *

- * These are tests, which require a third-party service in order to run. They - * may require the user to manually configure an external process (such as rabbitmq), - * or may additionally require some external configuration (e.g. AWS credentials) - * via the {@code tests.config} system property. - */ - @Inherited - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.TYPE) - @TestGroup(enabled = false, sysProperty = ESIntegTestCase.SYSPROP_THIRDPARTY) - public @interface ThirdParty { - } - /** node names of the corresponding clusters will start with these prefixes */ public static final String SUITE_CLUSTER_NODE_PREFIX = "node_s"; public static final String TEST_CLUSTER_NODE_PREFIX = "node_t"; From d7453159e80603f25ab0bd5e0fb84b1b2bdd7ea6 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Wed, 11 Dec 2024 05:54:00 -0800 Subject: [PATCH 07/77] Remove old instrumentation tests (#118411) The newer "sythetic" tests cover all the cases of instrumentation --- .../impl/InstrumenterTests.java | 278 ------------------ ...arch.entitlement.bridge.EntitlementChecker | 10 - 2 files changed, 288 deletions(-) delete mode 100644 libs/entitlement/asm-provider/src/test/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterTests.java delete mode 100644 libs/entitlement/asm-provider/src/test/resources/META-INF/services/org.elasticsearch.entitlement.bridge.EntitlementChecker diff --git a/libs/entitlement/asm-provider/src/test/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterTests.java b/libs/entitlement/asm-provider/src/test/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterTests.java deleted file mode 100644 index c8e1b26d1fc52..0000000000000 --- a/libs/entitlement/asm-provider/src/test/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterTests.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.entitlement.instrumentation.impl; - -import org.elasticsearch.entitlement.bridge.EntitlementChecker; -import org.elasticsearch.entitlement.instrumentation.CheckMethod; -import org.elasticsearch.entitlement.instrumentation.MethodKey; -import org.elasticsearch.logging.LogManager; -import org.elasticsearch.logging.Logger; -import org.elasticsearch.test.ESTestCase; -import org.junit.Before; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Type; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.net.URLStreamHandlerFactory; -import java.util.List; -import java.util.Map; - -import static org.elasticsearch.entitlement.instrumentation.impl.ASMUtils.bytecode2text; -import static org.elasticsearch.entitlement.instrumentation.impl.TestMethodUtils.callStaticMethod; -import static org.elasticsearch.entitlement.instrumentation.impl.TestMethodUtils.getCheckMethod; -import static org.elasticsearch.entitlement.instrumentation.impl.TestMethodUtils.methodKeyForConstructor; -import static org.elasticsearch.entitlement.instrumentation.impl.TestMethodUtils.methodKeyForTarget; -import static org.hamcrest.Matchers.arrayContaining; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.instanceOf; -import static org.objectweb.asm.Opcodes.INVOKESTATIC; - -/** - * This tests {@link InstrumenterImpl} in isolation, without a java agent. - * It causes the methods to be instrumented, and verifies that the instrumentation is called as expected. - * Problems with bytecode generation are easier to debug this way than in the context of an agent. - */ -@ESTestCase.WithoutSecurityManager -public class InstrumenterTests extends ESTestCase { - - static volatile TestEntitlementChecker testChecker; - - public static TestEntitlementChecker getTestEntitlementChecker() { - return testChecker; - } - - @Before - public void initialize() { - testChecker = new TestEntitlementChecker(); - } - - /** - * Contains all the virtual methods from {@link ClassToInstrument}, - * allowing this test to call them on the dynamically loaded instrumented class. - */ - public interface Testable {} - - /** - * This is a placeholder for real class library methods. - * Without the java agent, we can't instrument the real methods, so we instrument this instead. - *

- * Methods of this class must have the same signature and the same static/virtual condition as the corresponding real method. - * They should assert that the arguments came through correctly. - * They must not throw {@link TestException}. - */ - public static class ClassToInstrument implements Testable { - - public ClassToInstrument() {} - - // URLClassLoader ctor - public ClassToInstrument(URL[] urls) {} - - public static void systemExit(int status) { - assertEquals(123, status); - } - } - - private static final String SAMPLE_NAME = "TEST"; - - private static final URL SAMPLE_URL = createSampleUrl(); - - private static URL createSampleUrl() { - try { - return URI.create("file:/test/example").toURL(); - } catch (MalformedURLException e) { - return null; - } - } - - /** - * We're not testing the permission checking logic here; - * only that the instrumented methods are calling the correct check methods with the correct arguments. - * This is a trivial implementation of {@link EntitlementChecker} that just always throws, - * just to demonstrate that the injected bytecodes succeed in calling these methods. - * It also asserts that the arguments are correct. - */ - public static class TestEntitlementChecker implements EntitlementChecker { - /** - * This allows us to test that the instrumentation is correct in both cases: - * if the check throws, and if it doesn't. - */ - volatile boolean isActive; - - int checkSystemExitCallCount = 0; - int checkURLClassLoaderCallCount = 0; - - @Override - public void check$java_lang_System$exit(Class callerClass, int status) { - checkSystemExitCallCount++; - assertSame(TestMethodUtils.class, callerClass); - assertEquals(123, status); - throwIfActive(); - } - - @Override - public void check$java_net_URLClassLoader$(Class callerClass, URL[] urls) { - checkURLClassLoaderCallCount++; - assertSame(InstrumenterTests.class, callerClass); - assertThat(urls, arrayContaining(SAMPLE_URL)); - throwIfActive(); - } - - @Override - public void check$java_net_URLClassLoader$(Class callerClass, URL[] urls, ClassLoader parent) { - checkURLClassLoaderCallCount++; - assertSame(InstrumenterTests.class, callerClass); - assertThat(urls, arrayContaining(SAMPLE_URL)); - assertThat(parent, equalTo(ClassLoader.getSystemClassLoader())); - throwIfActive(); - } - - @Override - public void check$java_net_URLClassLoader$(Class callerClass, URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) { - checkURLClassLoaderCallCount++; - assertSame(InstrumenterTests.class, callerClass); - assertThat(urls, arrayContaining(SAMPLE_URL)); - assertThat(parent, equalTo(ClassLoader.getSystemClassLoader())); - throwIfActive(); - } - - @Override - public void check$java_net_URLClassLoader$(Class callerClass, String name, URL[] urls, ClassLoader parent) { - checkURLClassLoaderCallCount++; - assertSame(InstrumenterTests.class, callerClass); - assertThat(name, equalTo(SAMPLE_NAME)); - assertThat(urls, arrayContaining(SAMPLE_URL)); - assertThat(parent, equalTo(ClassLoader.getSystemClassLoader())); - throwIfActive(); - } - - @Override - public void check$java_net_URLClassLoader$( - Class callerClass, - String name, - URL[] urls, - ClassLoader parent, - URLStreamHandlerFactory factory - ) { - checkURLClassLoaderCallCount++; - assertSame(InstrumenterTests.class, callerClass); - assertThat(name, equalTo(SAMPLE_NAME)); - assertThat(urls, arrayContaining(SAMPLE_URL)); - assertThat(parent, equalTo(ClassLoader.getSystemClassLoader())); - throwIfActive(); - } - - private void throwIfActive() { - if (isActive) { - throw new TestException(); - } - } - } - - public void testSystemExitIsInstrumented() throws Exception { - var classToInstrument = ClassToInstrument.class; - - Map checkMethods = Map.of( - methodKeyForTarget(classToInstrument.getMethod("systemExit", int.class)), - getCheckMethod(EntitlementChecker.class, "check$java_lang_System$exit", Class.class, int.class) - ); - - var instrumenter = createInstrumenter(checkMethods); - - byte[] newBytecode = instrumenter.instrumentClassFile(classToInstrument).bytecodes(); - - if (logger.isTraceEnabled()) { - logger.trace("Bytecode after instrumentation:\n{}", bytecode2text(newBytecode)); - } - - Class newClass = new TestLoader(Testable.class.getClassLoader()).defineClassFromBytes( - classToInstrument.getName() + "_NEW", - newBytecode - ); - - getTestEntitlementChecker().isActive = false; - - // Before checking is active, nothing should throw - callStaticMethod(newClass, "systemExit", 123); - - getTestEntitlementChecker().isActive = true; - - // After checking is activated, everything should throw - assertThrows(TestException.class, () -> callStaticMethod(newClass, "systemExit", 123)); - } - - public void testURLClassLoaderIsInstrumented() throws Exception { - var classToInstrument = ClassToInstrument.class; - - Map checkMethods = Map.of( - methodKeyForConstructor(classToInstrument, List.of(Type.getInternalName(URL[].class))), - getCheckMethod(EntitlementChecker.class, "check$java_net_URLClassLoader$", Class.class, URL[].class) - ); - - var instrumenter = createInstrumenter(checkMethods); - - byte[] newBytecode = instrumenter.instrumentClassFile(classToInstrument).bytecodes(); - - if (logger.isTraceEnabled()) { - logger.trace("Bytecode after instrumentation:\n{}", bytecode2text(newBytecode)); - } - - Class newClass = new TestLoader(Testable.class.getClassLoader()).defineClassFromBytes( - classToInstrument.getName() + "_NEW", - newBytecode - ); - - getTestEntitlementChecker().isActive = false; - - // Before checking is active, nothing should throw - newClass.getConstructor(URL[].class).newInstance((Object) new URL[] { SAMPLE_URL }); - - getTestEntitlementChecker().isActive = true; - - // After checking is activated, everything should throw - var exception = assertThrows( - InvocationTargetException.class, - () -> newClass.getConstructor(URL[].class).newInstance((Object) new URL[] { SAMPLE_URL }) - ); - assertThat(exception.getCause(), instanceOf(TestException.class)); - } - - /** This test doesn't replace classToInstrument in-place but instead loads a separate - * class with the same class name plus a "_NEW" suffix (classToInstrument.class.getName() + "_NEW") - * that contains the instrumentation. Because of this, we need to configure the Transformer to use a - * MethodKey and instrumentationMethod with slightly different signatures (using the common interface - * Testable) which is not what would happen when it's run by the agent. - */ - private InstrumenterImpl createInstrumenter(Map checkMethods) throws NoSuchMethodException { - Method getter = InstrumenterTests.class.getMethod("getTestEntitlementChecker"); - - return new InstrumenterImpl(null, null, "_NEW", checkMethods) { - /** - * We're not testing the bridge library here. - * Just call our own getter instead. - */ - @Override - protected void pushEntitlementChecker(MethodVisitor mv) { - mv.visitMethodInsn( - INVOKESTATIC, - Type.getInternalName(getter.getDeclaringClass()), - getter.getName(), - Type.getMethodDescriptor(getter), - false - ); - } - }; - } - - private static final Logger logger = LogManager.getLogger(InstrumenterTests.class); -} diff --git a/libs/entitlement/asm-provider/src/test/resources/META-INF/services/org.elasticsearch.entitlement.bridge.EntitlementChecker b/libs/entitlement/asm-provider/src/test/resources/META-INF/services/org.elasticsearch.entitlement.bridge.EntitlementChecker deleted file mode 100644 index 172ac1d2ab30b..0000000000000 --- a/libs/entitlement/asm-provider/src/test/resources/META-INF/services/org.elasticsearch.entitlement.bridge.EntitlementChecker +++ /dev/null @@ -1,10 +0,0 @@ -# - # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - # or more contributor license agreements. Licensed under the "Elastic License - # 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - # Public License v 1"; you may not use this file except in compliance with, at - # your election, the "Elastic License 2.0", the "GNU Affero General Public - # License v3.0 only", or the "Server Side Public License, v 1". -# - -org.elasticsearch.entitlement.instrumentation.impl.InstrumenterTests$TestEntitlementChecker From 4da72997e956532a2a9d2b98c4c4659a5d26ea86 Mon Sep 17 00:00:00 2001 From: Patrick Doyle <810052+prdoyle@users.noreply.github.com> Date: Wed, 11 Dec 2024 09:38:40 -0500 Subject: [PATCH 08/77] Use single-task queues in ReservedClusterStateService (#118351) * Refactor: submitUpdateTask method * Test for one task per reserved state udate; currently fails * Separate queue per task * Spotless --- .../service/ReservedClusterStateService.java | 32 ++++---- .../ReservedClusterStateServiceTests.java | 77 +++++++++++++++++++ 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateService.java b/server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateService.java index 499b5e6515a8c..248d37914cf32 100644 --- a/server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateService.java +++ b/server/src/main/java/org/elasticsearch/reservedstate/service/ReservedClusterStateService.java @@ -18,7 +18,6 @@ import org.elasticsearch.cluster.metadata.ReservedStateMetadata; import org.elasticsearch.cluster.routing.RerouteService; import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.cluster.service.MasterServiceTaskQueue; import org.elasticsearch.common.Priority; import org.elasticsearch.core.Tuple; import org.elasticsearch.env.BuildVersion; @@ -61,8 +60,6 @@ public class ReservedClusterStateService { final Map> handlers; final ClusterService clusterService; - private final MasterServiceTaskQueue updateTaskQueue; - private final MasterServiceTaskQueue errorTaskQueue; @SuppressWarnings("unchecked") private final ConstructingObjectParser stateChunkParser = new ConstructingObjectParser<>( @@ -77,6 +74,8 @@ public class ReservedClusterStateService { return new ReservedStateChunk(stateMap, (ReservedStateVersion) a[1]); } ); + private final ReservedStateUpdateTaskExecutor updateTaskExecutor; + private final ReservedStateErrorTaskExecutor errorTaskExecutor; /** * Controller class for saving and reserving {@link ClusterState}. @@ -89,12 +88,8 @@ public ReservedClusterStateService( List> handlerList ) { this.clusterService = clusterService; - this.updateTaskQueue = clusterService.createTaskQueue( - "reserved state update", - Priority.URGENT, - new ReservedStateUpdateTaskExecutor(rerouteService) - ); - this.errorTaskQueue = clusterService.createTaskQueue("reserved state error", Priority.URGENT, new ReservedStateErrorTaskExecutor()); + this.updateTaskExecutor = new ReservedStateUpdateTaskExecutor(rerouteService); + this.errorTaskExecutor = new ReservedStateErrorTaskExecutor(); this.handlers = handlerList.stream().collect(Collectors.toMap(ReservedClusterStateHandler::name, Function.identity())); stateChunkParser.declareNamedObjects(ConstructingObjectParser.constructorArg(), (p, c, name) -> { if (handlers.containsKey(name) == false) { @@ -160,7 +155,7 @@ public void process( public void initEmpty(String namespace, ActionListener listener) { var missingVersion = new ReservedStateVersion(EMPTY_VERSION, BuildVersion.current()); var emptyState = new ReservedStateChunk(Map.of(), missingVersion); - updateTaskQueue.submitTask( + submitUpdateTask( "empty initial cluster state [" + namespace + "]", new ReservedStateUpdateTask( namespace, @@ -171,10 +166,8 @@ public void initEmpty(String namespace, ActionListener lis // error state should not be possible since there is no metadata being parsed or processed errorState -> { throw new AssertionError(); }, listener - ), - null + ) ); - } /** @@ -234,7 +227,7 @@ public void process( errorListener.accept(error); return; } - updateTaskQueue.submitTask( + submitUpdateTask( "reserved cluster state [" + namespace + "]", new ReservedStateUpdateTask( namespace, @@ -242,7 +235,7 @@ public void process( versionCheck, handlers, orderedHandlers, - ReservedClusterStateService.this::updateErrorState, + this::updateErrorState, new ActionListener<>() { @Override public void onResponse(ActionResponse.Empty empty) { @@ -261,8 +254,7 @@ public void onFailure(Exception e) { } } } - ), - null + ) ); } @@ -293,6 +285,11 @@ Exception checkAndReportError( return null; } + void submitUpdateTask(String source, ReservedStateUpdateTask task) { + var updateTaskQueue = clusterService.createTaskQueue("reserved state update", Priority.URGENT, updateTaskExecutor); + updateTaskQueue.submitTask(source, task, null); + } + // package private for testing void updateErrorState(ErrorState errorState) { // optimistic check here - the cluster state might change after this, so also need to re-check later @@ -305,6 +302,7 @@ void updateErrorState(ErrorState errorState) { } private void submitErrorUpdateTask(ErrorState errorState) { + var errorTaskQueue = clusterService.createTaskQueue("reserved state error", Priority.URGENT, errorTaskExecutor); errorTaskQueue.submitTask( "reserved cluster state update error for [ " + errorState.namespace() + "]", new ReservedStateErrorTask(errorState, new ActionListener<>() { diff --git a/server/src/test/java/org/elasticsearch/reservedstate/service/ReservedClusterStateServiceTests.java b/server/src/test/java/org/elasticsearch/reservedstate/service/ReservedClusterStateServiceTests.java index efe3566064170..982f5c4a93ae0 100644 --- a/server/src/test/java/org/elasticsearch/reservedstate/service/ReservedClusterStateServiceTests.java +++ b/server/src/test/java/org/elasticsearch/reservedstate/service/ReservedClusterStateServiceTests.java @@ -47,6 +47,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.function.LongFunction; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.contains; @@ -67,6 +68,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -332,6 +334,81 @@ public void testUpdateErrorState() { verifyNoMoreInteractions(errorQueue); } + @SuppressWarnings("unchecked") + public void testOneUpdateTaskPerQueue() { + ClusterState state = ClusterState.builder(new ClusterName("test")).build(); + MasterServiceTaskQueue queue1 = mockTaskQueue(); + MasterServiceTaskQueue queue2 = mockTaskQueue(); + MasterServiceTaskQueue unusedQueue = mockTaskQueue(); + + ClusterService clusterService = mock(ClusterService.class); + when(clusterService.createTaskQueue(anyString(), any(), any())) // For non-update tasks + .thenReturn(unusedQueue); + when(clusterService.createTaskQueue(ArgumentMatchers.contains("reserved state update"), any(), any())) + .thenReturn(queue1, queue2, unusedQueue); + when(clusterService.state()).thenReturn(state); + + ReservedClusterStateService service = new ReservedClusterStateService(clusterService, mock(RerouteService.class), List.of()); + LongFunction update = version -> { + ReservedStateUpdateTask task = spy( + new ReservedStateUpdateTask( + "test", + new ReservedStateChunk(Map.of(), new ReservedStateVersion(version, BuildVersion.current())), + ReservedStateVersionCheck.HIGHER_VERSION_ONLY, + Map.of(), + Set.of(), + errorState -> {}, + ActionListener.noop() + ) + ); + doReturn(state).when(task).execute(any()); + return task; + }; + + service.submitUpdateTask("test", update.apply(2L)); + service.submitUpdateTask("test", update.apply(3L)); + + // One task to each queue + verify(queue1).submitTask(any(), any(), any()); + verify(queue2).submitTask(any(), any(), any()); + + // No additional unexpected tasks + verifyNoInteractions(unusedQueue); + } + + @SuppressWarnings("unchecked") + public void testOneErrorTaskPerQueue() { + ClusterState state = ClusterState.builder(new ClusterName("test")).build(); + MasterServiceTaskQueue queue1 = mockTaskQueue(); + MasterServiceTaskQueue queue2 = mockTaskQueue(); + MasterServiceTaskQueue unusedQueue = mockTaskQueue(); + + ClusterService clusterService = mock(ClusterService.class); + when(clusterService.createTaskQueue(anyString(), any(), any())) // For non-error tasks + .thenReturn(unusedQueue); + when(clusterService.createTaskQueue(ArgumentMatchers.contains("reserved state error"), any(), any())) + .thenReturn(queue1, queue2, unusedQueue); + when(clusterService.state()).thenReturn(state); + + ReservedClusterStateService service = new ReservedClusterStateService(clusterService, mock(RerouteService.class), List.of()); + LongFunction error = version -> new ErrorState( + "namespace", + version, + ReservedStateVersionCheck.HIGHER_VERSION_ONLY, + List.of("error"), + ReservedStateErrorMetadata.ErrorKind.TRANSIENT + ); + service.updateErrorState(error.apply(2)); + service.updateErrorState(error.apply(3)); + + // One task to each queue + verify(queue1).submitTask(any(), any(), any()); + verify(queue2).submitTask(any(), any(), any()); + + // No additional unexpected tasks + verifyNoInteractions(unusedQueue); + } + public void testErrorStateTask() throws Exception { ClusterState state = ClusterState.builder(new ClusterName("test")).build(); From ea37a8acc0bc1b22867e6d033004ed37be176863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Wed, 11 Dec 2024 15:55:02 +0100 Subject: [PATCH 09/77] [Entitlements] Moving and refactoring IT tests (#118254) --- .../entitlement/qa}/build.gradle | 19 +-- libs/entitlement/qa/common/build.gradle | 15 +++ .../qa/common/src/main/java/module-info.java | 16 +++ .../common/RestEntitlementsCheckAction.java | 112 ++++++++++++++++++ .../build.gradle | 24 ++++ .../EntitlementAllowedNonModularPlugin.java | 45 +++++++ .../plugin-metadata/entitlement-policy.yaml | 2 + .../qa/entitlement-allowed/build.gradle | 25 ++++ .../src/main/java/module-info.java | 15 +++ .../qa/EntitlementAllowedPlugin.java | 45 +++++++ .../plugin-metadata/entitlement-policy.yaml | 2 + .../build.gradle | 24 ++++ .../EntitlementDeniedNonModularPlugin.java | 45 +++++++ .../qa/entitlement-denied/build.gradle | 25 ++++ .../src/main/java/module-info.java | 3 +- .../qa/EntitlementDeniedPlugin.java | 9 +- .../entitlement/qa/EntitlementsAllowedIT.java | 66 +++++++++++ .../entitlement/qa/EntitlementsDeniedIT.java | 65 ++++++++++ .../test/entitlements/EntitlementsIT.java | 49 -------- ...estEntitlementsCheckClassLoaderAction.java | 54 --------- ...RestEntitlementsCheckSystemExitAction.java | 46 ------- 21 files changed, 537 insertions(+), 169 deletions(-) rename {qa/entitlements => libs/entitlement/qa}/build.gradle (59%) create mode 100644 libs/entitlement/qa/common/build.gradle create mode 100644 libs/entitlement/qa/common/src/main/java/module-info.java create mode 100644 libs/entitlement/qa/common/src/main/java/org/elasticsearch/entitlement/qa/common/RestEntitlementsCheckAction.java create mode 100644 libs/entitlement/qa/entitlement-allowed-nonmodular/build.gradle create mode 100644 libs/entitlement/qa/entitlement-allowed-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementAllowedNonModularPlugin.java create mode 100644 libs/entitlement/qa/entitlement-allowed-nonmodular/src/main/plugin-metadata/entitlement-policy.yaml create mode 100644 libs/entitlement/qa/entitlement-allowed/build.gradle create mode 100644 libs/entitlement/qa/entitlement-allowed/src/main/java/module-info.java create mode 100644 libs/entitlement/qa/entitlement-allowed/src/main/java/org/elasticsearch/entitlement/qa/EntitlementAllowedPlugin.java create mode 100644 libs/entitlement/qa/entitlement-allowed/src/main/plugin-metadata/entitlement-policy.yaml create mode 100644 libs/entitlement/qa/entitlement-denied-nonmodular/build.gradle create mode 100644 libs/entitlement/qa/entitlement-denied-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementDeniedNonModularPlugin.java create mode 100644 libs/entitlement/qa/entitlement-denied/build.gradle rename {qa/entitlements => libs/entitlement/qa/entitlement-denied}/src/main/java/module-info.java (53%) rename qa/entitlements/src/main/java/org/elasticsearch/test/entitlements/EntitlementsCheckPlugin.java => libs/entitlement/qa/entitlement-denied/src/main/java/org/elasticsearch/entitlement/qa/EntitlementDeniedPlugin.java (83%) create mode 100644 libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAllowedIT.java create mode 100644 libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsDeniedIT.java delete mode 100644 qa/entitlements/src/javaRestTest/java/org/elasticsearch/test/entitlements/EntitlementsIT.java delete mode 100644 qa/entitlements/src/main/java/org/elasticsearch/test/entitlements/RestEntitlementsCheckClassLoaderAction.java delete mode 100644 qa/entitlements/src/main/java/org/elasticsearch/test/entitlements/RestEntitlementsCheckSystemExitAction.java diff --git a/qa/entitlements/build.gradle b/libs/entitlement/qa/build.gradle similarity index 59% rename from qa/entitlements/build.gradle rename to libs/entitlement/qa/build.gradle index 9a5058a3b11ac..86bafc34f4d00 100644 --- a/qa/entitlements/build.gradle +++ b/libs/entitlement/qa/build.gradle @@ -7,23 +7,14 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -apply plugin: 'elasticsearch.base-internal-es-plugin' apply plugin: 'elasticsearch.internal-java-rest-test' // Necessary to use tests in Serverless apply plugin: 'elasticsearch.internal-test-artifact' -esplugin { - name 'entitlement-qa' - description 'A test module that triggers entitlement checks' - classname 'org.elasticsearch.test.entitlements.EntitlementsCheckPlugin' -} - dependencies { - clusterPlugins project(':qa:entitlements') + javaRestTestImplementation project(':libs:entitlement:qa:common') + clusterPlugins project(':libs:entitlement:qa:entitlement-allowed') + clusterPlugins project(':libs:entitlement:qa:entitlement-allowed-nonmodular') + clusterPlugins project(':libs:entitlement:qa:entitlement-denied') + clusterPlugins project(':libs:entitlement:qa:entitlement-denied-nonmodular') } - -tasks.named("javadoc").configure { - // There seems to be some problem generating javadoc on a QA project that has a module definition - enabled = false -} - diff --git a/libs/entitlement/qa/common/build.gradle b/libs/entitlement/qa/common/build.gradle new file mode 100644 index 0000000000000..df3bc66cba21b --- /dev/null +++ b/libs/entitlement/qa/common/build.gradle @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +apply plugin: 'elasticsearch.build' + +dependencies { + implementation project(':server') + implementation project(':libs:logging') +} diff --git a/libs/entitlement/qa/common/src/main/java/module-info.java b/libs/entitlement/qa/common/src/main/java/module-info.java new file mode 100644 index 0000000000000..2dd37e3174e08 --- /dev/null +++ b/libs/entitlement/qa/common/src/main/java/module-info.java @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module org.elasticsearch.entitlement.qa.common { + requires org.elasticsearch.server; + requires org.elasticsearch.base; + requires org.elasticsearch.logging; + + exports org.elasticsearch.entitlement.qa.common; +} diff --git a/libs/entitlement/qa/common/src/main/java/org/elasticsearch/entitlement/qa/common/RestEntitlementsCheckAction.java b/libs/entitlement/qa/common/src/main/java/org/elasticsearch/entitlement/qa/common/RestEntitlementsCheckAction.java new file mode 100644 index 0000000000000..e63fa4f3b726b --- /dev/null +++ b/libs/entitlement/qa/common/src/main/java/org/elasticsearch/entitlement/qa/common/RestEntitlementsCheckAction.java @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.entitlement.qa.common; + +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.common.Strings; +import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.Map.entry; +import static org.elasticsearch.rest.RestRequest.Method.GET; + +public class RestEntitlementsCheckAction extends BaseRestHandler { + private static final Logger logger = LogManager.getLogger(RestEntitlementsCheckAction.class); + private final String prefix; + + private record CheckAction(Runnable action, boolean isServerOnly) { + + static CheckAction serverOnly(Runnable action) { + return new CheckAction(action, true); + } + + static CheckAction serverAndPlugin(Runnable action) { + return new CheckAction(action, false); + } + } + + private static final Map checkActions = Map.ofEntries( + entry("system_exit", CheckAction.serverOnly(RestEntitlementsCheckAction::systemExit)), + entry("create_classloader", CheckAction.serverAndPlugin(RestEntitlementsCheckAction::createClassLoader)) + ); + + @SuppressForbidden(reason = "Specifically testing System.exit") + private static void systemExit() { + logger.info("Calling System.exit(123);"); + System.exit(123); + } + + private static void createClassLoader() { + logger.info("Calling new URLClassLoader"); + try (var classLoader = new URLClassLoader("test", new URL[0], RestEntitlementsCheckAction.class.getClassLoader())) { + logger.info("Created URLClassLoader [{}]", classLoader.getName()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public RestEntitlementsCheckAction(String prefix) { + this.prefix = prefix; + } + + public static Set getServerAndPluginsCheckActions() { + return checkActions.entrySet() + .stream() + .filter(kv -> kv.getValue().isServerOnly() == false) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + } + + public static Set getAllCheckActions() { + return checkActions.keySet(); + } + + @Override + public List routes() { + return List.of(new Route(GET, "/_entitlement/" + prefix + "/_check")); + } + + @Override + public String getName() { + return "check_" + prefix + "_action"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + logger.info("RestEntitlementsCheckAction rest handler [{}]", request.path()); + var actionName = request.param("action"); + if (Strings.isNullOrEmpty(actionName)) { + throw new IllegalArgumentException("Missing action parameter"); + } + var checkAction = checkActions.get(actionName); + if (checkAction == null) { + throw new IllegalArgumentException(Strings.format("Unknown action [%s]", actionName)); + } + + return channel -> { + checkAction.action().run(); + channel.sendResponse(new RestResponse(RestStatus.OK, Strings.format("Succesfully executed action [%s]", actionName))); + }; + } +} diff --git a/libs/entitlement/qa/entitlement-allowed-nonmodular/build.gradle b/libs/entitlement/qa/entitlement-allowed-nonmodular/build.gradle new file mode 100644 index 0000000000000..7b3015a5ab831 --- /dev/null +++ b/libs/entitlement/qa/entitlement-allowed-nonmodular/build.gradle @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +apply plugin: 'elasticsearch.base-internal-es-plugin' + +esplugin { + name 'entitlement-allowed-nonmodular' + description 'A non-modular test module that invokes entitlement checks that are supposed to be granted' + classname 'org.elasticsearch.entitlement.qa.nonmodular.EntitlementAllowedNonModularPlugin' +} + +dependencies { + implementation project(':libs:entitlement:qa:common') +} + +tasks.named("javadoc").configure { + enabled = false +} diff --git a/libs/entitlement/qa/entitlement-allowed-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementAllowedNonModularPlugin.java b/libs/entitlement/qa/entitlement-allowed-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementAllowedNonModularPlugin.java new file mode 100644 index 0000000000000..d65981c30f0be --- /dev/null +++ b/libs/entitlement/qa/entitlement-allowed-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementAllowedNonModularPlugin.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +package org.elasticsearch.entitlement.qa.nonmodular; + +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.entitlement.qa.common.RestEntitlementsCheckAction; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestHandler; + +import java.util.List; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public class EntitlementAllowedNonModularPlugin extends Plugin implements ActionPlugin { + + @Override + public List getRestHandlers( + final Settings settings, + NamedWriteableRegistry namedWriteableRegistry, + final RestController restController, + final ClusterSettings clusterSettings, + final IndexScopedSettings indexScopedSettings, + final SettingsFilter settingsFilter, + final IndexNameExpressionResolver indexNameExpressionResolver, + final Supplier nodesInCluster, + Predicate clusterSupportsFeature + ) { + return List.of(new RestEntitlementsCheckAction("allowed_nonmodular")); + } +} diff --git a/libs/entitlement/qa/entitlement-allowed-nonmodular/src/main/plugin-metadata/entitlement-policy.yaml b/libs/entitlement/qa/entitlement-allowed-nonmodular/src/main/plugin-metadata/entitlement-policy.yaml new file mode 100644 index 0000000000000..45d4e57f66521 --- /dev/null +++ b/libs/entitlement/qa/entitlement-allowed-nonmodular/src/main/plugin-metadata/entitlement-policy.yaml @@ -0,0 +1,2 @@ +ALL-UNNAMED: + - create_class_loader diff --git a/libs/entitlement/qa/entitlement-allowed/build.gradle b/libs/entitlement/qa/entitlement-allowed/build.gradle new file mode 100644 index 0000000000000..6090d658d2081 --- /dev/null +++ b/libs/entitlement/qa/entitlement-allowed/build.gradle @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +apply plugin: 'elasticsearch.base-internal-es-plugin' + +esplugin { + name 'entitlement-allowed' + description 'A test module that invokes entitlement checks that are supposed to be granted' + classname 'org.elasticsearch.entitlement.qa.EntitlementAllowedPlugin' +} + +dependencies { + implementation project(':libs:entitlement:qa:common') +} + +tasks.named("javadoc").configure { + enabled = false +} + diff --git a/libs/entitlement/qa/entitlement-allowed/src/main/java/module-info.java b/libs/entitlement/qa/entitlement-allowed/src/main/java/module-info.java new file mode 100644 index 0000000000000..a88611e6ac9a5 --- /dev/null +++ b/libs/entitlement/qa/entitlement-allowed/src/main/java/module-info.java @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +module org.elasticsearch.entitlement.qa.allowed { + requires org.elasticsearch.server; + requires org.elasticsearch.base; + requires org.elasticsearch.logging; + requires org.elasticsearch.entitlement.qa.common; +} diff --git a/libs/entitlement/qa/entitlement-allowed/src/main/java/org/elasticsearch/entitlement/qa/EntitlementAllowedPlugin.java b/libs/entitlement/qa/entitlement-allowed/src/main/java/org/elasticsearch/entitlement/qa/EntitlementAllowedPlugin.java new file mode 100644 index 0000000000000..d81e23e311be1 --- /dev/null +++ b/libs/entitlement/qa/entitlement-allowed/src/main/java/org/elasticsearch/entitlement/qa/EntitlementAllowedPlugin.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +package org.elasticsearch.entitlement.qa; + +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.entitlement.qa.common.RestEntitlementsCheckAction; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestHandler; + +import java.util.List; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public class EntitlementAllowedPlugin extends Plugin implements ActionPlugin { + + @Override + public List getRestHandlers( + final Settings settings, + NamedWriteableRegistry namedWriteableRegistry, + final RestController restController, + final ClusterSettings clusterSettings, + final IndexScopedSettings indexScopedSettings, + final SettingsFilter settingsFilter, + final IndexNameExpressionResolver indexNameExpressionResolver, + final Supplier nodesInCluster, + Predicate clusterSupportsFeature + ) { + return List.of(new RestEntitlementsCheckAction("allowed")); + } +} diff --git a/libs/entitlement/qa/entitlement-allowed/src/main/plugin-metadata/entitlement-policy.yaml b/libs/entitlement/qa/entitlement-allowed/src/main/plugin-metadata/entitlement-policy.yaml new file mode 100644 index 0000000000000..7b5e848f414b2 --- /dev/null +++ b/libs/entitlement/qa/entitlement-allowed/src/main/plugin-metadata/entitlement-policy.yaml @@ -0,0 +1,2 @@ +org.elasticsearch.entitlement.qa.common: + - create_class_loader diff --git a/libs/entitlement/qa/entitlement-denied-nonmodular/build.gradle b/libs/entitlement/qa/entitlement-denied-nonmodular/build.gradle new file mode 100644 index 0000000000000..bddd6c83c7cc4 --- /dev/null +++ b/libs/entitlement/qa/entitlement-denied-nonmodular/build.gradle @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +apply plugin: 'elasticsearch.base-internal-es-plugin' + +esplugin { + name 'entitlement-denied-nonmodular' + description 'A non-modular test module that invokes non-granted entitlement and triggers exceptions' + classname 'org.elasticsearch.entitlement.qa.nonmodular.EntitlementDeniedNonModularPlugin' +} + +dependencies { + implementation project(':libs:entitlement:qa:common') +} + +tasks.named("javadoc").configure { + enabled = false +} diff --git a/libs/entitlement/qa/entitlement-denied-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementDeniedNonModularPlugin.java b/libs/entitlement/qa/entitlement-denied-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementDeniedNonModularPlugin.java new file mode 100644 index 0000000000000..0f908d84260fb --- /dev/null +++ b/libs/entitlement/qa/entitlement-denied-nonmodular/src/main/java/org/elasticsearch/entitlement/qa/nonmodular/EntitlementDeniedNonModularPlugin.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +package org.elasticsearch.entitlement.qa.nonmodular; + +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.entitlement.qa.common.RestEntitlementsCheckAction; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestHandler; + +import java.util.List; +import java.util.function.Predicate; +import java.util.function.Supplier; + +public class EntitlementDeniedNonModularPlugin extends Plugin implements ActionPlugin { + + @Override + public List getRestHandlers( + final Settings settings, + NamedWriteableRegistry namedWriteableRegistry, + final RestController restController, + final ClusterSettings clusterSettings, + final IndexScopedSettings indexScopedSettings, + final SettingsFilter settingsFilter, + final IndexNameExpressionResolver indexNameExpressionResolver, + final Supplier nodesInCluster, + Predicate clusterSupportsFeature + ) { + return List.of(new RestEntitlementsCheckAction("denied_nonmodular")); + } +} diff --git a/libs/entitlement/qa/entitlement-denied/build.gradle b/libs/entitlement/qa/entitlement-denied/build.gradle new file mode 100644 index 0000000000000..cc269135c5bf5 --- /dev/null +++ b/libs/entitlement/qa/entitlement-denied/build.gradle @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +apply plugin: 'elasticsearch.base-internal-es-plugin' + +esplugin { + name 'entitlement-denied' + description 'A test module that invokes non-granted entitlement and triggers exceptions' + classname 'org.elasticsearch.entitlement.qa.EntitlementDeniedPlugin' +} + +dependencies { + implementation project(':libs:entitlement:qa:common') +} + +tasks.named("javadoc").configure { + enabled = false +} + diff --git a/qa/entitlements/src/main/java/module-info.java b/libs/entitlement/qa/entitlement-denied/src/main/java/module-info.java similarity index 53% rename from qa/entitlements/src/main/java/module-info.java rename to libs/entitlement/qa/entitlement-denied/src/main/java/module-info.java index cf33ff95d834c..3def472be7a45 100644 --- a/qa/entitlements/src/main/java/module-info.java +++ b/libs/entitlement/qa/entitlement-denied/src/main/java/module-info.java @@ -1,5 +1,6 @@ -module elasticsearch.qa.entitlements { +module org.elasticsearch.entitlement.qa.denied { requires org.elasticsearch.server; requires org.elasticsearch.base; requires org.apache.logging.log4j; + requires org.elasticsearch.entitlement.qa.common; } diff --git a/qa/entitlements/src/main/java/org/elasticsearch/test/entitlements/EntitlementsCheckPlugin.java b/libs/entitlement/qa/entitlement-denied/src/main/java/org/elasticsearch/entitlement/qa/EntitlementDeniedPlugin.java similarity index 83% rename from qa/entitlements/src/main/java/org/elasticsearch/test/entitlements/EntitlementsCheckPlugin.java rename to libs/entitlement/qa/entitlement-denied/src/main/java/org/elasticsearch/entitlement/qa/EntitlementDeniedPlugin.java index 94ad54c8c8ba8..0ed27e2e576e7 100644 --- a/qa/entitlements/src/main/java/org/elasticsearch/test/entitlements/EntitlementsCheckPlugin.java +++ b/libs/entitlement/qa/entitlement-denied/src/main/java/org/elasticsearch/entitlement/qa/EntitlementDeniedPlugin.java @@ -6,7 +6,7 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ -package org.elasticsearch.test.entitlements; +package org.elasticsearch.entitlement.qa; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNodes; @@ -15,7 +15,7 @@ import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; -import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.entitlement.qa.common.RestEntitlementsCheckAction; import org.elasticsearch.features.NodeFeature; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.Plugin; @@ -26,10 +26,9 @@ import java.util.function.Predicate; import java.util.function.Supplier; -public class EntitlementsCheckPlugin extends Plugin implements ActionPlugin { +public class EntitlementDeniedPlugin extends Plugin implements ActionPlugin { @Override - @SuppressForbidden(reason = "Specifically testing System.exit") public List getRestHandlers( final Settings settings, NamedWriteableRegistry namedWriteableRegistry, @@ -41,6 +40,6 @@ public List getRestHandlers( final Supplier nodesInCluster, Predicate clusterSupportsFeature ) { - return List.of(new RestEntitlementsCheckSystemExitAction(), new RestEntitlementsCheckClassLoaderAction()); + return List.of(new RestEntitlementsCheckAction("denied")); } } diff --git a/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAllowedIT.java b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAllowedIT.java new file mode 100644 index 0000000000000..5135fff44531a --- /dev/null +++ b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAllowedIT.java @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.entitlement.qa; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.entitlement.qa.common.RestEntitlementsCheckAction; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.junit.ClassRule; + +import java.io.IOException; +import java.util.stream.Stream; + +import static org.hamcrest.Matchers.equalTo; + +public class EntitlementsAllowedIT extends ESRestTestCase { + + @ClassRule + public static ElasticsearchCluster cluster = ElasticsearchCluster.local() + .plugin("entitlement-allowed") + .plugin("entitlement-allowed-nonmodular") + .systemProperty("es.entitlements.enabled", "true") + .setting("xpack.security.enabled", "false") + .build(); + + private final String pathPrefix; + private final String actionName; + + public EntitlementsAllowedIT(@Name("pathPrefix") String pathPrefix, @Name("actionName") String actionName) { + this.pathPrefix = pathPrefix; + this.actionName = actionName; + } + + @ParametersFactory + public static Iterable data() { + return Stream.of("allowed", "allowed_nonmodular") + .flatMap( + path -> RestEntitlementsCheckAction.getServerAndPluginsCheckActions().stream().map(action -> new Object[] { path, action }) + ) + .toList(); + } + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } + + public void testCheckActionWithPolicyPass() throws IOException { + logger.info("Executing Entitlement test [{}] for [{}]", pathPrefix, actionName); + var request = new Request("GET", "/_entitlement/" + pathPrefix + "/_check"); + request.addParameter("action", actionName); + Response result = client().performRequest(request); + assertThat(result.getStatusLine().getStatusCode(), equalTo(200)); + } +} diff --git a/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsDeniedIT.java b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsDeniedIT.java new file mode 100644 index 0000000000000..9f55a7c9e894d --- /dev/null +++ b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsDeniedIT.java @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.entitlement.qa; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import org.elasticsearch.client.Request; +import org.elasticsearch.entitlement.qa.common.RestEntitlementsCheckAction; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.junit.ClassRule; + +import java.io.IOException; +import java.util.stream.Stream; + +import static org.hamcrest.Matchers.containsString; + +public class EntitlementsDeniedIT extends ESRestTestCase { + + @ClassRule + public static ElasticsearchCluster cluster = ElasticsearchCluster.local() + .plugin("entitlement-denied") + .plugin("entitlement-denied-nonmodular") + .systemProperty("es.entitlements.enabled", "true") + .setting("xpack.security.enabled", "false") + .build(); + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } + + private final String pathPrefix; + private final String actionName; + + public EntitlementsDeniedIT(@Name("pathPrefix") String pathPrefix, @Name("actionName") String actionName) { + this.pathPrefix = pathPrefix; + this.actionName = actionName; + } + + @ParametersFactory + public static Iterable data() { + return Stream.of("denied", "denied_nonmodular") + .flatMap(path -> RestEntitlementsCheckAction.getAllCheckActions().stream().map(action -> new Object[] { path, action })) + .toList(); + } + + public void testCheckThrows() { + logger.info("Executing Entitlement test [{}] for [{}]", pathPrefix, actionName); + var exception = expectThrows(IOException.class, () -> { + var request = new Request("GET", "/_entitlement/" + pathPrefix + "/_check"); + request.addParameter("action", actionName); + client().performRequest(request); + }); + assertThat(exception.getMessage(), containsString("not_entitled_exception")); + } +} diff --git a/qa/entitlements/src/javaRestTest/java/org/elasticsearch/test/entitlements/EntitlementsIT.java b/qa/entitlements/src/javaRestTest/java/org/elasticsearch/test/entitlements/EntitlementsIT.java deleted file mode 100644 index f8bae10492ba8..0000000000000 --- a/qa/entitlements/src/javaRestTest/java/org/elasticsearch/test/entitlements/EntitlementsIT.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.test.entitlements; - -import org.elasticsearch.client.Request; -import org.elasticsearch.test.cluster.ElasticsearchCluster; -import org.elasticsearch.test.rest.ESRestTestCase; -import org.junit.ClassRule; - -import java.io.IOException; - -import static org.hamcrest.Matchers.containsString; - -public class EntitlementsIT extends ESRestTestCase { - - @ClassRule - public static ElasticsearchCluster cluster = ElasticsearchCluster.local() - .plugin("entitlement-qa") - .systemProperty("es.entitlements.enabled", "true") - .setting("xpack.security.enabled", "false") - .build(); - - @Override - protected String getTestRestCluster() { - return cluster.getHttpAddresses(); - } - - public void testCheckSystemExit() { - var exception = expectThrows( - IOException.class, - () -> { client().performRequest(new Request("GET", "/_entitlement/_check_system_exit")); } - ); - assertThat(exception.getMessage(), containsString("not_entitled_exception")); - } - - public void testCheckCreateURLClassLoader() { - var exception = expectThrows(IOException.class, () -> { - client().performRequest(new Request("GET", "/_entitlement/_check_create_url_classloader")); - }); - assertThat(exception.getMessage(), containsString("not_entitled_exception")); - } -} diff --git a/qa/entitlements/src/main/java/org/elasticsearch/test/entitlements/RestEntitlementsCheckClassLoaderAction.java b/qa/entitlements/src/main/java/org/elasticsearch/test/entitlements/RestEntitlementsCheckClassLoaderAction.java deleted file mode 100644 index 0b5ca28739ed0..0000000000000 --- a/qa/entitlements/src/main/java/org/elasticsearch/test/entitlements/RestEntitlementsCheckClassLoaderAction.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.test.entitlements; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.client.internal.node.NodeClient; -import org.elasticsearch.rest.BaseRestHandler; -import org.elasticsearch.rest.RestRequest; - -import java.net.URL; -import java.net.URLClassLoader; -import java.util.List; - -import static org.elasticsearch.rest.RestRequest.Method.GET; - -public class RestEntitlementsCheckClassLoaderAction extends BaseRestHandler { - - private static final Logger logger = LogManager.getLogger(RestEntitlementsCheckClassLoaderAction.class); - - RestEntitlementsCheckClassLoaderAction() {} - - @Override - public List routes() { - return List.of(new Route(GET, "/_entitlement/_check_create_url_classloader")); - } - - @Override - public String getName() { - return "check_classloader_action"; - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { - logger.info("RestEntitlementsCheckClassLoaderAction rest handler [{}]", request.path()); - if (request.path().equals("/_entitlement/_check_create_url_classloader")) { - return channel -> { - logger.info("Calling new URLClassLoader"); - try (var classLoader = new URLClassLoader("test", new URL[0], this.getClass().getClassLoader())) { - logger.info("Created URLClassLoader [{}]", classLoader.getName()); - } - }; - } - - throw new UnsupportedOperationException(); - } -} diff --git a/qa/entitlements/src/main/java/org/elasticsearch/test/entitlements/RestEntitlementsCheckSystemExitAction.java b/qa/entitlements/src/main/java/org/elasticsearch/test/entitlements/RestEntitlementsCheckSystemExitAction.java deleted file mode 100644 index 692c8728cbda0..0000000000000 --- a/qa/entitlements/src/main/java/org/elasticsearch/test/entitlements/RestEntitlementsCheckSystemExitAction.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.test.entitlements; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.client.internal.node.NodeClient; -import org.elasticsearch.rest.BaseRestHandler; -import org.elasticsearch.rest.RestRequest; - -import java.util.List; - -import static org.elasticsearch.rest.RestRequest.Method.GET; - -public class RestEntitlementsCheckSystemExitAction extends BaseRestHandler { - - private static final Logger logger = LogManager.getLogger(RestEntitlementsCheckSystemExitAction.class); - - RestEntitlementsCheckSystemExitAction() {} - - @Override - public List routes() { - return List.of(new Route(GET, "/_entitlement/_check_system_exit")); - } - - @Override - public String getName() { - return "check_system_exit_action"; - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { - logger.info("RestEntitlementsCheckSystemExitAction rest handler"); - return channel -> { - logger.info("Calling System.exit(123);"); - System.exit(123); - }; - } -} From 56e1ca52ea38671a320c9e9421fe3cb8fe5f15e3 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:06:24 +0100 Subject: [PATCH 10/77] [DOCS][101] Aggregations quickstart tutorial (#116251) --- .../quickstart/aggs-tutorial.asciidoc | 2184 +++++++++++++++++ docs/reference/quickstart/index.asciidoc | 2 + 2 files changed, 2186 insertions(+) create mode 100644 docs/reference/quickstart/aggs-tutorial.asciidoc diff --git a/docs/reference/quickstart/aggs-tutorial.asciidoc b/docs/reference/quickstart/aggs-tutorial.asciidoc new file mode 100644 index 0000000000000..0a8494c3eb75d --- /dev/null +++ b/docs/reference/quickstart/aggs-tutorial.asciidoc @@ -0,0 +1,2184 @@ +[[aggregations-tutorial]] +== Analyze eCommerce data with aggregations using Query DSL +++++ +Basics: Analyze eCommerce data with aggregations +++++ + +This hands-on tutorial shows you how to analyze eCommerce data using {es} <> with the `_search` API and Query DSL. + +You'll learn how to: + +* Calculate key business metrics such as average order value +* Analyze sales patterns over time +* Compare performance across product categories +* Track moving averages and cumulative totals + +[discrete] +[[aggregations-tutorial-requirements]] +=== Requirements + +You'll need: + +. A running instance of <>, either on {serverless-full} or together with {kib} on Elastic Cloud Hosted/Self Managed deployments. +** If you don't have a deployment, you can run the following command in your terminal to set up a <>: ++ +[source,sh] +---- +curl -fsSL https://elastic.co/start-local | sh +---- +// NOTCONSOLE +. The {kibana-ref}/get-started.html#gs-get-data-into-kibana[sample eCommerce data] loaded into {es}. To load sample data follow these steps in your UI: +* Open the *Integrations* pages by searching in the global search field. +* Search for `sample data` in the **Integrations** search field. +* Open the *Sample data* page. +* Select the *Other sample data sets* collapsible. +* Add the *Sample eCommerce orders* data set. +This will create and populate an index called `kibana_sample_data_ecommerce`. + +[discrete] +[[aggregations-tutorial-inspect-data]] +=== Inspect index structure + +Before we start analyzing the data, let's examine the structure of the documents in our sample eCommerce index. Run this command to see the field <>: + +[source,console] +---- +GET kibana_sample_data_ecommerce/_mapping +---- +// TEST[skip:Using Kibana sample data] + +The response shows the field mappings for the `kibana_sample_data_ecommerce` index. + +.Example response +[%collapsible] +============== +[source,console-response] +---- +{ + "kibana_sample_data_ecommerce": { + "mappings": { + "properties": { + "category": { + "type": "text", + "fields": { <1> + "keyword": { + "type": "keyword" + } + } + }, + "currency": { + "type": "keyword" + }, + "customer_birth_date": { + "type": "date" + }, + "customer_first_name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "customer_full_name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "customer_gender": { + "type": "keyword" + }, + "customer_id": { + "type": "keyword" + }, + "customer_last_name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "customer_phone": { + "type": "keyword" + }, + "day_of_week": { + "type": "keyword" + }, + "day_of_week_i": { + "type": "integer" + }, + "email": { + "type": "keyword" + }, + "event": { + "properties": { + "dataset": { + "type": "keyword" + } + } + }, + "geoip": { + "properties": { <2> + "city_name": { + "type": "keyword" + }, + "continent_name": { + "type": "keyword" + }, + "country_iso_code": { + "type": "keyword" + }, + "location": { + "type": "geo_point" <3> + }, + "region_name": { + "type": "keyword" + } + } + }, + "manufacturer": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + }, + "order_date": { + "type": "date" + }, + "order_id": { + "type": "keyword" + }, + "products": { + "properties": { <4> + "_id": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "base_price": { + "type": "half_float" + }, + "base_unit_price": { + "type": "half_float" + }, + "category": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + }, + "created_on": { + "type": "date" + }, + "discount_amount": { + "type": "half_float" + }, + "discount_percentage": { + "type": "half_float" + }, + "manufacturer": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + }, + "min_price": { + "type": "half_float" + }, + "price": { + "type": "half_float" + }, + "product_id": { + "type": "long" + }, + "product_name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + }, + "analyzer": "english" + }, + "quantity": { + "type": "integer" + }, + "sku": { + "type": "keyword" + }, + "tax_amount": { + "type": "half_float" + }, + "taxful_price": { + "type": "half_float" + }, + "taxless_price": { + "type": "half_float" + }, + "unit_discount_amount": { + "type": "half_float" + } + } + }, + "sku": { + "type": "keyword" + }, + "taxful_total_price": { + "type": "half_float" + }, + "taxless_total_price": { + "type": "half_float" + }, + "total_quantity": { + "type": "integer" + }, + "total_unique_products": { + "type": "integer" + }, + "type": { + "type": "keyword" + }, + "user": { + "type": "keyword" + } + } + } + } +} +---- +<1> `fields`: Multi-field mapping that allows both full text and exact matching +<2> `geoip.properties`: Object type field containing location-related properties +<3> `geoip.location`: Geographic coordinates stored as geo_point for location-based queries +<4> `products.properties`: Nested structure containing details about items in each order +============== + +The sample data includes the following <>: + +* <> and <> for text fields +** Most `text` fields have a `.keyword` subfield for exact matching using <> +* <> for date fields +* 3 <> types: +** `integer` for whole numbers +** `long` for large whole numbers +** `half_float` for floating-point numbers +* <> for geographic coordinates +* <> for nested structures such as `products`, `geoip`, `event` + +Now that we understand the structure of our sample data, let's start analyzing it. + +[discrete] +[[aggregations-tutorial-basic-metrics]] +=== Get key business metrics + +Let's start by calculating important metrics about orders and customers. + +[discrete] +[[aggregations-tutorial-order-value]] +==== Get average order size + +Calculate the average order value across all orders in the dataset using the <> aggregation. + +[source,console] +---- +GET kibana_sample_data_ecommerce/_search +{ + "size": 0, <1> + "aggs": { + "avg_order_value": { <2> + "avg": { <3> + "field": "taxful_total_price" + } + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> Set `size` to 0 to avoid returning matched documents in the response and return only the aggregation results +<2> A meaningful name that describes what this metric represents +<3> Configures an `avg` aggregation, which calculates a simple arithmetic mean + +.Example response +[%collapsible] +============== +[source,console-result] +---- +{ + "took": 0, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 4675, <1> + "relation": "eq" + }, + "max_score": null, + "hits": [] <2> + }, + "aggregations": { + "avg_order_value": { <3> + "value": 75.05542864304813 <4> + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> Total number of orders in the dataset +<2> `hits` is empty because we set `size` to 0 +<3> Results appear under the name we specified in the request +<4> The average order value is calculated dynamically from all the orders in the dataset +============== + +[discrete] +[[aggregations-tutorial-order-stats]] +==== Get multiple order statistics at once + +Calculate multiple statistics about orders in one request using the <> aggregation. + +[source,console] +---- +GET kibana_sample_data_ecommerce/_search +{ + "size": 0, + "aggs": { + "order_stats": { <1> + "stats": { <2> + "field": "taxful_total_price" + } + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> A descriptive name for this set of statistics +<2> `stats` returns count, min, max, avg, and sum at once + +.Example response +[%collapsible] +============== +[source,console-result] +---- +{ + "aggregations": { + "order_stats": { + "count": 4675, <1> + "min": 6.98828125, <2> + "max": 2250, <3> + "avg": 75.05542864304813, <4> + "sum": 350884.12890625 <5> + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> `"count"`: Total number of orders in the dataset +<2> `"min"`: Lowest individual order value in the dataset +<3> `"max"`: Highest individual order value in the dataset +<4> `"avg"`: Average value per order across all orders +<5> `"sum"`: Total revenue from all orders combined +============== + +[TIP] +==== +The <> is more efficient than running individual min, max, avg, and sum aggregations. +==== + +[discrete] +[[aggregations-tutorial-sales-patterns]] +=== Analyze sales patterns + +Let's group orders in different ways to understand sales patterns. + +[discrete] +[[aggregations-tutorial-category-breakdown]] +==== Break down sales by category + +Group orders by category to see which product categories are most popular, using the <> aggregation. + +[source,console] +---- +GET kibana_sample_data_ecommerce/_search +{ + "size": 0, + "aggs": { + "sales_by_category": { <1> + "terms": { <2> + "field": "category.keyword", <3> + "size": 5, <4> + "order": { "_count": "desc" } <5> + } + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> Name reflecting the business purpose of this breakdown +<2> `terms` aggregation groups documents by field values +<3> Use <> field for exact matching on text fields +<4> Limit to top 5 categories +<5> Order by number of orders (descending) + +.Example response +[%collapsible] +============== +[source,console-result] +---- +{ + "took": 4, + "timed_out": false, + "_shards": { + "total": 5, + "successful": 5, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 4675, + "relation": "eq" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "sales_by_category": { + "doc_count_error_upper_bound": 0, <1> + "sum_other_doc_count": 572, <2> + "buckets": [ <3> + { + "key": "Men's Clothing", <4> + "doc_count": 2024 <5> + }, + { + "key": "Women's Clothing", + "doc_count": 1903 + }, + { + "key": "Women's Shoes", + "doc_count": 1136 + }, + { + "key": "Men's Shoes", + "doc_count": 944 + }, + { + "key": "Women's Accessories", + "doc_count": 830 + } + ] + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> Due to Elasticsearch's distributed architecture, when <> run across multiple shards, the doc counts may have a small margin of error. This value indicates the maximum possible error in the counts. +<2> Count of documents in categories beyond the requested size. +<3> Array of category buckets, ordered by count. +<4> Category name. +<5> Number of orders in this category. +============== + +[discrete] +[[aggregations-tutorial-daily-sales]] +==== Track daily sales patterns + +Group orders by day to track daily sales patterns using the <> aggregation. + +[source,console] +---- +GET kibana_sample_data_ecommerce/_search +{ + "size": 0, + "aggs": { + "daily_orders": { <1> + "date_histogram": { <2> + "field": "order_date", + "calendar_interval": "day", <3> + "format": "yyyy-MM-dd", <4> + "min_doc_count": 0 <5> + } + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> Descriptive name for the time-series aggregation results. +<2> The `date_histogram` aggregration groups documents into time-based buckets, similar to terms aggregation but for dates. +<3> Uses <> to handle months with different lengths. `"day"` ensures consistent daily grouping regardless of timezone. +<4> Formats dates in response using <> (e.g. "yyyy-MM-dd"). Refer to <> for additional options. +<5> When `min_doc_count` is 0, returns buckets for days with no orders, useful for continuous time series visualization. + +.Example response +[%collapsible] +============== +[source,console-result] +---- +{ + "took": 2, + "timed_out": false, + "_shards": { + "total": 5, + "successful": 5, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 4675, + "relation": "eq" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "daily_orders": { <1> + "buckets": [ <2> + { + "key_as_string": "2024-11-28", <3> + "key": 1732752000000, <4> + "doc_count": 146 <5> + }, + { + "key_as_string": "2024-11-29", + "key": 1732838400000, + "doc_count": 153 + }, + { + "key_as_string": "2024-11-30", + "key": 1732924800000, + "doc_count": 143 + }, + { + "key_as_string": "2024-12-01", + "key": 1733011200000, + "doc_count": 140 + }, + { + "key_as_string": "2024-12-02", + "key": 1733097600000, + "doc_count": 139 + }, + { + "key_as_string": "2024-12-03", + "key": 1733184000000, + "doc_count": 157 + }, + { + "key_as_string": "2024-12-04", + "key": 1733270400000, + "doc_count": 145 + }, + { + "key_as_string": "2024-12-05", + "key": 1733356800000, + "doc_count": 152 + }, + { + "key_as_string": "2024-12-06", + "key": 1733443200000, + "doc_count": 163 + }, + { + "key_as_string": "2024-12-07", + "key": 1733529600000, + "doc_count": 141 + }, + { + "key_as_string": "2024-12-08", + "key": 1733616000000, + "doc_count": 151 + }, + { + "key_as_string": "2024-12-09", + "key": 1733702400000, + "doc_count": 143 + }, + { + "key_as_string": "2024-12-10", + "key": 1733788800000, + "doc_count": 143 + }, + { + "key_as_string": "2024-12-11", + "key": 1733875200000, + "doc_count": 142 + }, + { + "key_as_string": "2024-12-12", + "key": 1733961600000, + "doc_count": 161 + }, + { + "key_as_string": "2024-12-13", + "key": 1734048000000, + "doc_count": 144 + }, + { + "key_as_string": "2024-12-14", + "key": 1734134400000, + "doc_count": 157 + }, + { + "key_as_string": "2024-12-15", + "key": 1734220800000, + "doc_count": 158 + }, + { + "key_as_string": "2024-12-16", + "key": 1734307200000, + "doc_count": 144 + }, + { + "key_as_string": "2024-12-17", + "key": 1734393600000, + "doc_count": 151 + }, + { + "key_as_string": "2024-12-18", + "key": 1734480000000, + "doc_count": 145 + }, + { + "key_as_string": "2024-12-19", + "key": 1734566400000, + "doc_count": 157 + }, + { + "key_as_string": "2024-12-20", + "key": 1734652800000, + "doc_count": 158 + }, + { + "key_as_string": "2024-12-21", + "key": 1734739200000, + "doc_count": 153 + }, + { + "key_as_string": "2024-12-22", + "key": 1734825600000, + "doc_count": 165 + }, + { + "key_as_string": "2024-12-23", + "key": 1734912000000, + "doc_count": 153 + }, + { + "key_as_string": "2024-12-24", + "key": 1734998400000, + "doc_count": 158 + }, + { + "key_as_string": "2024-12-25", + "key": 1735084800000, + "doc_count": 160 + }, + { + "key_as_string": "2024-12-26", + "key": 1735171200000, + "doc_count": 159 + }, + { + "key_as_string": "2024-12-27", + "key": 1735257600000, + "doc_count": 152 + }, + { + "key_as_string": "2024-12-28", + "key": 1735344000000, + "doc_count": 142 + } + ] + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> Results of our named aggregation "daily_orders" +<2> Time-based buckets from date_histogram aggregation +<3> `key_as_string` is the human-readable date for this bucket +<4> `key` is the same date represented as the Unix timestamp for this bucket +<5> `doc_count` counts the number of documents that fall into this time bucket +============== + +[discrete] +[[aggregations-tutorial-combined-analysis]] +=== Combine metrics with groupings + +Now let's calculate <> within each group to get deeper insights. + +[discrete] +[[aggregations-tutorial-category-metrics]] +==== Compare category performance + +Calculate metrics within each category to compare performance across categories. + +[source,console] +---- +GET kibana_sample_data_ecommerce/_search +{ + "size": 0, + "aggs": { + "categories": { + "terms": { + "field": "category.keyword", + "size": 5, + "order": { "total_revenue": "desc" } <1> + }, + "aggs": { <2> + "total_revenue": { <3> + "sum": { + "field": "taxful_total_price" + } + }, + "avg_order_value": { <4> + "avg": { + "field": "taxful_total_price" + } + }, + "total_items": { <5> + "sum": { + "field": "total_quantity" + } + } + } + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> Order categories by their total revenue instead of count +<2> Define metrics to calculate within each category +<3> Total revenue for the category +<4> Average order value in the category +<5> Total number of items sold + +.Example response +[%collapsible] +============== +[source,console-result] +---- +{ + "aggregations": { + "categories": { + "buckets": [ + { + "key": "Men's Clothing", <1> + "doc_count": 2179, <2> + "total_revenue": { <3> + "value": 156729.453125 + }, + "avg_order_value": { <4> + "value": 71.92726898715927 + }, + "total_items": { <5> + "value": 8716 + } + }, + { + "key": "Women's Clothing", + "doc_count": 2262, + ... + } + ] + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> Category name +<2> Number of orders +<3> Total revenue for this category +<4> Average order value for this category +<5> Total quantity of items sold +============== + +[discrete] +[[aggregations-tutorial-daily-metrics]] +==== Analyze daily sales performance + +Let's combine metrics to track daily trends: daily revenue, unique customers, and average basket size. + +[source,console] +---- +GET kibana_sample_data_ecommerce/_search +{ + "size": 0, + "aggs": { + "daily_sales": { + "date_histogram": { + "field": "order_date", + "calendar_interval": "day", + "format": "yyyy-MM-dd" + }, + "aggs": { + "revenue": { <1> + "sum": { + "field": "taxful_total_price" + } + }, + "unique_customers": { <2> + "cardinality": { + "field": "customer_id" + } + }, + "avg_basket_size": { <3> + "avg": { + "field": "total_quantity" + } + } + } + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> Daily revenue +<2> Uses the <> aggregation to count unique customers per day +<3> Average number of items per order + +.Example response +[%collapsible] +============== +[source,console-result] +---- +{ + "took": 119, + "timed_out": false, + "_shards": { + "total": 5, + "successful": 5, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 4675, + "relation": "eq" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "daily_sales": { + "buckets": [ + { + "key_as_string": "2024-11-14", + "key": 1731542400000, + "doc_count": 146, + "unique_customers": { <1> + "value": 42 + }, + "revenue": { <2> + "value": 10578.53125 + }, + "avg_basket_size": { <3> + "value": 2.1780821917808217 + } + }, + { + "key_as_string": "2024-11-15", + "key": 1731628800000, + "doc_count": 153, + "unique_customers": { + "value": 44 + }, + "revenue": { + "value": 10448 + }, + "avg_basket_size": { + "value": 2.183006535947712 + } + }, + { + "key_as_string": "2024-11-16", + "key": 1731715200000, + "doc_count": 143, + "unique_customers": { + "value": 45 + }, + "revenue": { + "value": 10283.484375 + }, + "avg_basket_size": { + "value": 2.111888111888112 + } + }, + { + "key_as_string": "2024-11-17", + "key": 1731801600000, + "doc_count": 140, + "unique_customers": { + "value": 42 + }, + "revenue": { + "value": 10145.5234375 + }, + "avg_basket_size": { + "value": 2.142857142857143 + } + }, + { + "key_as_string": "2024-11-18", + "key": 1731888000000, + "doc_count": 139, + "unique_customers": { + "value": 42 + }, + "revenue": { + "value": 12012.609375 + }, + "avg_basket_size": { + "value": 2.158273381294964 + } + }, + { + "key_as_string": "2024-11-19", + "key": 1731974400000, + "doc_count": 157, + "unique_customers": { + "value": 43 + }, + "revenue": { + "value": 11009.45703125 + }, + "avg_basket_size": { + "value": 2.0955414012738856 + } + }, + { + "key_as_string": "2024-11-20", + "key": 1732060800000, + "doc_count": 145, + "unique_customers": { + "value": 44 + }, + "revenue": { + "value": 10720.59375 + }, + "avg_basket_size": { + "value": 2.179310344827586 + } + }, + { + "key_as_string": "2024-11-21", + "key": 1732147200000, + "doc_count": 152, + "unique_customers": { + "value": 43 + }, + "revenue": { + "value": 11185.3671875 + }, + "avg_basket_size": { + "value": 2.1710526315789473 + } + }, + { + "key_as_string": "2024-11-22", + "key": 1732233600000, + "doc_count": 163, + "unique_customers": { + "value": 44 + }, + "revenue": { + "value": 13560.140625 + }, + "avg_basket_size": { + "value": 2.2576687116564416 + } + }, + { + "key_as_string": "2024-11-23", + "key": 1732320000000, + "doc_count": 141, + "unique_customers": { + "value": 45 + }, + "revenue": { + "value": 9884.78125 + }, + "avg_basket_size": { + "value": 2.099290780141844 + } + }, + { + "key_as_string": "2024-11-24", + "key": 1732406400000, + "doc_count": 151, + "unique_customers": { + "value": 44 + }, + "revenue": { + "value": 11075.65625 + }, + "avg_basket_size": { + "value": 2.0927152317880795 + } + }, + { + "key_as_string": "2024-11-25", + "key": 1732492800000, + "doc_count": 143, + "unique_customers": { + "value": 41 + }, + "revenue": { + "value": 10323.8515625 + }, + "avg_basket_size": { + "value": 2.167832167832168 + } + }, + { + "key_as_string": "2024-11-26", + "key": 1732579200000, + "doc_count": 143, + "unique_customers": { + "value": 44 + }, + "revenue": { + "value": 10369.546875 + }, + "avg_basket_size": { + "value": 2.167832167832168 + } + }, + { + "key_as_string": "2024-11-27", + "key": 1732665600000, + "doc_count": 142, + "unique_customers": { + "value": 46 + }, + "revenue": { + "value": 11711.890625 + }, + "avg_basket_size": { + "value": 2.1971830985915495 + } + }, + { + "key_as_string": "2024-11-28", + "key": 1732752000000, + "doc_count": 161, + "unique_customers": { + "value": 43 + }, + "revenue": { + "value": 12612.6640625 + }, + "avg_basket_size": { + "value": 2.1180124223602483 + } + }, + { + "key_as_string": "2024-11-29", + "key": 1732838400000, + "doc_count": 144, + "unique_customers": { + "value": 42 + }, + "revenue": { + "value": 10176.87890625 + }, + "avg_basket_size": { + "value": 2.0347222222222223 + } + }, + { + "key_as_string": "2024-11-30", + "key": 1732924800000, + "doc_count": 157, + "unique_customers": { + "value": 43 + }, + "revenue": { + "value": 11480.33203125 + }, + "avg_basket_size": { + "value": 2.159235668789809 + } + }, + { + "key_as_string": "2024-12-01", + "key": 1733011200000, + "doc_count": 158, + "unique_customers": { + "value": 42 + }, + "revenue": { + "value": 11533.265625 + }, + "avg_basket_size": { + "value": 2.0822784810126582 + } + }, + { + "key_as_string": "2024-12-02", + "key": 1733097600000, + "doc_count": 144, + "unique_customers": { + "value": 43 + }, + "revenue": { + "value": 10499.8125 + }, + "avg_basket_size": { + "value": 2.201388888888889 + } + }, + { + "key_as_string": "2024-12-03", + "key": 1733184000000, + "doc_count": 151, + "unique_customers": { + "value": 40 + }, + "revenue": { + "value": 12111.6875 + }, + "avg_basket_size": { + "value": 2.172185430463576 + } + }, + { + "key_as_string": "2024-12-04", + "key": 1733270400000, + "doc_count": 145, + "unique_customers": { + "value": 40 + }, + "revenue": { + "value": 10530.765625 + }, + "avg_basket_size": { + "value": 2.0965517241379312 + } + }, + { + "key_as_string": "2024-12-05", + "key": 1733356800000, + "doc_count": 157, + "unique_customers": { + "value": 43 + }, + "revenue": { + "value": 11872.5625 + }, + "avg_basket_size": { + "value": 2.1464968152866244 + } + }, + { + "key_as_string": "2024-12-06", + "key": 1733443200000, + "doc_count": 158, + "unique_customers": { + "value": 42 + }, + "revenue": { + "value": 12109.453125 + }, + "avg_basket_size": { + "value": 2.151898734177215 + } + }, + { + "key_as_string": "2024-12-07", + "key": 1733529600000, + "doc_count": 153, + "unique_customers": { + "value": 42 + }, + "revenue": { + "value": 11057.40625 + }, + "avg_basket_size": { + "value": 2.111111111111111 + } + }, + { + "key_as_string": "2024-12-08", + "key": 1733616000000, + "doc_count": 165, + "unique_customers": { + "value": 42 + }, + "revenue": { + "value": 13095.609375 + }, + "avg_basket_size": { + "value": 2.1818181818181817 + } + }, + { + "key_as_string": "2024-12-09", + "key": 1733702400000, + "doc_count": 153, + "unique_customers": { + "value": 41 + }, + "revenue": { + "value": 12574.015625 + }, + "avg_basket_size": { + "value": 2.2287581699346406 + } + }, + { + "key_as_string": "2024-12-10", + "key": 1733788800000, + "doc_count": 158, + "unique_customers": { + "value": 42 + }, + "revenue": { + "value": 11188.1875 + }, + "avg_basket_size": { + "value": 2.151898734177215 + } + }, + { + "key_as_string": "2024-12-11", + "key": 1733875200000, + "doc_count": 160, + "unique_customers": { + "value": 42 + }, + "revenue": { + "value": 12117.65625 + }, + "avg_basket_size": { + "value": 2.20625 + } + }, + { + "key_as_string": "2024-12-12", + "key": 1733961600000, + "doc_count": 159, + "unique_customers": { + "value": 45 + }, + "revenue": { + "value": 11558.25 + }, + "avg_basket_size": { + "value": 2.1823899371069184 + } + }, + { + "key_as_string": "2024-12-13", + "key": 1734048000000, + "doc_count": 152, + "unique_customers": { + "value": 45 + }, + "revenue": { + "value": 11921.1171875 + }, + "avg_basket_size": { + "value": 2.289473684210526 + } + }, + { + "key_as_string": "2024-12-14", + "key": 1734134400000, + "doc_count": 142, + "unique_customers": { + "value": 45 + }, + "revenue": { + "value": 11135.03125 + }, + "avg_basket_size": { + "value": 2.183098591549296 + } + } + ] + } + } +} +---- +// TEST[skip:Using Kibana sample data] +============== + +[discrete] +[[aggregations-tutorial-trends]] +=== Track trends and patterns + +You can use <> on the results of other aggregations. +Let's analyze how metrics change over time. + +[discrete] +[[aggregations-tutorial-moving-average]] +==== Smooth out daily fluctuations + +Moving averages help identify trends by reducing day-to-day noise in the data. +Let's observe sales trends more clearly by smoothing daily revenue variations, using the <> aggregation. + +[source,console] +---- +GET kibana_sample_data_ecommerce/_search +{ + "size": 0, + "aggs": { + "daily_sales": { + "date_histogram": { + "field": "order_date", + "calendar_interval": "day" + }, + "aggs": { + "daily_revenue": { <1> + "sum": { + "field": "taxful_total_price" + } + }, + "smoothed_revenue": { <2> + "moving_fn": { <3> + "buckets_path": "daily_revenue", <4> + "window": 3, <5> + "script": "MovingFunctions.unweightedAvg(values)" <6> + } + } + } + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> Calculate daily revenue first. +<2> Create a smoothed version of the daily revenue. +<3> Use `moving_fn` for moving window calculations. +<4> Reference the revenue from our date histogram. +<5> Use a 3-day window — use different window sizes to see trends at different time scales. +<6> Use the built-in unweighted average function in the `moving_fn` aggregation. + +.Example response +[%collapsible] +============== +[source,console-result] +---- +{ + "took": 13, + "timed_out": false, + "_shards": { + "total": 5, + "successful": 5, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 4675, + "relation": "eq" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "daily_sales": { + "buckets": [ + { + "key_as_string": "2024-11-14T00:00:00.000Z", <1> + "key": 1731542400000, + "doc_count": 146, <2> + "daily_revenue": { <3> + "value": 10578.53125 + }, + "smoothed_revenue": { <4> + "value": null + } + }, + { + "key_as_string": "2024-11-15T00:00:00.000Z", + "key": 1731628800000, + "doc_count": 153, + "daily_revenue": { + "value": 10448 + }, + "smoothed_revenue": { <5> + "value": 10578.53125 + } + }, + { + "key_as_string": "2024-11-16T00:00:00.000Z", + "key": 1731715200000, + "doc_count": 143, + "daily_revenue": { + "value": 10283.484375 + }, + "smoothed_revenue": { + "value": 10513.265625 + } + }, + { + "key_as_string": "2024-11-17T00:00:00.000Z", + "key": 1731801600000, + "doc_count": 140, + "daily_revenue": { + "value": 10145.5234375 + }, + "smoothed_revenue": { + "value": 10436.671875 + } + }, + { + "key_as_string": "2024-11-18T00:00:00.000Z", + "key": 1731888000000, + "doc_count": 139, + "daily_revenue": { + "value": 12012.609375 + }, + "smoothed_revenue": { + "value": 10292.3359375 + } + }, + { + "key_as_string": "2024-11-19T00:00:00.000Z", + "key": 1731974400000, + "doc_count": 157, + "daily_revenue": { + "value": 11009.45703125 + }, + "smoothed_revenue": { + "value": 10813.872395833334 + } + }, + { + "key_as_string": "2024-11-20T00:00:00.000Z", + "key": 1732060800000, + "doc_count": 145, + "daily_revenue": { + "value": 10720.59375 + }, + "smoothed_revenue": { + "value": 11055.86328125 + } + }, + { + "key_as_string": "2024-11-21T00:00:00.000Z", + "key": 1732147200000, + "doc_count": 152, + "daily_revenue": { + "value": 11185.3671875 + }, + "smoothed_revenue": { + "value": 11247.553385416666 + } + }, + { + "key_as_string": "2024-11-22T00:00:00.000Z", + "key": 1732233600000, + "doc_count": 163, + "daily_revenue": { + "value": 13560.140625 + }, + "smoothed_revenue": { + "value": 10971.805989583334 + } + }, + { + "key_as_string": "2024-11-23T00:00:00.000Z", + "key": 1732320000000, + "doc_count": 141, + "daily_revenue": { + "value": 9884.78125 + }, + "smoothed_revenue": { + "value": 11822.033854166666 + } + }, + { + "key_as_string": "2024-11-24T00:00:00.000Z", + "key": 1732406400000, + "doc_count": 151, + "daily_revenue": { + "value": 11075.65625 + }, + "smoothed_revenue": { + "value": 11543.4296875 + } + }, + { + "key_as_string": "2024-11-25T00:00:00.000Z", + "key": 1732492800000, + "doc_count": 143, + "daily_revenue": { + "value": 10323.8515625 + }, + "smoothed_revenue": { + "value": 11506.859375 + } + }, + { + "key_as_string": "2024-11-26T00:00:00.000Z", + "key": 1732579200000, + "doc_count": 143, + "daily_revenue": { + "value": 10369.546875 + }, + "smoothed_revenue": { + "value": 10428.096354166666 + } + }, + { + "key_as_string": "2024-11-27T00:00:00.000Z", + "key": 1732665600000, + "doc_count": 142, + "daily_revenue": { + "value": 11711.890625 + }, + "smoothed_revenue": { + "value": 10589.684895833334 + } + }, + { + "key_as_string": "2024-11-28T00:00:00.000Z", + "key": 1732752000000, + "doc_count": 161, + "daily_revenue": { + "value": 12612.6640625 + }, + "smoothed_revenue": { + "value": 10801.763020833334 + } + }, + { + "key_as_string": "2024-11-29T00:00:00.000Z", + "key": 1732838400000, + "doc_count": 144, + "daily_revenue": { + "value": 10176.87890625 + }, + "smoothed_revenue": { + "value": 11564.700520833334 + } + }, + { + "key_as_string": "2024-11-30T00:00:00.000Z", + "key": 1732924800000, + "doc_count": 157, + "daily_revenue": { + "value": 11480.33203125 + }, + "smoothed_revenue": { + "value": 11500.477864583334 + } + }, + { + "key_as_string": "2024-12-01T00:00:00.000Z", + "key": 1733011200000, + "doc_count": 158, + "daily_revenue": { + "value": 11533.265625 + }, + "smoothed_revenue": { + "value": 11423.291666666666 + } + }, + { + "key_as_string": "2024-12-02T00:00:00.000Z", + "key": 1733097600000, + "doc_count": 144, + "daily_revenue": { + "value": 10499.8125 + }, + "smoothed_revenue": { + "value": 11063.4921875 + } + }, + { + "key_as_string": "2024-12-03T00:00:00.000Z", + "key": 1733184000000, + "doc_count": 151, + "daily_revenue": { + "value": 12111.6875 + }, + "smoothed_revenue": { + "value": 11171.13671875 + } + }, + { + "key_as_string": "2024-12-04T00:00:00.000Z", + "key": 1733270400000, + "doc_count": 145, + "daily_revenue": { + "value": 10530.765625 + }, + "smoothed_revenue": { + "value": 11381.588541666666 + } + }, + { + "key_as_string": "2024-12-05T00:00:00.000Z", + "key": 1733356800000, + "doc_count": 157, + "daily_revenue": { + "value": 11872.5625 + }, + "smoothed_revenue": { + "value": 11047.421875 + } + }, + { + "key_as_string": "2024-12-06T00:00:00.000Z", + "key": 1733443200000, + "doc_count": 158, + "daily_revenue": { + "value": 12109.453125 + }, + "smoothed_revenue": { + "value": 11505.005208333334 + } + }, + { + "key_as_string": "2024-12-07T00:00:00.000Z", + "key": 1733529600000, + "doc_count": 153, + "daily_revenue": { + "value": 11057.40625 + }, + "smoothed_revenue": { + "value": 11504.260416666666 + } + }, + { + "key_as_string": "2024-12-08T00:00:00.000Z", + "key": 1733616000000, + "doc_count": 165, + "daily_revenue": { + "value": 13095.609375 + }, + "smoothed_revenue": { + "value": 11679.807291666666 + } + }, + { + "key_as_string": "2024-12-09T00:00:00.000Z", + "key": 1733702400000, + "doc_count": 153, + "daily_revenue": { + "value": 12574.015625 + }, + "smoothed_revenue": { + "value": 12087.489583333334 + } + }, + { + "key_as_string": "2024-12-10T00:00:00.000Z", + "key": 1733788800000, + "doc_count": 158, + "daily_revenue": { + "value": 11188.1875 + }, + "smoothed_revenue": { + "value": 12242.34375 + } + }, + { + "key_as_string": "2024-12-11T00:00:00.000Z", + "key": 1733875200000, + "doc_count": 160, + "daily_revenue": { + "value": 12117.65625 + }, + "smoothed_revenue": { + "value": 12285.9375 + } + }, + { + "key_as_string": "2024-12-12T00:00:00.000Z", + "key": 1733961600000, + "doc_count": 159, + "daily_revenue": { + "value": 11558.25 + }, + "smoothed_revenue": { + "value": 11959.953125 + } + }, + { + "key_as_string": "2024-12-13T00:00:00.000Z", + "key": 1734048000000, + "doc_count": 152, + "daily_revenue": { + "value": 11921.1171875 + }, + "smoothed_revenue": { + "value": 11621.364583333334 + } + }, + { + "key_as_string": "2024-12-14T00:00:00.000Z", + "key": 1734134400000, + "doc_count": 142, + "daily_revenue": { + "value": 11135.03125 + }, + "smoothed_revenue": { + "value": 11865.674479166666 + } + } + ] + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> Date of the bucket is in default ISO format because we didn't specify a format +<2> Number of orders for this day +<3> Raw daily revenue before smoothing +<4> First day has no smoothed value as it needs previous days for the calculation +<5> Moving average starts from second day, using a 3-day window +============== + +[TIP] +==== +Notice how the smoothed values lag behind the actual values - this is because they need previous days' data to calculate. The first day will always be null when using moving averages. +==== + +[discrete] +[[aggregations-tutorial-cumulative]] +==== Track running totals + +Track running totals over time using the <> aggregation. + +[source,console] +---- +GET kibana_sample_data_ecommerce/_search +{ + "size": 0, + "aggs": { + "daily_sales": { + "date_histogram": { + "field": "order_date", + "calendar_interval": "day" + }, + "aggs": { + "revenue": { + "sum": { + "field": "taxful_total_price" + } + }, + "cumulative_revenue": { <1> + "cumulative_sum": { <2> + "buckets_path": "revenue" <3> + } + } + } + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> Name for our running total +<2> `cumulative_sum` adds up values across buckets +<3> Reference the revenue we want to accumulate + +.Example response +[%collapsible] +============== +[source,console-result] +---- +{ + "took": 4, + "timed_out": false, + "_shards": { + "total": 5, + "successful": 5, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 4675, + "relation": "eq" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "daily_sales": { <1> + "buckets": [ <2> + { + "key_as_string": "2024-11-14T00:00:00.000Z", <3> + "key": 1731542400000, + "doc_count": 146, + "revenue": { <4> + "value": 10578.53125 + }, + "cumulative_revenue": { <5> + "value": 10578.53125 + } + }, + { + "key_as_string": "2024-11-15T00:00:00.000Z", + "key": 1731628800000, + "doc_count": 153, + "revenue": { + "value": 10448 + }, + "cumulative_revenue": { + "value": 21026.53125 + } + }, + { + "key_as_string": "2024-11-16T00:00:00.000Z", + "key": 1731715200000, + "doc_count": 143, + "revenue": { + "value": 10283.484375 + }, + "cumulative_revenue": { + "value": 31310.015625 + } + }, + { + "key_as_string": "2024-11-17T00:00:00.000Z", + "key": 1731801600000, + "doc_count": 140, + "revenue": { + "value": 10145.5234375 + }, + "cumulative_revenue": { + "value": 41455.5390625 + } + }, + { + "key_as_string": "2024-11-18T00:00:00.000Z", + "key": 1731888000000, + "doc_count": 139, + "revenue": { + "value": 12012.609375 + }, + "cumulative_revenue": { + "value": 53468.1484375 + } + }, + { + "key_as_string": "2024-11-19T00:00:00.000Z", + "key": 1731974400000, + "doc_count": 157, + "revenue": { + "value": 11009.45703125 + }, + "cumulative_revenue": { + "value": 64477.60546875 + } + }, + { + "key_as_string": "2024-11-20T00:00:00.000Z", + "key": 1732060800000, + "doc_count": 145, + "revenue": { + "value": 10720.59375 + }, + "cumulative_revenue": { + "value": 75198.19921875 + } + }, + { + "key_as_string": "2024-11-21T00:00:00.000Z", + "key": 1732147200000, + "doc_count": 152, + "revenue": { + "value": 11185.3671875 + }, + "cumulative_revenue": { + "value": 86383.56640625 + } + }, + { + "key_as_string": "2024-11-22T00:00:00.000Z", + "key": 1732233600000, + "doc_count": 163, + "revenue": { + "value": 13560.140625 + }, + "cumulative_revenue": { + "value": 99943.70703125 + } + }, + { + "key_as_string": "2024-11-23T00:00:00.000Z", + "key": 1732320000000, + "doc_count": 141, + "revenue": { + "value": 9884.78125 + }, + "cumulative_revenue": { + "value": 109828.48828125 + } + }, + { + "key_as_string": "2024-11-24T00:00:00.000Z", + "key": 1732406400000, + "doc_count": 151, + "revenue": { + "value": 11075.65625 + }, + "cumulative_revenue": { + "value": 120904.14453125 + } + }, + { + "key_as_string": "2024-11-25T00:00:00.000Z", + "key": 1732492800000, + "doc_count": 143, + "revenue": { + "value": 10323.8515625 + }, + "cumulative_revenue": { + "value": 131227.99609375 + } + }, + { + "key_as_string": "2024-11-26T00:00:00.000Z", + "key": 1732579200000, + "doc_count": 143, + "revenue": { + "value": 10369.546875 + }, + "cumulative_revenue": { + "value": 141597.54296875 + } + }, + { + "key_as_string": "2024-11-27T00:00:00.000Z", + "key": 1732665600000, + "doc_count": 142, + "revenue": { + "value": 11711.890625 + }, + "cumulative_revenue": { + "value": 153309.43359375 + } + }, + { + "key_as_string": "2024-11-28T00:00:00.000Z", + "key": 1732752000000, + "doc_count": 161, + "revenue": { + "value": 12612.6640625 + }, + "cumulative_revenue": { + "value": 165922.09765625 + } + }, + { + "key_as_string": "2024-11-29T00:00:00.000Z", + "key": 1732838400000, + "doc_count": 144, + "revenue": { + "value": 10176.87890625 + }, + "cumulative_revenue": { + "value": 176098.9765625 + } + }, + { + "key_as_string": "2024-11-30T00:00:00.000Z", + "key": 1732924800000, + "doc_count": 157, + "revenue": { + "value": 11480.33203125 + }, + "cumulative_revenue": { + "value": 187579.30859375 + } + }, + { + "key_as_string": "2024-12-01T00:00:00.000Z", + "key": 1733011200000, + "doc_count": 158, + "revenue": { + "value": 11533.265625 + }, + "cumulative_revenue": { + "value": 199112.57421875 + } + }, + { + "key_as_string": "2024-12-02T00:00:00.000Z", + "key": 1733097600000, + "doc_count": 144, + "revenue": { + "value": 10499.8125 + }, + "cumulative_revenue": { + "value": 209612.38671875 + } + }, + { + "key_as_string": "2024-12-03T00:00:00.000Z", + "key": 1733184000000, + "doc_count": 151, + "revenue": { + "value": 12111.6875 + }, + "cumulative_revenue": { + "value": 221724.07421875 + } + }, + { + "key_as_string": "2024-12-04T00:00:00.000Z", + "key": 1733270400000, + "doc_count": 145, + "revenue": { + "value": 10530.765625 + }, + "cumulative_revenue": { + "value": 232254.83984375 + } + }, + { + "key_as_string": "2024-12-05T00:00:00.000Z", + "key": 1733356800000, + "doc_count": 157, + "revenue": { + "value": 11872.5625 + }, + "cumulative_revenue": { + "value": 244127.40234375 + } + }, + { + "key_as_string": "2024-12-06T00:00:00.000Z", + "key": 1733443200000, + "doc_count": 158, + "revenue": { + "value": 12109.453125 + }, + "cumulative_revenue": { + "value": 256236.85546875 + } + }, + { + "key_as_string": "2024-12-07T00:00:00.000Z", + "key": 1733529600000, + "doc_count": 153, + "revenue": { + "value": 11057.40625 + }, + "cumulative_revenue": { + "value": 267294.26171875 + } + }, + { + "key_as_string": "2024-12-08T00:00:00.000Z", + "key": 1733616000000, + "doc_count": 165, + "revenue": { + "value": 13095.609375 + }, + "cumulative_revenue": { + "value": 280389.87109375 + } + }, + { + "key_as_string": "2024-12-09T00:00:00.000Z", + "key": 1733702400000, + "doc_count": 153, + "revenue": { + "value": 12574.015625 + }, + "cumulative_revenue": { + "value": 292963.88671875 + } + }, + { + "key_as_string": "2024-12-10T00:00:00.000Z", + "key": 1733788800000, + "doc_count": 158, + "revenue": { + "value": 11188.1875 + }, + "cumulative_revenue": { + "value": 304152.07421875 + } + }, + { + "key_as_string": "2024-12-11T00:00:00.000Z", + "key": 1733875200000, + "doc_count": 160, + "revenue": { + "value": 12117.65625 + }, + "cumulative_revenue": { + "value": 316269.73046875 + } + }, + { + "key_as_string": "2024-12-12T00:00:00.000Z", + "key": 1733961600000, + "doc_count": 159, + "revenue": { + "value": 11558.25 + }, + "cumulative_revenue": { + "value": 327827.98046875 + } + }, + { + "key_as_string": "2024-12-13T00:00:00.000Z", + "key": 1734048000000, + "doc_count": 152, + "revenue": { + "value": 11921.1171875 + }, + "cumulative_revenue": { + "value": 339749.09765625 + } + }, + { + "key_as_string": "2024-12-14T00:00:00.000Z", + "key": 1734134400000, + "doc_count": 142, + "revenue": { + "value": 11135.03125 + }, + "cumulative_revenue": { + "value": 350884.12890625 + } + } + ] + } + } +} +---- +// TEST[skip:Using Kibana sample data] +<1> `daily_sales`: Results from our daily sales date histogram +<2> `buckets`: Array of time-based buckets +<3> `key_as_string`: Date for this bucket (in ISO format since no format specified) +<4> `revenue`: Daily revenue for this date +<5> `cumulative_revenue`: Running total of revenue up to this date +============== + +[discrete] +[[aggregations-tutorial-next-steps]] +=== Next steps + +Refer to the <> for more details on all available aggregation types. \ No newline at end of file diff --git a/docs/reference/quickstart/index.asciidoc b/docs/reference/quickstart/index.asciidoc index 3fa6d53e6345d..cb3a5f2440220 100644 --- a/docs/reference/quickstart/index.asciidoc +++ b/docs/reference/quickstart/index.asciidoc @@ -25,6 +25,7 @@ Alternatively, refer to our <>. Learn about indices, documents, and mappings, and perform a basic search using the Query DSL. * <>. Learn about different options for querying data, including full-text search and filtering, using the Query DSL. +* <>. Learn how to analyze data using different types of aggregations, including metrics, buckets, and pipelines. * <>: Learn how to create embeddings for your data with `semantic_text` and query using the `semantic` query. ** <>: Learn how to combine semantic search with full-text search. * <>: Learn how to ingest dense vector embeddings into {es}. @@ -40,3 +41,4 @@ If you're interested in using {es} with Python, check out Elastic Search Labs: include::getting-started.asciidoc[] include::full-text-filtering-tutorial.asciidoc[] +include::aggs-tutorial.asciidoc[] From c7925957f91fc4ba5a2b834f0e368dd4864f8cf4 Mon Sep 17 00:00:00 2001 From: Kostas Krikellas <131142368+kkrik-es@users.noreply.github.com> Date: Wed, 11 Dec 2024 17:30:43 +0200 Subject: [PATCH 11/77] Unify logsdb index settings providers (#118342) * Unify logsdb index settings providers * restore diff * rename method --- .../xpack/logsdb/LogsDBPlugin.java | 17 +- .../LogsdbIndexModeSettingsProvider.java | 175 +++++++- .../SyntheticSourceIndexSettingsProvider.java | 200 --------- .../LogsdbIndexModeSettingsProviderTests.java | 408 +++++++++++++++++ ...exSettingsProviderLegacyLicenseTests.java} | 15 +- ...heticSourceIndexSettingsProviderTests.java | 417 ------------------ 6 files changed, 588 insertions(+), 644 deletions(-) delete mode 100644 x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/SyntheticSourceIndexSettingsProvider.java rename x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/{SyntheticSourceIndexSettingsProviderLegacyLicenseTests.java => LogsdbIndexSettingsProviderLegacyLicenseTests.java} (91%) delete mode 100644 x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/SyntheticSourceIndexSettingsProviderTests.java diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsDBPlugin.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsDBPlugin.java index 904b00e6d0450..a8085f3d50a82 100644 --- a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsDBPlugin.java +++ b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsDBPlugin.java @@ -43,7 +43,7 @@ public class LogsDBPlugin extends Plugin implements ActionPlugin { public LogsDBPlugin(Settings settings) { this.settings = settings; this.licenseService = new SyntheticSourceLicenseService(settings); - this.logsdbIndexModeSettingsProvider = new LogsdbIndexModeSettingsProvider(settings); + this.logsdbIndexModeSettingsProvider = new LogsdbIndexModeSettingsProvider(licenseService, settings); } @Override @@ -67,16 +67,13 @@ public Collection createComponents(PluginServices services) { @Override public Collection getAdditionalIndexSettingProviders(IndexSettingProvider.Parameters parameters) { - if (DiscoveryNode.isStateless(settings)) { - return List.of(logsdbIndexModeSettingsProvider); + if (DiscoveryNode.isStateless(settings) == false) { + logsdbIndexModeSettingsProvider.init( + parameters.mapperServiceFactory(), + () -> parameters.clusterService().state().nodes().getMinSupportedIndexVersion() + ); } - var syntheticSettingProvider = new SyntheticSourceIndexSettingsProvider( - licenseService, - parameters.mapperServiceFactory(), - logsdbIndexModeSettingsProvider, - () -> parameters.clusterService().state().nodes().getMinSupportedIndexVersion() - ); - return List.of(syntheticSettingProvider, logsdbIndexModeSettingsProvider); + return List.of(logsdbIndexModeSettingsProvider); } @Override diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProvider.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProvider.java index 481657eaf7225..977b0e1c57578 100644 --- a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProvider.java +++ b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProvider.java @@ -7,25 +7,45 @@ package org.elasticsearch.xpack.logsdb; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.CheckedFunction; +import org.elasticsearch.core.Strings; import org.elasticsearch.index.IndexMode; import org.elasticsearch.index.IndexSettingProvider; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.mapper.SourceFieldMapper; +import java.io.IOException; import java.time.Instant; import java.util.List; import java.util.Locale; +import java.util.function.Supplier; +import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_PATH; import static org.elasticsearch.xpack.logsdb.LogsDBPlugin.CLUSTER_LOGSDB_ENABLED; final class LogsdbIndexModeSettingsProvider implements IndexSettingProvider { + private static final Logger LOGGER = LogManager.getLogger(LogsdbIndexModeSettingsProvider.class); private static final String LOGS_PATTERN = "logs-*-*"; + + private final SyntheticSourceLicenseService syntheticSourceLicenseService; + private final SetOnce> mapperServiceFactory = new SetOnce<>(); + private final SetOnce> createdIndexVersion = new SetOnce<>(); + private volatile boolean isLogsdbEnabled; - LogsdbIndexModeSettingsProvider(final Settings settings) { + LogsdbIndexModeSettingsProvider(SyntheticSourceLicenseService syntheticSourceLicenseService, final Settings settings) { + this.syntheticSourceLicenseService = syntheticSourceLicenseService; this.isLogsdbEnabled = CLUSTER_LOGSDB_ENABLED.get(settings); } @@ -33,6 +53,21 @@ void updateClusterIndexModeLogsdbEnabled(boolean isLogsdbEnabled) { this.isLogsdbEnabled = isLogsdbEnabled; } + void init(CheckedFunction factory, Supplier indexVersion) { + mapperServiceFactory.set(factory); + createdIndexVersion.set(indexVersion); + } + + private boolean supportFallbackToStoredSource() { + return mapperServiceFactory.get() != null; + } + + @Override + public boolean overrulesTemplateAndRequestSettings() { + // Indicates that the provider value takes precedence over any user setting. + return true; + } + @Override public Settings getAdditionalIndexSettings( final String indexName, @@ -40,20 +75,42 @@ public Settings getAdditionalIndexSettings( IndexMode templateIndexMode, final Metadata metadata, final Instant resolvedAt, - final Settings settings, + Settings settings, final List combinedTemplateMappings ) { - return getLogsdbModeSetting(dataStreamName, settings); - } - - Settings getLogsdbModeSetting(final String dataStreamName, final Settings settings) { + Settings.Builder settingsBuilder = null; if (isLogsdbEnabled && dataStreamName != null && resolveIndexMode(settings.get(IndexSettings.MODE.getKey())) == null && matchesLogsPattern(dataStreamName)) { - return Settings.builder().put("index.mode", IndexMode.LOGSDB.getName()).build(); + settingsBuilder = Settings.builder().put(IndexSettings.MODE.getKey(), IndexMode.LOGSDB.getName()); + if (supportFallbackToStoredSource()) { + settings = Settings.builder().put(IndexSettings.MODE.getKey(), IndexMode.LOGSDB.getName()).put(settings).build(); + } + } + + if (supportFallbackToStoredSource()) { + // This index name is used when validating component and index templates, we should skip this check in that case. + // (See MetadataIndexTemplateService#validateIndexTemplateV2(...) method) + boolean isTemplateValidation = "validate-index-name".equals(indexName); + boolean legacyLicensedUsageOfSyntheticSourceAllowed = isLegacyLicensedUsageOfSyntheticSourceAllowed( + templateIndexMode, + indexName, + dataStreamName + ); + if (newIndexHasSyntheticSourceUsage(indexName, templateIndexMode, settings, combinedTemplateMappings) + && syntheticSourceLicenseService.fallbackToStoredSource( + isTemplateValidation, + legacyLicensedUsageOfSyntheticSourceAllowed + )) { + LOGGER.debug("creation of index [{}] with synthetic source without it being allowed", indexName); + if (settingsBuilder == null) { + settingsBuilder = Settings.builder(); + } + settingsBuilder.put(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(), SourceFieldMapper.Mode.STORED.toString()); + } } - return Settings.EMPTY; + return settingsBuilder == null ? Settings.EMPTY : settingsBuilder.build(); } private static boolean matchesLogsPattern(final String name) { @@ -63,4 +120,106 @@ private static boolean matchesLogsPattern(final String name) { private IndexMode resolveIndexMode(final String mode) { return mode != null ? Enum.valueOf(IndexMode.class, mode.toUpperCase(Locale.ROOT)) : null; } + + boolean newIndexHasSyntheticSourceUsage( + String indexName, + IndexMode templateIndexMode, + Settings indexTemplateAndCreateRequestSettings, + List combinedTemplateMappings + ) { + if ("validate-index-name".equals(indexName)) { + // This index name is used when validating component and index templates, we should skip this check in that case. + // (See MetadataIndexTemplateService#validateIndexTemplateV2(...) method) + return false; + } + + try { + var tmpIndexMetadata = buildIndexMetadataForMapperService(indexName, templateIndexMode, indexTemplateAndCreateRequestSettings); + var indexMode = tmpIndexMetadata.getIndexMode(); + if (SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.exists(tmpIndexMetadata.getSettings()) + || indexMode == IndexMode.LOGSDB + || indexMode == IndexMode.TIME_SERIES) { + // In case when index mode is tsdb or logsdb and only _source.mode mapping attribute is specified, then the default + // could be wrong. However, it doesn't really matter, because if the _source.mode mapping attribute is set to stored, + // then configuring the index.mapping.source.mode setting to stored has no effect. Additionally _source.mode can't be set + // to disabled, because that isn't allowed with logsdb/tsdb. In other words setting index.mapping.source.mode setting to + // stored when _source.mode mapping attribute is stored is fine as it has no effect, but avoids creating MapperService. + var sourceMode = SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.get(tmpIndexMetadata.getSettings()); + return sourceMode == SourceFieldMapper.Mode.SYNTHETIC; + } + + // TODO: remove this when _source.mode attribute has been removed: + try (var mapperService = mapperServiceFactory.get().apply(tmpIndexMetadata)) { + // combinedTemplateMappings can be null when creating system indices + // combinedTemplateMappings can be empty when creating a normal index that doesn't match any template and without mapping. + if (combinedTemplateMappings == null || combinedTemplateMappings.isEmpty()) { + combinedTemplateMappings = List.of(new CompressedXContent("{}")); + } + mapperService.merge(MapperService.SINGLE_MAPPING_NAME, combinedTemplateMappings, MapperService.MergeReason.INDEX_TEMPLATE); + return mapperService.documentMapper().sourceMapper().isSynthetic(); + } + } catch (AssertionError | Exception e) { + // In case invalid mappings or setting are provided, then mapper service creation can fail. + // In that case it is ok to return false here. The index creation will fail anyway later, so no need to fallback to stored + // source. + LOGGER.info(() -> Strings.format("unable to create mapper service for index [%s]", indexName), e); + return false; + } + } + + // Create a dummy IndexMetadata instance that can be used to create a MapperService in order to check whether synthetic source is used: + private IndexMetadata buildIndexMetadataForMapperService( + String indexName, + IndexMode templateIndexMode, + Settings indexTemplateAndCreateRequestSettings + ) { + var tmpIndexMetadata = IndexMetadata.builder(indexName); + + int dummyPartitionSize = IndexMetadata.INDEX_ROUTING_PARTITION_SIZE_SETTING.get(indexTemplateAndCreateRequestSettings); + int dummyShards = indexTemplateAndCreateRequestSettings.getAsInt( + IndexMetadata.SETTING_NUMBER_OF_SHARDS, + dummyPartitionSize == 1 ? 1 : dummyPartitionSize + 1 + ); + int shardReplicas = indexTemplateAndCreateRequestSettings.getAsInt(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0); + var finalResolvedSettings = Settings.builder() + .put(IndexMetadata.SETTING_VERSION_CREATED, createdIndexVersion.get().get()) + .put(indexTemplateAndCreateRequestSettings) + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, dummyShards) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, shardReplicas) + .put(IndexMetadata.SETTING_INDEX_UUID, UUIDs.randomBase64UUID()); + + if (templateIndexMode == IndexMode.TIME_SERIES) { + finalResolvedSettings.put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES); + // Avoid failing because index.routing_path is missing (in case fields are marked as dimension) + finalResolvedSettings.putList(INDEX_ROUTING_PATH.getKey(), List.of("path")); + } + + tmpIndexMetadata.settings(finalResolvedSettings); + return tmpIndexMetadata.build(); + } + + /** + * The GA-ed use cases in which synthetic source usage is allowed with gold or platinum license. + */ + private boolean isLegacyLicensedUsageOfSyntheticSourceAllowed(IndexMode templateIndexMode, String indexName, String dataStreamName) { + if (templateIndexMode == IndexMode.TIME_SERIES) { + return true; + } + + // To allow the following patterns: profiling-metrics and profiling-events + if (dataStreamName != null && dataStreamName.startsWith("profiling-")) { + return true; + } + // To allow the following patterns: .profiling-sq-executables, .profiling-sq-leafframes and .profiling-stacktraces + if (indexName.startsWith(".profiling-")) { + return true; + } + // To allow the following patterns: metrics-apm.transaction.*, metrics-apm.service_transaction.*, metrics-apm.service_summary.*, + // metrics-apm.service_destination.*, "metrics-apm.internal-* and metrics-apm.app.* + if (dataStreamName != null && dataStreamName.startsWith("metrics-apm.")) { + return true; + } + + return false; + } } diff --git a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/SyntheticSourceIndexSettingsProvider.java b/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/SyntheticSourceIndexSettingsProvider.java deleted file mode 100644 index 462bad4b19551..0000000000000 --- a/x-pack/plugin/logsdb/src/main/java/org/elasticsearch/xpack/logsdb/SyntheticSourceIndexSettingsProvider.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.logsdb; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.elasticsearch.cluster.metadata.IndexMetadata; -import org.elasticsearch.cluster.metadata.Metadata; -import org.elasticsearch.common.UUIDs; -import org.elasticsearch.common.compress.CompressedXContent; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.core.CheckedFunction; -import org.elasticsearch.core.Strings; -import org.elasticsearch.index.IndexMode; -import org.elasticsearch.index.IndexSettingProvider; -import org.elasticsearch.index.IndexSettings; -import org.elasticsearch.index.IndexVersion; -import org.elasticsearch.index.mapper.MapperService; -import org.elasticsearch.index.mapper.SourceFieldMapper; - -import java.io.IOException; -import java.time.Instant; -import java.util.List; -import java.util.function.Supplier; - -import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_ROUTING_PATH; - -/** - * An index setting provider that overwrites the source mode from synthetic to stored if synthetic source isn't allowed to be used. - */ -final class SyntheticSourceIndexSettingsProvider implements IndexSettingProvider { - - private static final Logger LOGGER = LogManager.getLogger(SyntheticSourceIndexSettingsProvider.class); - - private final SyntheticSourceLicenseService syntheticSourceLicenseService; - private final CheckedFunction mapperServiceFactory; - private final LogsdbIndexModeSettingsProvider logsdbIndexModeSettingsProvider; - private final Supplier createdIndexVersion; - - SyntheticSourceIndexSettingsProvider( - SyntheticSourceLicenseService syntheticSourceLicenseService, - CheckedFunction mapperServiceFactory, - LogsdbIndexModeSettingsProvider logsdbIndexModeSettingsProvider, - Supplier createdIndexVersion - ) { - this.syntheticSourceLicenseService = syntheticSourceLicenseService; - this.mapperServiceFactory = mapperServiceFactory; - this.logsdbIndexModeSettingsProvider = logsdbIndexModeSettingsProvider; - this.createdIndexVersion = createdIndexVersion; - } - - @Override - public boolean overrulesTemplateAndRequestSettings() { - // Indicates that the provider value takes precedence over any user setting. - return true; - } - - @Override - public Settings getAdditionalIndexSettings( - String indexName, - String dataStreamName, - IndexMode templateIndexMode, - Metadata metadata, - Instant resolvedAt, - Settings indexTemplateAndCreateRequestSettings, - List combinedTemplateMappings - ) { - var logsdbSettings = logsdbIndexModeSettingsProvider.getLogsdbModeSetting(dataStreamName, indexTemplateAndCreateRequestSettings); - if (logsdbSettings != Settings.EMPTY) { - indexTemplateAndCreateRequestSettings = Settings.builder() - .put(logsdbSettings) - .put(indexTemplateAndCreateRequestSettings) - .build(); - } - - // This index name is used when validating component and index templates, we should skip this check in that case. - // (See MetadataIndexTemplateService#validateIndexTemplateV2(...) method) - boolean isTemplateValidation = "validate-index-name".equals(indexName); - boolean legacyLicensedUsageOfSyntheticSourceAllowed = isLegacyLicensedUsageOfSyntheticSourceAllowed( - templateIndexMode, - indexName, - dataStreamName - ); - if (newIndexHasSyntheticSourceUsage(indexName, templateIndexMode, indexTemplateAndCreateRequestSettings, combinedTemplateMappings) - && syntheticSourceLicenseService.fallbackToStoredSource(isTemplateValidation, legacyLicensedUsageOfSyntheticSourceAllowed)) { - LOGGER.debug("creation of index [{}] with synthetic source without it being allowed", indexName); - return Settings.builder() - .put(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(), SourceFieldMapper.Mode.STORED.toString()) - .build(); - } - return Settings.EMPTY; - } - - boolean newIndexHasSyntheticSourceUsage( - String indexName, - IndexMode templateIndexMode, - Settings indexTemplateAndCreateRequestSettings, - List combinedTemplateMappings - ) { - if ("validate-index-name".equals(indexName)) { - // This index name is used when validating component and index templates, we should skip this check in that case. - // (See MetadataIndexTemplateService#validateIndexTemplateV2(...) method) - return false; - } - - try { - var tmpIndexMetadata = buildIndexMetadataForMapperService(indexName, templateIndexMode, indexTemplateAndCreateRequestSettings); - var indexMode = tmpIndexMetadata.getIndexMode(); - if (SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.exists(tmpIndexMetadata.getSettings()) - || indexMode == IndexMode.LOGSDB - || indexMode == IndexMode.TIME_SERIES) { - // In case when index mode is tsdb or logsdb and only _source.mode mapping attribute is specified, then the default - // could be wrong. However, it doesn't really matter, because if the _source.mode mapping attribute is set to stored, - // then configuring the index.mapping.source.mode setting to stored has no effect. Additionally _source.mode can't be set - // to disabled, because that isn't allowed with logsdb/tsdb. In other words setting index.mapping.source.mode setting to - // stored when _source.mode mapping attribute is stored is fine as it has no effect, but avoids creating MapperService. - var sourceMode = SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.get(tmpIndexMetadata.getSettings()); - return sourceMode == SourceFieldMapper.Mode.SYNTHETIC; - } - - // TODO: remove this when _source.mode attribute has been removed: - try (var mapperService = mapperServiceFactory.apply(tmpIndexMetadata)) { - // combinedTemplateMappings can be null when creating system indices - // combinedTemplateMappings can be empty when creating a normal index that doesn't match any template and without mapping. - if (combinedTemplateMappings == null || combinedTemplateMappings.isEmpty()) { - combinedTemplateMappings = List.of(new CompressedXContent("{}")); - } - mapperService.merge(MapperService.SINGLE_MAPPING_NAME, combinedTemplateMappings, MapperService.MergeReason.INDEX_TEMPLATE); - return mapperService.documentMapper().sourceMapper().isSynthetic(); - } - } catch (AssertionError | Exception e) { - // In case invalid mappings or setting are provided, then mapper service creation can fail. - // In that case it is ok to return false here. The index creation will fail anyway later, so no need to fallback to stored - // source. - LOGGER.info(() -> Strings.format("unable to create mapper service for index [%s]", indexName), e); - return false; - } - } - - // Create a dummy IndexMetadata instance that can be used to create a MapperService in order to check whether synthetic source is used: - private IndexMetadata buildIndexMetadataForMapperService( - String indexName, - IndexMode templateIndexMode, - Settings indexTemplateAndCreateRequestSettings - ) { - var tmpIndexMetadata = IndexMetadata.builder(indexName); - - int dummyPartitionSize = IndexMetadata.INDEX_ROUTING_PARTITION_SIZE_SETTING.get(indexTemplateAndCreateRequestSettings); - int dummyShards = indexTemplateAndCreateRequestSettings.getAsInt( - IndexMetadata.SETTING_NUMBER_OF_SHARDS, - dummyPartitionSize == 1 ? 1 : dummyPartitionSize + 1 - ); - int shardReplicas = indexTemplateAndCreateRequestSettings.getAsInt(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0); - var finalResolvedSettings = Settings.builder() - .put(IndexMetadata.SETTING_VERSION_CREATED, createdIndexVersion.get()) - .put(indexTemplateAndCreateRequestSettings) - .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, dummyShards) - .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, shardReplicas) - .put(IndexMetadata.SETTING_INDEX_UUID, UUIDs.randomBase64UUID()); - - if (templateIndexMode == IndexMode.TIME_SERIES) { - finalResolvedSettings.put(IndexSettings.MODE.getKey(), IndexMode.TIME_SERIES); - // Avoid failing because index.routing_path is missing (in case fields are marked as dimension) - finalResolvedSettings.putList(INDEX_ROUTING_PATH.getKey(), List.of("path")); - } - - tmpIndexMetadata.settings(finalResolvedSettings); - return tmpIndexMetadata.build(); - } - - /** - * The GA-ed use cases in which synthetic source usage is allowed with gold or platinum license. - */ - boolean isLegacyLicensedUsageOfSyntheticSourceAllowed(IndexMode templateIndexMode, String indexName, String dataStreamName) { - if (templateIndexMode == IndexMode.TIME_SERIES) { - return true; - } - - // To allow the following patterns: profiling-metrics and profiling-events - if (dataStreamName != null && dataStreamName.startsWith("profiling-")) { - return true; - } - // To allow the following patterns: .profiling-sq-executables, .profiling-sq-leafframes and .profiling-stacktraces - if (indexName.startsWith(".profiling-")) { - return true; - } - // To allow the following patterns: metrics-apm.transaction.*, metrics-apm.service_transaction.*, metrics-apm.service_summary.*, - // metrics-apm.service_destination.*, "metrics-apm.internal-* and metrics-apm.app.* - if (dataStreamName != null && dataStreamName.startsWith("metrics-apm.")) { - return true; - } - - return false; - } -} diff --git a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProviderTests.java b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProviderTests.java index 5f23dbdca1143..de4f0960f50e7 100644 --- a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProviderTests.java +++ b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LogsdbIndexModeSettingsProviderTests.java @@ -9,19 +9,37 @@ import org.elasticsearch.cluster.metadata.ComposableIndexTemplate; import org.elasticsearch.cluster.metadata.ComposableIndexTemplateMetadata; +import org.elasticsearch.cluster.metadata.DataStream; +import org.elasticsearch.cluster.metadata.DataStreamTestHelper; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.Template; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Tuple; import org.elasticsearch.index.IndexMode; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.MapperTestUtils; +import org.elasticsearch.index.mapper.SourceFieldMapper; +import org.elasticsearch.license.License; +import org.elasticsearch.license.LicenseService; +import org.elasticsearch.license.MockLicenseState; import org.elasticsearch.test.ESTestCase; +import org.junit.Before; import java.io.IOException; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.elasticsearch.common.settings.Settings.builder; +import static org.elasticsearch.xpack.logsdb.SyntheticSourceLicenseServiceTests.createEnterpriseLicense; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class LogsdbIndexModeSettingsProviderTests extends ESTestCase { @@ -43,8 +61,39 @@ public class LogsdbIndexModeSettingsProviderTests extends ESTestCase { } """; + private SyntheticSourceLicenseService syntheticSourceLicenseService; + private final AtomicInteger newMapperServiceCounter = new AtomicInteger(); + + @Before + public void setup() throws Exception { + MockLicenseState licenseState = MockLicenseState.createMock(); + when(licenseState.isAllowed(any())).thenReturn(true); + var licenseService = new SyntheticSourceLicenseService(Settings.EMPTY); + licenseService.setLicenseState(licenseState); + var mockLicenseService = mock(LicenseService.class); + License license = createEnterpriseLicense(); + when(mockLicenseService.getLicense()).thenReturn(license); + syntheticSourceLicenseService = new SyntheticSourceLicenseService(Settings.EMPTY); + syntheticSourceLicenseService.setLicenseState(licenseState); + syntheticSourceLicenseService.setLicenseService(mockLicenseService); + } + + LogsdbIndexModeSettingsProvider withSyntheticSourceDemotionSupport(boolean enabled) { + newMapperServiceCounter.set(0); + var provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, + Settings.builder().put("cluster.logsdb.enabled", enabled).build() + ); + provider.init(im -> { + newMapperServiceCounter.incrementAndGet(); + return MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), im.getSettings(), im.getIndex().getName()); + }, IndexVersion::current); + return provider; + } + public void testLogsDbDisabled() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", false).build() ); @@ -63,6 +112,7 @@ public void testLogsDbDisabled() throws IOException { public void testOnIndexCreation() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", true).build() ); @@ -81,6 +131,7 @@ public void testOnIndexCreation() throws IOException { public void testOnExplicitStandardIndex() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", true).build() ); @@ -99,6 +150,7 @@ public void testOnExplicitStandardIndex() throws IOException { public void testOnExplicitTimeSeriesIndex() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", true).build() ); @@ -117,6 +169,7 @@ public void testOnExplicitTimeSeriesIndex() throws IOException { public void testNonLogsDataStream() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", true).build() ); @@ -135,6 +188,7 @@ public void testNonLogsDataStream() throws IOException { public void testWithoutLogsComponentTemplate() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", true).build() ); @@ -153,6 +207,7 @@ public void testWithoutLogsComponentTemplate() throws IOException { public void testWithLogsComponentTemplate() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", true).build() ); @@ -171,6 +226,7 @@ public void testWithLogsComponentTemplate() throws IOException { public void testWithMultipleComponentTemplates() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", true).build() ); @@ -189,6 +245,7 @@ public void testWithMultipleComponentTemplates() throws IOException { public void testWithCustomComponentTemplatesOnly() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", true).build() ); @@ -207,6 +264,7 @@ public void testWithCustomComponentTemplatesOnly() throws IOException { public void testNonMatchingTemplateIndexPattern() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", true).build() ); @@ -225,6 +283,7 @@ public void testNonMatchingTemplateIndexPattern() throws IOException { public void testCaseSensitivity() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", true).build() ); @@ -243,6 +302,7 @@ public void testCaseSensitivity() throws IOException { public void testMultipleHyphensInDataStreamName() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", true).build() ); @@ -261,6 +321,7 @@ public void testMultipleHyphensInDataStreamName() throws IOException { public void testBeforeAndAFterSettingUpdate() throws IOException { final LogsdbIndexModeSettingsProvider provider = new LogsdbIndexModeSettingsProvider( + syntheticSourceLicenseService, Settings.builder().put("cluster.logsdb.enabled", false).build() ); @@ -323,4 +384,351 @@ private void assertIndexMode(final Settings settings, final String expectedIndex assertEquals(expectedIndexMode, settings.get(IndexSettings.MODE.getKey())); } + public void testNewIndexHasSyntheticSourceUsage() throws IOException { + String dataStreamName = "logs-app1"; + String indexName = DataStream.getDefaultBackingIndexName(dataStreamName, 0); + Settings settings = Settings.EMPTY; + LogsdbIndexModeSettingsProvider provider = withSyntheticSourceDemotionSupport(false); + { + String mapping = """ + { + "_doc": { + "_source": { + "mode": "synthetic" + }, + "properties": { + "my_field": { + "type": "keyword" + } + } + } + } + """; + boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); + assertTrue(result); + assertThat(newMapperServiceCounter.get(), equalTo(1)); + assertWarnings(SourceFieldMapper.DEPRECATION_WARNING); + } + { + String mapping; + boolean withSourceMode = randomBoolean(); + if (withSourceMode) { + mapping = """ + { + "_doc": { + "_source": { + "mode": "stored" + }, + "properties": { + "my_field": { + "type": "keyword" + } + } + } + } + """; + } else { + mapping = """ + { + "_doc": { + "properties": { + "my_field": { + "type": "keyword" + } + } + } + } + """; + } + boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); + assertFalse(result); + assertThat(newMapperServiceCounter.get(), equalTo(2)); + if (withSourceMode) { + assertWarnings(SourceFieldMapper.DEPRECATION_WARNING); + } + } + } + + public void testValidateIndexName() throws IOException { + String indexName = "validate-index-name"; + String mapping = """ + { + "_doc": { + "_source": { + "mode": "synthetic" + }, + "properties": { + "my_field": { + "type": "keyword" + } + } + } + } + """; + Settings settings = Settings.EMPTY; + LogsdbIndexModeSettingsProvider provider = withSyntheticSourceDemotionSupport(false); + boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); + assertFalse(result); + } + + public void testNewIndexHasSyntheticSourceUsageLogsdbIndex() throws IOException { + String dataStreamName = "logs-app1"; + String indexName = DataStream.getDefaultBackingIndexName(dataStreamName, 0); + String mapping = """ + { + "_doc": { + "properties": { + "my_field": { + "type": "keyword" + } + } + } + } + """; + LogsdbIndexModeSettingsProvider provider = withSyntheticSourceDemotionSupport(false); + { + Settings settings = Settings.builder().put("index.mode", "logsdb").build(); + boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); + assertTrue(result); + assertThat(newMapperServiceCounter.get(), equalTo(0)); + } + { + Settings settings = Settings.builder().put("index.mode", "logsdb").build(); + boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of()); + assertTrue(result); + assertThat(newMapperServiceCounter.get(), equalTo(0)); + } + { + boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, Settings.EMPTY, List.of()); + assertFalse(result); + assertThat(newMapperServiceCounter.get(), equalTo(1)); + } + { + boolean result = provider.newIndexHasSyntheticSourceUsage( + indexName, + null, + Settings.EMPTY, + List.of(new CompressedXContent(mapping)) + ); + assertFalse(result); + assertThat(newMapperServiceCounter.get(), equalTo(2)); + } + } + + public void testNewIndexHasSyntheticSourceUsageTimeSeries() throws IOException { + String dataStreamName = "logs-app1"; + String indexName = DataStream.getDefaultBackingIndexName(dataStreamName, 0); + String mapping = """ + { + "_doc": { + "properties": { + "my_field": { + "type": "keyword", + "time_series_dimension": true + } + } + } + } + """; + LogsdbIndexModeSettingsProvider provider = withSyntheticSourceDemotionSupport(false); + { + Settings settings = Settings.builder().put("index.mode", "time_series").put("index.routing_path", "my_field").build(); + boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); + assertTrue(result); + } + { + Settings settings = Settings.builder().put("index.mode", "time_series").put("index.routing_path", "my_field").build(); + boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of()); + assertTrue(result); + } + { + boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, Settings.EMPTY, List.of()); + assertFalse(result); + } + { + boolean result = provider.newIndexHasSyntheticSourceUsage( + indexName, + null, + Settings.EMPTY, + List.of(new CompressedXContent(mapping)) + ); + assertFalse(result); + } + } + + public void testNewIndexHasSyntheticSourceUsage_invalidSettings() throws IOException { + String dataStreamName = "logs-app1"; + String indexName = DataStream.getDefaultBackingIndexName(dataStreamName, 0); + Settings settings = Settings.builder().put("index.soft_deletes.enabled", false).build(); + LogsdbIndexModeSettingsProvider provider = withSyntheticSourceDemotionSupport(false); + { + String mapping = """ + { + "_doc": { + "_source": { + "mode": "synthetic" + }, + "properties": { + "my_field": { + "type": "keyword" + } + } + } + } + """; + boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); + assertFalse(result); + assertThat(newMapperServiceCounter.get(), equalTo(1)); + } + { + String mapping = """ + { + "_doc": { + "properties": { + "my_field": { + "type": "keyword" + } + } + } + } + """; + boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); + assertFalse(result); + assertThat(newMapperServiceCounter.get(), equalTo(2)); + } + } + + public void testGetAdditionalIndexSettingsDowngradeFromSyntheticSource() throws IOException { + String dataStreamName = "logs-app1"; + Metadata.Builder mb = Metadata.builder( + DataStreamTestHelper.getClusterStateWithDataStreams( + List.of(Tuple.tuple(dataStreamName, 1)), + List.of(), + Instant.now().toEpochMilli(), + builder().build(), + 1 + ).getMetadata() + ); + Metadata metadata = mb.build(); + LogsdbIndexModeSettingsProvider provider = withSyntheticSourceDemotionSupport(false); + Settings settings = builder().put(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(), SourceFieldMapper.Mode.SYNTHETIC) + .build(); + + Settings result = provider.getAdditionalIndexSettings( + DataStream.getDefaultBackingIndexName(dataStreamName, 2), + dataStreamName, + null, + metadata, + Instant.ofEpochMilli(1L), + settings, + List.of() + ); + assertThat(result.size(), equalTo(0)); + assertThat(newMapperServiceCounter.get(), equalTo(0)); + + syntheticSourceLicenseService.setSyntheticSourceFallback(true); + result = provider.getAdditionalIndexSettings( + DataStream.getDefaultBackingIndexName(dataStreamName, 2), + dataStreamName, + null, + metadata, + Instant.ofEpochMilli(1L), + settings, + List.of() + ); + assertThat(result.size(), equalTo(1)); + assertEquals(SourceFieldMapper.Mode.STORED, SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.get(result)); + assertThat(newMapperServiceCounter.get(), equalTo(0)); + + result = provider.getAdditionalIndexSettings( + DataStream.getDefaultBackingIndexName(dataStreamName, 2), + dataStreamName, + IndexMode.TIME_SERIES, + metadata, + Instant.ofEpochMilli(1L), + settings, + List.of() + ); + assertThat(result.size(), equalTo(1)); + assertEquals(SourceFieldMapper.Mode.STORED, SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.get(result)); + assertThat(newMapperServiceCounter.get(), equalTo(0)); + + result = provider.getAdditionalIndexSettings( + DataStream.getDefaultBackingIndexName(dataStreamName, 2), + dataStreamName, + IndexMode.LOGSDB, + metadata, + Instant.ofEpochMilli(1L), + settings, + List.of() + ); + assertThat(result.size(), equalTo(1)); + assertEquals(SourceFieldMapper.Mode.STORED, SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.get(result)); + assertThat(newMapperServiceCounter.get(), equalTo(0)); + } + + public void testGetAdditionalIndexSettingsDowngradeFromSyntheticSourceFileMatch() throws IOException { + syntheticSourceLicenseService.setSyntheticSourceFallback(true); + LogsdbIndexModeSettingsProvider provider = withSyntheticSourceDemotionSupport(true); + final Settings settings = Settings.EMPTY; + + String dataStreamName = "logs-app1"; + Metadata.Builder mb = Metadata.builder( + DataStreamTestHelper.getClusterStateWithDataStreams( + List.of(Tuple.tuple(dataStreamName, 1)), + List.of(), + Instant.now().toEpochMilli(), + builder().build(), + 1 + ).getMetadata() + ); + Metadata metadata = mb.build(); + Settings result = provider.getAdditionalIndexSettings( + DataStream.getDefaultBackingIndexName(dataStreamName, 2), + dataStreamName, + null, + metadata, + Instant.ofEpochMilli(1L), + settings, + List.of() + ); + assertThat(result.size(), equalTo(0)); + + dataStreamName = "logs-app1-0"; + mb = Metadata.builder( + DataStreamTestHelper.getClusterStateWithDataStreams( + List.of(Tuple.tuple(dataStreamName, 1)), + List.of(), + Instant.now().toEpochMilli(), + builder().build(), + 1 + ).getMetadata() + ); + metadata = mb.build(); + + result = provider.getAdditionalIndexSettings( + DataStream.getDefaultBackingIndexName(dataStreamName, 2), + dataStreamName, + null, + metadata, + Instant.ofEpochMilli(1L), + settings, + List.of() + ); + assertThat(result.size(), equalTo(2)); + assertEquals(SourceFieldMapper.Mode.STORED, SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.get(result)); + assertEquals(IndexMode.LOGSDB, IndexSettings.MODE.get(result)); + + result = provider.getAdditionalIndexSettings( + DataStream.getDefaultBackingIndexName(dataStreamName, 2), + dataStreamName, + null, + metadata, + Instant.ofEpochMilli(1L), + builder().put(IndexSettings.MODE.getKey(), IndexMode.STANDARD.toString()).build(), + List.of() + ); + assertThat(result.size(), equalTo(0)); + } + } diff --git a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/SyntheticSourceIndexSettingsProviderLegacyLicenseTests.java b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LogsdbIndexSettingsProviderLegacyLicenseTests.java similarity index 91% rename from x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/SyntheticSourceIndexSettingsProviderLegacyLicenseTests.java rename to x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LogsdbIndexSettingsProviderLegacyLicenseTests.java index c871a7d0216ed..8a4adf18b3e67 100644 --- a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/SyntheticSourceIndexSettingsProviderLegacyLicenseTests.java +++ b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LogsdbIndexSettingsProviderLegacyLicenseTests.java @@ -25,15 +25,14 @@ import java.time.ZoneOffset; import java.util.List; -import static org.elasticsearch.xpack.logsdb.SyntheticSourceIndexSettingsProviderTests.getLogsdbIndexModeSettingsProvider; import static org.elasticsearch.xpack.logsdb.SyntheticSourceLicenseServiceTests.createGoldOrPlatinumLicense; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class SyntheticSourceIndexSettingsProviderLegacyLicenseTests extends ESTestCase { +public class LogsdbIndexSettingsProviderLegacyLicenseTests extends ESTestCase { - private SyntheticSourceIndexSettingsProvider provider; + private LogsdbIndexModeSettingsProvider provider; @Before public void setup() throws Exception { @@ -50,10 +49,9 @@ public void setup() throws Exception { syntheticSourceLicenseService.setLicenseState(licenseState); syntheticSourceLicenseService.setLicenseService(mockLicenseService); - provider = new SyntheticSourceIndexSettingsProvider( - syntheticSourceLicenseService, + provider = new LogsdbIndexModeSettingsProvider(syntheticSourceLicenseService, Settings.EMPTY); + provider.init( im -> MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), im.getSettings(), im.getIndex().getName()), - getLogsdbIndexModeSettingsProvider(false), IndexVersion::current ); } @@ -112,10 +110,9 @@ public void testGetAdditionalIndexSettingsTsdbAfterCutoffDate() throws Exception syntheticSourceLicenseService.setLicenseState(licenseState); syntheticSourceLicenseService.setLicenseService(mockLicenseService); - provider = new SyntheticSourceIndexSettingsProvider( - syntheticSourceLicenseService, + provider = new LogsdbIndexModeSettingsProvider(syntheticSourceLicenseService, Settings.EMPTY); + provider.init( im -> MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), im.getSettings(), im.getIndex().getName()), - getLogsdbIndexModeSettingsProvider(false), IndexVersion::current ); diff --git a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/SyntheticSourceIndexSettingsProviderTests.java b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/SyntheticSourceIndexSettingsProviderTests.java deleted file mode 100644 index df1fb8f2d958c..0000000000000 --- a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/SyntheticSourceIndexSettingsProviderTests.java +++ /dev/null @@ -1,417 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.logsdb; - -import org.elasticsearch.cluster.metadata.DataStream; -import org.elasticsearch.cluster.metadata.DataStreamTestHelper; -import org.elasticsearch.cluster.metadata.Metadata; -import org.elasticsearch.common.compress.CompressedXContent; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.core.Tuple; -import org.elasticsearch.index.IndexMode; -import org.elasticsearch.index.IndexSettings; -import org.elasticsearch.index.IndexVersion; -import org.elasticsearch.index.MapperTestUtils; -import org.elasticsearch.index.mapper.SourceFieldMapper; -import org.elasticsearch.license.License; -import org.elasticsearch.license.LicenseService; -import org.elasticsearch.license.MockLicenseState; -import org.elasticsearch.test.ESTestCase; -import org.junit.Before; - -import java.io.IOException; -import java.time.Instant; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.elasticsearch.common.settings.Settings.builder; -import static org.elasticsearch.xpack.logsdb.SyntheticSourceLicenseServiceTests.createEnterpriseLicense; -import static org.hamcrest.Matchers.equalTo; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class SyntheticSourceIndexSettingsProviderTests extends ESTestCase { - - private SyntheticSourceLicenseService syntheticSourceLicenseService; - private SyntheticSourceIndexSettingsProvider provider; - private final AtomicInteger newMapperServiceCounter = new AtomicInteger(); - - static LogsdbIndexModeSettingsProvider getLogsdbIndexModeSettingsProvider(boolean enabled) { - return new LogsdbIndexModeSettingsProvider(Settings.builder().put("cluster.logsdb.enabled", enabled).build()); - } - - @Before - public void setup() throws Exception { - MockLicenseState licenseState = MockLicenseState.createMock(); - when(licenseState.isAllowed(any())).thenReturn(true); - var licenseService = new SyntheticSourceLicenseService(Settings.EMPTY); - licenseService.setLicenseState(licenseState); - var mockLicenseService = mock(LicenseService.class); - License license = createEnterpriseLicense(); - when(mockLicenseService.getLicense()).thenReturn(license); - syntheticSourceLicenseService = new SyntheticSourceLicenseService(Settings.EMPTY); - syntheticSourceLicenseService.setLicenseState(licenseState); - syntheticSourceLicenseService.setLicenseService(mockLicenseService); - - provider = new SyntheticSourceIndexSettingsProvider(syntheticSourceLicenseService, im -> { - newMapperServiceCounter.incrementAndGet(); - return MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), im.getSettings(), im.getIndex().getName()); - }, getLogsdbIndexModeSettingsProvider(false), IndexVersion::current); - newMapperServiceCounter.set(0); - } - - public void testNewIndexHasSyntheticSourceUsage() throws IOException { - String dataStreamName = "logs-app1"; - String indexName = DataStream.getDefaultBackingIndexName(dataStreamName, 0); - Settings settings = Settings.EMPTY; - { - String mapping = """ - { - "_doc": { - "_source": { - "mode": "synthetic" - }, - "properties": { - "my_field": { - "type": "keyword" - } - } - } - } - """; - boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); - assertTrue(result); - assertThat(newMapperServiceCounter.get(), equalTo(1)); - assertWarnings(SourceFieldMapper.DEPRECATION_WARNING); - } - { - String mapping; - boolean withSourceMode = randomBoolean(); - if (withSourceMode) { - mapping = """ - { - "_doc": { - "_source": { - "mode": "stored" - }, - "properties": { - "my_field": { - "type": "keyword" - } - } - } - } - """; - } else { - mapping = """ - { - "_doc": { - "properties": { - "my_field": { - "type": "keyword" - } - } - } - } - """; - } - boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); - assertFalse(result); - assertThat(newMapperServiceCounter.get(), equalTo(2)); - if (withSourceMode) { - assertWarnings(SourceFieldMapper.DEPRECATION_WARNING); - } - } - } - - public void testValidateIndexName() throws IOException { - String indexName = "validate-index-name"; - String mapping = """ - { - "_doc": { - "_source": { - "mode": "synthetic" - }, - "properties": { - "my_field": { - "type": "keyword" - } - } - } - } - """; - Settings settings = Settings.EMPTY; - boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); - assertFalse(result); - } - - public void testNewIndexHasSyntheticSourceUsageLogsdbIndex() throws IOException { - String dataStreamName = "logs-app1"; - String indexName = DataStream.getDefaultBackingIndexName(dataStreamName, 0); - String mapping = """ - { - "_doc": { - "properties": { - "my_field": { - "type": "keyword" - } - } - } - } - """; - { - Settings settings = Settings.builder().put("index.mode", "logsdb").build(); - boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); - assertTrue(result); - assertThat(newMapperServiceCounter.get(), equalTo(0)); - } - { - Settings settings = Settings.builder().put("index.mode", "logsdb").build(); - boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of()); - assertTrue(result); - assertThat(newMapperServiceCounter.get(), equalTo(0)); - } - { - boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, Settings.EMPTY, List.of()); - assertFalse(result); - assertThat(newMapperServiceCounter.get(), equalTo(1)); - } - { - boolean result = provider.newIndexHasSyntheticSourceUsage( - indexName, - null, - Settings.EMPTY, - List.of(new CompressedXContent(mapping)) - ); - assertFalse(result); - assertThat(newMapperServiceCounter.get(), equalTo(2)); - } - } - - public void testNewIndexHasSyntheticSourceUsageTimeSeries() throws IOException { - String dataStreamName = "logs-app1"; - String indexName = DataStream.getDefaultBackingIndexName(dataStreamName, 0); - String mapping = """ - { - "_doc": { - "properties": { - "my_field": { - "type": "keyword", - "time_series_dimension": true - } - } - } - } - """; - { - Settings settings = Settings.builder().put("index.mode", "time_series").put("index.routing_path", "my_field").build(); - boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); - assertTrue(result); - } - { - Settings settings = Settings.builder().put("index.mode", "time_series").put("index.routing_path", "my_field").build(); - boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of()); - assertTrue(result); - } - { - boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, Settings.EMPTY, List.of()); - assertFalse(result); - } - { - boolean result = provider.newIndexHasSyntheticSourceUsage( - indexName, - null, - Settings.EMPTY, - List.of(new CompressedXContent(mapping)) - ); - assertFalse(result); - } - } - - public void testNewIndexHasSyntheticSourceUsage_invalidSettings() throws IOException { - String dataStreamName = "logs-app1"; - String indexName = DataStream.getDefaultBackingIndexName(dataStreamName, 0); - Settings settings = Settings.builder().put("index.soft_deletes.enabled", false).build(); - { - String mapping = """ - { - "_doc": { - "_source": { - "mode": "synthetic" - }, - "properties": { - "my_field": { - "type": "keyword" - } - } - } - } - """; - boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); - assertFalse(result); - assertThat(newMapperServiceCounter.get(), equalTo(1)); - } - { - String mapping = """ - { - "_doc": { - "properties": { - "my_field": { - "type": "keyword" - } - } - } - } - """; - boolean result = provider.newIndexHasSyntheticSourceUsage(indexName, null, settings, List.of(new CompressedXContent(mapping))); - assertFalse(result); - assertThat(newMapperServiceCounter.get(), equalTo(2)); - } - } - - public void testGetAdditionalIndexSettingsDowngradeFromSyntheticSource() throws IOException { - String dataStreamName = "logs-app1"; - Metadata.Builder mb = Metadata.builder( - DataStreamTestHelper.getClusterStateWithDataStreams( - List.of(Tuple.tuple(dataStreamName, 1)), - List.of(), - Instant.now().toEpochMilli(), - builder().build(), - 1 - ).getMetadata() - ); - Metadata metadata = mb.build(); - - Settings settings = builder().put(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(), SourceFieldMapper.Mode.SYNTHETIC) - .build(); - - Settings result = provider.getAdditionalIndexSettings( - DataStream.getDefaultBackingIndexName(dataStreamName, 2), - dataStreamName, - null, - metadata, - Instant.ofEpochMilli(1L), - settings, - List.of() - ); - assertThat(result.size(), equalTo(0)); - assertThat(newMapperServiceCounter.get(), equalTo(0)); - - syntheticSourceLicenseService.setSyntheticSourceFallback(true); - result = provider.getAdditionalIndexSettings( - DataStream.getDefaultBackingIndexName(dataStreamName, 2), - dataStreamName, - null, - metadata, - Instant.ofEpochMilli(1L), - settings, - List.of() - ); - assertThat(result.size(), equalTo(1)); - assertEquals(SourceFieldMapper.Mode.STORED, SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.get(result)); - assertThat(newMapperServiceCounter.get(), equalTo(0)); - - result = provider.getAdditionalIndexSettings( - DataStream.getDefaultBackingIndexName(dataStreamName, 2), - dataStreamName, - IndexMode.TIME_SERIES, - metadata, - Instant.ofEpochMilli(1L), - settings, - List.of() - ); - assertThat(result.size(), equalTo(1)); - assertEquals(SourceFieldMapper.Mode.STORED, SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.get(result)); - assertThat(newMapperServiceCounter.get(), equalTo(0)); - - result = provider.getAdditionalIndexSettings( - DataStream.getDefaultBackingIndexName(dataStreamName, 2), - dataStreamName, - IndexMode.LOGSDB, - metadata, - Instant.ofEpochMilli(1L), - settings, - List.of() - ); - assertThat(result.size(), equalTo(1)); - assertEquals(SourceFieldMapper.Mode.STORED, SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.get(result)); - assertThat(newMapperServiceCounter.get(), equalTo(0)); - } - - public void testGetAdditionalIndexSettingsDowngradeFromSyntheticSourceFileMatch() throws IOException { - syntheticSourceLicenseService.setSyntheticSourceFallback(true); - provider = new SyntheticSourceIndexSettingsProvider( - syntheticSourceLicenseService, - im -> MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), im.getSettings(), im.getIndex().getName()), - getLogsdbIndexModeSettingsProvider(true), - IndexVersion::current - ); - final Settings settings = Settings.EMPTY; - - String dataStreamName = "logs-app1"; - Metadata.Builder mb = Metadata.builder( - DataStreamTestHelper.getClusterStateWithDataStreams( - List.of(Tuple.tuple(dataStreamName, 1)), - List.of(), - Instant.now().toEpochMilli(), - builder().build(), - 1 - ).getMetadata() - ); - Metadata metadata = mb.build(); - Settings result = provider.getAdditionalIndexSettings( - DataStream.getDefaultBackingIndexName(dataStreamName, 2), - dataStreamName, - null, - metadata, - Instant.ofEpochMilli(1L), - settings, - List.of() - ); - assertThat(result.size(), equalTo(0)); - assertThat(newMapperServiceCounter.get(), equalTo(0)); - - dataStreamName = "logs-app1-0"; - mb = Metadata.builder( - DataStreamTestHelper.getClusterStateWithDataStreams( - List.of(Tuple.tuple(dataStreamName, 1)), - List.of(), - Instant.now().toEpochMilli(), - builder().build(), - 1 - ).getMetadata() - ); - metadata = mb.build(); - - result = provider.getAdditionalIndexSettings( - DataStream.getDefaultBackingIndexName(dataStreamName, 2), - dataStreamName, - null, - metadata, - Instant.ofEpochMilli(1L), - settings, - List.of() - ); - assertThat(result.size(), equalTo(1)); - assertEquals(SourceFieldMapper.Mode.STORED, SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.get(result)); - assertThat(newMapperServiceCounter.get(), equalTo(0)); - - result = provider.getAdditionalIndexSettings( - DataStream.getDefaultBackingIndexName(dataStreamName, 2), - dataStreamName, - null, - metadata, - Instant.ofEpochMilli(1L), - builder().put(IndexSettings.MODE.getKey(), IndexMode.STANDARD.toString()).build(), - List.of() - ); - assertThat(result.size(), equalTo(0)); - assertThat(newMapperServiceCounter.get(), equalTo(0)); - } -} From d839205135774841bdad8a6604264e0256684830 Mon Sep 17 00:00:00 2001 From: Dan Rubinstein Date: Wed, 11 Dec 2024 10:47:46 -0500 Subject: [PATCH 12/77] Removing index alias creation for deprecated transforms notification index (#117583) * Removing index alias creation for deprecated transforms notification index * Update docs/changelog/117583.yaml * Updating changelog * Updating deprecation area to Transform --------- Co-authored-by: Elastic Machine --- docs/changelog/117583.yaml | 17 +++++ .../TransformInternalIndexConstants.java | 1 - .../integration/TransformAuditorIT.java | 27 -------- .../TransformClusterStateListener.java | 63 ------------------- 4 files changed, 17 insertions(+), 91 deletions(-) create mode 100644 docs/changelog/117583.yaml diff --git a/docs/changelog/117583.yaml b/docs/changelog/117583.yaml new file mode 100644 index 0000000000000..e0c482b8d9f72 --- /dev/null +++ b/docs/changelog/117583.yaml @@ -0,0 +1,17 @@ +pr: 117583 +summary: Removing index alias creation for deprecated transforms notification index +area: Machine Learning +type: deprecation +issues: [] +deprecation: + title: Removing index alias creation for deprecated transforms notification index + area: Transform + details: >- + As part of the migration from 7.x to 8.x, the `.data-frame-notifications-1` index + was deprecated and replaced with the `.transform-notifications-000002` index. + The index is no longer created by default, all writes are directed to the new index, + and any clusters with the deprecated index will have an alias created to ensure that + reads are still retrieving data that was written to the index before the migration to 8.x. + This change removes the alias from the deprecated index in 9.x. Any clusters with the alias present + will retain it, but it will not be created on new clusters. + impact: No known end user impact. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/persistence/TransformInternalIndexConstants.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/persistence/TransformInternalIndexConstants.java index 0d54583b89976..8439c9cd76fad 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/persistence/TransformInternalIndexConstants.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/transforms/persistence/TransformInternalIndexConstants.java @@ -45,7 +45,6 @@ public final class TransformInternalIndexConstants { public static final String AUDIT_TEMPLATE_VERSION = "000002"; public static final String AUDIT_INDEX_PREFIX = TRANSFORM_PREFIX + "notifications-"; public static final String AUDIT_INDEX_PATTERN = AUDIT_INDEX_PREFIX + "*"; - public static final String AUDIT_INDEX_DEPRECATED = TRANSFORM_PREFIX_DEPRECATED + "notifications-1"; public static final String AUDIT_INDEX_PATTERN_DEPRECATED = TRANSFORM_PREFIX_DEPRECATED + "notifications-*"; public static final String AUDIT_INDEX_READ_ALIAS = TRANSFORM_PREFIX + "notifications-read"; diff --git a/x-pack/plugin/transform/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformAuditorIT.java b/x-pack/plugin/transform/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformAuditorIT.java index 7e31b7ec0c5e4..97851f79322b3 100644 --- a/x-pack/plugin/transform/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformAuditorIT.java +++ b/x-pack/plugin/transform/qa/single-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformAuditorIT.java @@ -8,9 +8,6 @@ package org.elasticsearch.xpack.transform.integration; import org.elasticsearch.client.Request; -import org.elasticsearch.client.RequestOptions; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.xpack.core.transform.transforms.persistence.TransformInternalIndexConstants; import org.junit.Before; @@ -92,28 +89,4 @@ public void testAuditorWritesAudits() throws Exception { }); } - - public void testAliasCreatedforBWCIndexes() throws Exception { - Settings.Builder settings = indexSettings(1, 0); - - // These indices should only exist if created in previous versions, ignore the deprecation warning for this test - RequestOptions options = expectWarnings( - "index name [" - + TransformInternalIndexConstants.AUDIT_INDEX_DEPRECATED - + "] starts " - + "with a dot '.', in the next major version, index names starting with a dot are reserved for hidden indices " - + "and system indices" - ).toBuilder().addHeader("X-elastic-product-origin", "elastic").build(); - Request request = new Request("PUT", "/" + TransformInternalIndexConstants.AUDIT_INDEX_DEPRECATED); - String entity = "{\"settings\": " + Strings.toString(settings.build()) + "}"; - request.setJsonEntity(entity); - request.setOptions(options); - client().performRequest(request); - - assertBusy( - () -> assertTrue( - aliasExists(TransformInternalIndexConstants.AUDIT_INDEX_DEPRECATED, TransformInternalIndexConstants.AUDIT_INDEX_READ_ALIAS) - ) - ); - } } diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/TransformClusterStateListener.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/TransformClusterStateListener.java index 4c867616e9be0..e49beb9d57f4d 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/TransformClusterStateListener.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/TransformClusterStateListener.java @@ -9,26 +9,18 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; -import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse; import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateListener; -import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.gateway.GatewayService; -import org.elasticsearch.xpack.core.transform.transforms.persistence.TransformInternalIndexConstants; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; -import static org.elasticsearch.xpack.core.ClientHelper.TRANSFORM_ORIGIN; -import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; - class TransformClusterStateListener implements ClusterStateListener, Supplier> { private static final Logger logger = LogManager.getLogger(TransformClusterStateListener.class); @@ -51,61 +43,6 @@ public void clusterChanged(ClusterChangedEvent event) { } clusterState.set(event.state()); - - // The atomic flag prevents multiple simultaneous attempts to run alias creation - // if there is a flurry of cluster state updates in quick succession - if (event.localNodeMaster() && isIndexCreationInProgress.compareAndSet(false, true)) { - createAuditAliasForDataFrameBWC(event.state(), client, ActionListener.wrap(r -> { - isIndexCreationInProgress.set(false); - if (r) { - logger.info("Created alias for deprecated data frame notifications index"); - } else { - logger.debug("Skipped creating alias for deprecated data frame notifications index"); - } - }, e -> { - isIndexCreationInProgress.set(false); - logger.error("Error creating alias for deprecated data frame notifications index", e); - })); - } - } - - private static void createAuditAliasForDataFrameBWC(ClusterState state, Client client, final ActionListener finalListener) { - - // check if old audit index exists, no need to create the alias if it does not - if (state.getMetadata().hasIndexAbstraction(TransformInternalIndexConstants.AUDIT_INDEX_DEPRECATED) == false) { - finalListener.onResponse(false); - return; - } - - Metadata metadata = state.metadata(); - if (state.getMetadata() - .getIndicesLookup() - .get(TransformInternalIndexConstants.AUDIT_INDEX_DEPRECATED) - .getIndices() - .stream() - .anyMatch(name -> metadata.index(name).getAliases().containsKey(TransformInternalIndexConstants.AUDIT_INDEX_READ_ALIAS))) { - finalListener.onResponse(false); - return; - } - - final IndicesAliasesRequest request = client.admin() - .indices() - .prepareAliases() - .addAliasAction( - IndicesAliasesRequest.AliasActions.add() - .index(TransformInternalIndexConstants.AUDIT_INDEX_DEPRECATED) - .alias(TransformInternalIndexConstants.AUDIT_INDEX_READ_ALIAS) - .isHidden(true) - ) - .request(); - - executeAsyncWithOrigin( - client.threadPool().getThreadContext(), - TRANSFORM_ORIGIN, - request, - ActionListener.wrap(r -> finalListener.onResponse(r.isAcknowledged()), finalListener::onFailure), - client.admin().indices()::aliases - ); } /** From 03fa2705e7bbf38e886cc095a0e1723e6a524585 Mon Sep 17 00:00:00 2001 From: Iraklis Psaroudakis Date: Wed, 11 Dec 2024 17:49:12 +0200 Subject: [PATCH 13/77] Specialize skip for InputStreamIndexInput (#118436) Skip would previously defer to the default implementation that reads bytes unnecessarily and may be slow. We now specialize it so that it seeks quickly. Closes ES-10234 --- .../lucene/store/InputStreamIndexInput.java | 11 ++++++ .../store/InputStreamIndexInputTests.java | 37 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/common/lucene/store/InputStreamIndexInput.java b/server/src/main/java/org/elasticsearch/common/lucene/store/InputStreamIndexInput.java index 5603a1d4f1ab0..f3a3ec91ee931 100644 --- a/server/src/main/java/org/elasticsearch/common/lucene/store/InputStreamIndexInput.java +++ b/server/src/main/java/org/elasticsearch/common/lucene/store/InputStreamIndexInput.java @@ -88,4 +88,15 @@ public synchronized void reset() throws IOException { indexInput.seek(markPointer); counter = markCounter; } + + @Override + public long skip(long n) throws IOException { + long skipBytes = Math.min(n, Math.min(indexInput.length() - indexInput.getFilePointer(), limit - counter)); + if (skipBytes <= 0) { + return 0; + } + indexInput.skipBytes(skipBytes); + counter += skipBytes; + return skipBytes; + } } diff --git a/server/src/test/java/org/elasticsearch/common/lucene/store/InputStreamIndexInputTests.java b/server/src/test/java/org/elasticsearch/common/lucene/store/InputStreamIndexInputTests.java index a1bcf1b91fa4d..4bea6f50c7c4b 100644 --- a/server/src/test/java/org/elasticsearch/common/lucene/store/InputStreamIndexInputTests.java +++ b/server/src/test/java/org/elasticsearch/common/lucene/store/InputStreamIndexInputTests.java @@ -218,7 +218,7 @@ public void testReadMultiFourBytesLimit() throws IOException { assertThat(is.read(read), equalTo(-1)); } - public void testMarkRest() throws Exception { + public void testMarkReset() throws Exception { Directory dir = new ByteBuffersDirectory(); IndexOutput output = dir.createOutput("test", IOContext.DEFAULT); for (int i = 0; i < 3; i++) { @@ -243,6 +243,41 @@ public void testMarkRest() throws Exception { assertThat(is.read(), equalTo(2)); } + public void testSkipBytes() throws Exception { + Directory dir = new ByteBuffersDirectory(); + IndexOutput output = dir.createOutput("test", IOContext.DEFAULT); + int bytes = randomIntBetween(10, 100); + for (int i = 0; i < bytes; i++) { + output.writeByte((byte) i); + } + output.close(); + + int limit = randomIntBetween(0, bytes * 2); + int initialReadBytes = randomIntBetween(0, limit); + int skipBytes = randomIntBetween(0, limit); + int seekExpected = Math.min(Math.min(initialReadBytes + skipBytes, limit), bytes); + int skipBytesExpected = Math.max(seekExpected - initialReadBytes, 0); + logger.debug( + "bytes: {}, limit: {}, initialReadBytes: {}, skipBytes: {}, seekExpected: {}, skipBytesExpected: {}", + bytes, + limit, + initialReadBytes, + skipBytes, + seekExpected, + skipBytesExpected + ); + + IndexInput input = dir.openInput("test", IOContext.DEFAULT); + InputStreamIndexInput is = new InputStreamIndexInput(input, limit); + is.readNBytes(initialReadBytes); + assertThat(is.skip(skipBytes), equalTo((long) skipBytesExpected)); + + int remainingBytes = Math.min(bytes, limit) - seekExpected; + for (int i = seekExpected; i < seekExpected + remainingBytes; i++) { + assertThat(is.read(), equalTo(i)); + } + } + public void testReadZeroShouldReturnZero() throws IOException { try (Directory dir = new ByteBuffersDirectory()) { try (IndexOutput output = dir.createOutput("test", IOContext.DEFAULT)) { From 912d37abef76fd741767765adaf731dea4f21984 Mon Sep 17 00:00:00 2001 From: Iraklis Psaroudakis Date: Wed, 11 Dec 2024 17:49:32 +0200 Subject: [PATCH 14/77] No-op reset in SlicedInputStream (#118437) Previously if reset was called at the exact marked offset, it would unnecessarily re-open the current slice and skip bytes. We now detect this situation, and just do nothing in this case. Closes ES-10235 --- .../index/snapshots/blobstore/SlicedInputStream.java | 4 ++++ .../index/snapshots/blobstore/SlicedInputStreamTests.java | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/snapshots/blobstore/SlicedInputStream.java b/server/src/main/java/org/elasticsearch/index/snapshots/blobstore/SlicedInputStream.java index 1edd69a6443a7..2486cc66fd4c9 100644 --- a/server/src/main/java/org/elasticsearch/index/snapshots/blobstore/SlicedInputStream.java +++ b/server/src/main/java/org/elasticsearch/index/snapshots/blobstore/SlicedInputStream.java @@ -171,6 +171,10 @@ public void reset() throws IOException { if (markedSlice < 0 || markedSliceOffset < 0) { throw new IOException("Mark has not been set"); } + if (initialized && nextSlice == markedSlice + 1 && currentSliceOffset == markedSliceOffset) { + // Reset at the marked offset should return immediately without re-opening the slice + return; + } nextSlice = markedSlice; initialized = true; diff --git a/server/src/test/java/org/elasticsearch/index/snapshots/blobstore/SlicedInputStreamTests.java b/server/src/test/java/org/elasticsearch/index/snapshots/blobstore/SlicedInputStreamTests.java index c31a68f36de71..256d0f269edb4 100644 --- a/server/src/test/java/org/elasticsearch/index/snapshots/blobstore/SlicedInputStreamTests.java +++ b/server/src/test/java/org/elasticsearch/index/snapshots/blobstore/SlicedInputStreamTests.java @@ -155,9 +155,10 @@ protected InputStream openSlice(int slice) throws IOException { // Mark input.mark(randomNonNegativeInt()); + int slicesOpenedAtMark = streamsOpened.size(); // Read or skip up to another random point - final int moreBytes = randomIntBetween(0, bytes.length - mark); + int moreBytes = randomIntBetween(0, bytes.length - mark); if (moreBytes > 0) { if (randomBoolean()) { final var moreBytesRead = new byte[moreBytes]; @@ -171,11 +172,13 @@ protected InputStream openSlice(int slice) throws IOException { // Randomly read to EOF if (randomBoolean()) { - input.readAllBytes(); + moreBytes += input.readAllBytes().length; } // Reset input.reset(); + int slicesOpenedAfterReset = streamsOpened.size(); + assert moreBytes > 0 || mark == 0 || slicesOpenedAfterReset == slicesOpenedAtMark : "Reset at mark should not re-open slices"; // Read all remaining bytes, which should be the bytes from mark up to the end final int remainingBytes = bytes.length - mark; From e0ad97e8d56cd2fee9834fc3bf60e73c074c439a Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Wed, 11 Dec 2024 17:03:52 +0100 Subject: [PATCH 15/77] Add QA test module for Lucene N-2 version (#118363) This change introduces a new QA project to test Lucene support for reading indices created in version N-2. The test suite is inspired from the various full-cluster restart suites we already have. It creates a cluster in version N-2 (today 7.17.25), then upgrades the cluster to N-1 (today 8.18.0) and finally upgrades the cluster to the current version (today 9.0), allowing to execute test methods after every upgrade. The test suite has two variants: one for searchable snapshots and one for snapshot restore. The suites demonstrates that Elasticsearch does not allow reading indices written in version N-2 but we hope to make this feasible. Also, the tests can be used for investigation and debug with the command `./gradlew ":qa:lucene-index-compatibility:check" --debug-jvm-server` Relates ES-10274 --- .../gradle/internal/BwcVersions.java | 14 ++ qa/lucene-index-compatibility/build.gradle | 25 ++++ ...tractLuceneIndexCompatibilityTestCase.java | 141 ++++++++++++++++++ .../lucene/LuceneCompatibilityIT.java | 114 ++++++++++++++ .../SearchableSnapshotCompatibilityIT.java | 117 +++++++++++++++ 5 files changed, 411 insertions(+) create mode 100644 qa/lucene-index-compatibility/build.gradle create mode 100644 qa/lucene-index-compatibility/src/javaRestTest/java/org/elasticsearch/lucene/AbstractLuceneIndexCompatibilityTestCase.java create mode 100644 qa/lucene-index-compatibility/src/javaRestTest/java/org/elasticsearch/lucene/LuceneCompatibilityIT.java create mode 100644 qa/lucene-index-compatibility/src/javaRestTest/java/org/elasticsearch/lucene/SearchableSnapshotCompatibilityIT.java diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/BwcVersions.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/BwcVersions.java index 37b28389ad97b..9f7645349e852 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/BwcVersions.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/BwcVersions.java @@ -252,6 +252,20 @@ private List getReleased() { .toList(); } + public List getReadOnlyIndexCompatible() { + // Lucene can read indices in version N-2 + int compatibleMajor = currentVersion.getMajor() - 2; + return versions.stream().filter(v -> v.getMajor() == compatibleMajor).sorted(Comparator.naturalOrder()).toList(); + } + + public void withLatestReadOnlyIndexCompatible(Consumer versionAction) { + var compatibleVersions = getReadOnlyIndexCompatible(); + if (compatibleVersions == null || compatibleVersions.isEmpty()) { + throw new IllegalStateException("No read-only compatible version found."); + } + versionAction.accept(compatibleVersions.getLast()); + } + /** * Return versions of Elasticsearch which are index compatible with the current version. */ diff --git a/qa/lucene-index-compatibility/build.gradle b/qa/lucene-index-compatibility/build.gradle new file mode 100644 index 0000000000000..37e5eea85a08b --- /dev/null +++ b/qa/lucene-index-compatibility/build.gradle @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.internal-test-artifact' +apply plugin: 'elasticsearch.bwc-test' + +buildParams.bwcVersions.withLatestReadOnlyIndexCompatible { bwcVersion -> + tasks.named("javaRestTest").configure { + systemProperty("tests.minimum.index.compatible", bwcVersion) + usesBwcDistribution(bwcVersion) + enabled = true + } +} + +tasks.withType(Test).configureEach { + // CI doesn't like it when there's multiple clusters running at once + maxParallelForks = 1 +} + diff --git a/qa/lucene-index-compatibility/src/javaRestTest/java/org/elasticsearch/lucene/AbstractLuceneIndexCompatibilityTestCase.java b/qa/lucene-index-compatibility/src/javaRestTest/java/org/elasticsearch/lucene/AbstractLuceneIndexCompatibilityTestCase.java new file mode 100644 index 0000000000000..c42e879f84892 --- /dev/null +++ b/qa/lucene-index-compatibility/src/javaRestTest/java/org/elasticsearch/lucene/AbstractLuceneIndexCompatibilityTestCase.java @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.lucene; + +import com.carrotsearch.randomizedtesting.TestMethodAndParams; +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; +import com.carrotsearch.randomizedtesting.annotations.TestCaseOrdering; + +import org.elasticsearch.client.Request; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.local.LocalClusterConfigProvider; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; +import org.elasticsearch.test.cluster.util.Version; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.rules.RuleChain; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestRule; + +import java.util.Comparator; +import java.util.Locale; +import java.util.stream.Stream; + +import static org.elasticsearch.test.cluster.util.Version.CURRENT; +import static org.elasticsearch.test.cluster.util.Version.fromString; +import static org.elasticsearch.test.rest.ObjectPath.createFromResponse; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Test suite for Lucene indices backward compatibility with N-2 versions. The test suite creates a cluster in N-2 version, then upgrades it + * to N-1 version and finally upgrades it to the current version. Test methods are executed after each upgrade. + */ +@TestCaseOrdering(AbstractLuceneIndexCompatibilityTestCase.TestCaseOrdering.class) +public abstract class AbstractLuceneIndexCompatibilityTestCase extends ESRestTestCase { + + protected static final Version VERSION_MINUS_2 = fromString(System.getProperty("tests.minimum.index.compatible")); + protected static final Version VERSION_MINUS_1 = fromString(System.getProperty("tests.minimum.wire.compatible")); + protected static final Version VERSION_CURRENT = CURRENT; + + protected static TemporaryFolder REPOSITORY_PATH = new TemporaryFolder(); + + protected static LocalClusterConfigProvider clusterConfig = c -> {}; + private static ElasticsearchCluster cluster = ElasticsearchCluster.local() + .distribution(DistributionType.DEFAULT) + .version(VERSION_MINUS_2) + .nodes(2) + .setting("path.repo", () -> REPOSITORY_PATH.getRoot().getPath()) + .setting("xpack.security.enabled", "false") + .setting("xpack.ml.enabled", "false") + .setting("path.repo", () -> REPOSITORY_PATH.getRoot().getPath()) + .apply(() -> clusterConfig) + .build(); + + @ClassRule + public static TestRule ruleChain = RuleChain.outerRule(REPOSITORY_PATH).around(cluster); + + private static boolean upgradeFailed = false; + + private final Version clusterVersion; + + public AbstractLuceneIndexCompatibilityTestCase(@Name("cluster") Version clusterVersion) { + this.clusterVersion = clusterVersion; + } + + @ParametersFactory + public static Iterable parameters() { + return Stream.of(VERSION_MINUS_2, VERSION_MINUS_1, CURRENT).map(v -> new Object[] { v }).toList(); + } + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } + + @Override + protected boolean preserveClusterUponCompletion() { + return true; + } + + @Before + public void maybeUpgrade() throws Exception { + // We want to use this test suite for the V9 upgrade, but we are not fully committed to necessarily having N-2 support + // in V10, so we add a check here to ensure we'll revisit this decision once V10 exists. + assertThat("Explicit check that N-2 version is Elasticsearch 7", VERSION_MINUS_2.getMajor(), equalTo(7)); + + var currentVersion = clusterVersion(); + if (currentVersion.before(clusterVersion)) { + try { + cluster.upgradeToVersion(clusterVersion); + closeClients(); + initClient(); + } catch (Exception e) { + upgradeFailed = true; + throw e; + } + } + + // Skip remaining tests if upgrade failed + assumeFalse("Cluster upgrade failed", upgradeFailed); + } + + protected String suffix(String name) { + return name + '-' + getTestName().split(" ")[0].toLowerCase(Locale.ROOT); + } + + protected static Version clusterVersion() throws Exception { + var response = assertOK(client().performRequest(new Request("GET", "/"))); + var responseBody = createFromResponse(response); + var version = Version.fromString(responseBody.evaluate("version.number").toString()); + assertThat("Failed to retrieve cluster version", version, notNullValue()); + return version; + } + + protected static Version indexLuceneVersion(String indexName) throws Exception { + var response = assertOK(client().performRequest(new Request("GET", "/" + indexName + "/_settings"))); + int id = Integer.parseInt(createFromResponse(response).evaluate(indexName + ".settings.index.version.created")); + return new Version((byte) ((id / 1000000) % 100), (byte) ((id / 10000) % 100), (byte) ((id / 100) % 100)); + } + + /** + * Execute the test suite with the parameters provided by the {@link #parameters()} in version order. + */ + public static class TestCaseOrdering implements Comparator { + @Override + public int compare(TestMethodAndParams o1, TestMethodAndParams o2) { + var version1 = (Version) o1.getInstanceArguments().get(0); + var version2 = (Version) o2.getInstanceArguments().get(0); + return version1.compareTo(version2); + } + } +} diff --git a/qa/lucene-index-compatibility/src/javaRestTest/java/org/elasticsearch/lucene/LuceneCompatibilityIT.java b/qa/lucene-index-compatibility/src/javaRestTest/java/org/elasticsearch/lucene/LuceneCompatibilityIT.java new file mode 100644 index 0000000000000..d6dd949b843d6 --- /dev/null +++ b/qa/lucene-index-compatibility/src/javaRestTest/java/org/elasticsearch/lucene/LuceneCompatibilityIT.java @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.lucene; + +import org.elasticsearch.client.Request; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.repositories.fs.FsRepository; +import org.elasticsearch.test.cluster.util.Version; + +import java.util.stream.IntStream; + +import static org.elasticsearch.test.rest.ObjectPath.createFromResponse; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +public class LuceneCompatibilityIT extends AbstractLuceneIndexCompatibilityTestCase { + + static { + clusterConfig = config -> config.setting("xpack.license.self_generated.type", "trial"); + } + + public LuceneCompatibilityIT(Version version) { + super(version); + } + + public void testRestoreIndex() throws Exception { + final String repository = suffix("repository"); + final String snapshot = suffix("snapshot"); + final String index = suffix("index"); + final int numDocs = 1234; + + logger.debug("--> registering repository [{}]", repository); + registerRepository( + client(), + repository, + FsRepository.TYPE, + true, + Settings.builder().put("location", REPOSITORY_PATH.getRoot().getPath()).build() + ); + + if (VERSION_MINUS_2.equals(clusterVersion())) { + logger.debug("--> creating index [{}]", index); + createIndex( + client(), + index, + Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .build() + ); + + logger.debug("--> indexing [{}] docs in [{}]", numDocs, index); + final var bulks = new StringBuilder(); + IntStream.range(0, numDocs).forEach(n -> bulks.append(Strings.format(""" + {"index":{"_id":"%s","_index":"%s"}} + {"test":"test"} + """, n, index))); + + var bulkRequest = new Request("POST", "/_bulk"); + bulkRequest.setJsonEntity(bulks.toString()); + var bulkResponse = client().performRequest(bulkRequest); + assertOK(bulkResponse); + assertThat(entityAsMap(bulkResponse).get("errors"), allOf(notNullValue(), is(false))); + + logger.debug("--> creating snapshot [{}]", snapshot); + createSnapshot(client(), repository, snapshot, true); + return; + } + + if (VERSION_MINUS_1.equals(clusterVersion())) { + ensureGreen(index); + + assertThat(indexLuceneVersion(index), equalTo(VERSION_MINUS_2)); + assertDocCount(client(), index, numDocs); + + logger.debug("--> deleting index [{}]", index); + deleteIndex(index); + return; + } + + if (VERSION_CURRENT.equals(clusterVersion())) { + var restoredIndex = suffix("index-restored"); + logger.debug("--> restoring index [{}] as archive [{}]", index, restoredIndex); + + // Restoring the archive will fail as Elasticsearch does not support reading N-2 yet + var request = new Request("POST", "/_snapshot/" + repository + "/" + snapshot + "/_restore"); + request.addParameter("wait_for_completion", "true"); + request.setJsonEntity(Strings.format(""" + { + "indices": "%s", + "include_global_state": false, + "rename_pattern": "(.+)", + "rename_replacement": "%s", + "include_aliases": false + }""", index, restoredIndex)); + var responseBody = createFromResponse(client().performRequest(request)); + assertThat(responseBody.evaluate("snapshot.shards.total"), equalTo((int) responseBody.evaluate("snapshot.shards.failed"))); + assertThat(responseBody.evaluate("snapshot.shards.successful"), equalTo(0)); + } + } +} diff --git a/qa/lucene-index-compatibility/src/javaRestTest/java/org/elasticsearch/lucene/SearchableSnapshotCompatibilityIT.java b/qa/lucene-index-compatibility/src/javaRestTest/java/org/elasticsearch/lucene/SearchableSnapshotCompatibilityIT.java new file mode 100644 index 0000000000000..4f348b7fb122f --- /dev/null +++ b/qa/lucene-index-compatibility/src/javaRestTest/java/org/elasticsearch/lucene/SearchableSnapshotCompatibilityIT.java @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.lucene; + +import org.elasticsearch.client.Request; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.repositories.fs.FsRepository; +import org.elasticsearch.test.cluster.util.Version; + +import java.util.stream.IntStream; + +import static org.elasticsearch.test.rest.ObjectPath.createFromResponse; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +public class SearchableSnapshotCompatibilityIT extends AbstractLuceneIndexCompatibilityTestCase { + + static { + clusterConfig = config -> config.setting("xpack.license.self_generated.type", "trial") + .setting("xpack.searchable.snapshot.shared_cache.size", "16MB") + .setting("xpack.searchable.snapshot.shared_cache.region_size", "256KB"); + } + + public SearchableSnapshotCompatibilityIT(Version version) { + super(version); + } + + // TODO Add a test to mount the N-2 index on N-1 and then search it on N + + public void testSearchableSnapshot() throws Exception { + final String repository = suffix("repository"); + final String snapshot = suffix("snapshot"); + final String index = suffix("index"); + final int numDocs = 1234; + + logger.debug("--> registering repository [{}]", repository); + registerRepository( + client(), + repository, + FsRepository.TYPE, + true, + Settings.builder().put("location", REPOSITORY_PATH.getRoot().getPath()).build() + ); + + if (VERSION_MINUS_2.equals(clusterVersion())) { + logger.debug("--> creating index [{}]", index); + createIndex( + client(), + index, + Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true) + .build() + ); + + logger.debug("--> indexing [{}] docs in [{}]", numDocs, index); + final var bulks = new StringBuilder(); + IntStream.range(0, numDocs).forEach(n -> bulks.append(Strings.format(""" + {"index":{"_id":"%s","_index":"%s"}} + {"test":"test"} + """, n, index))); + + var bulkRequest = new Request("POST", "/_bulk"); + bulkRequest.setJsonEntity(bulks.toString()); + var bulkResponse = client().performRequest(bulkRequest); + assertOK(bulkResponse); + assertThat(entityAsMap(bulkResponse).get("errors"), allOf(notNullValue(), is(false))); + + logger.debug("--> creating snapshot [{}]", snapshot); + createSnapshot(client(), repository, snapshot, true); + return; + } + + if (VERSION_MINUS_1.equals(clusterVersion())) { + ensureGreen(index); + + assertThat(indexLuceneVersion(index), equalTo(VERSION_MINUS_2)); + assertDocCount(client(), index, numDocs); + + logger.debug("--> deleting index [{}]", index); + deleteIndex(index); + return; + } + + if (VERSION_CURRENT.equals(clusterVersion())) { + var mountedIndex = suffix("index-mounted"); + logger.debug("--> mounting index [{}] as [{}]", index, mountedIndex); + + // Mounting the index will fail as Elasticsearch does not support reading N-2 yet + var request = new Request("POST", "/_snapshot/" + repository + "/" + snapshot + "/_mount"); + request.addParameter("wait_for_completion", "true"); + var storage = randomBoolean() ? "shared_cache" : "full_copy"; + request.addParameter("storage", storage); + request.setJsonEntity(Strings.format(""" + { + "index": "%s", + "renamed_index": "%s" + }""", index, mountedIndex)); + var responseBody = createFromResponse(client().performRequest(request)); + assertThat(responseBody.evaluate("snapshot.shards.total"), equalTo((int) responseBody.evaluate("snapshot.shards.failed"))); + assertThat(responseBody.evaluate("snapshot.shards.successful"), equalTo(0)); + } + } +} From 9837e782e1a5787ce99afab31426e29be8aa3bc4 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Wed, 11 Dec 2024 08:09:21 -0800 Subject: [PATCH 16/77] Rename instrumenter tests (#118462) The "sythetic" tests are the only unit tests for the instrumenter. This commit renames the test suite to be more clear it is the place to put instrumenter tests. --- ...rumenterTests.java => InstrumenterTests.java} | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename libs/entitlement/asm-provider/src/test/java/org/elasticsearch/entitlement/instrumentation/impl/{SyntheticInstrumenterTests.java => InstrumenterTests.java} (96%) diff --git a/libs/entitlement/asm-provider/src/test/java/org/elasticsearch/entitlement/instrumentation/impl/SyntheticInstrumenterTests.java b/libs/entitlement/asm-provider/src/test/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterTests.java similarity index 96% rename from libs/entitlement/asm-provider/src/test/java/org/elasticsearch/entitlement/instrumentation/impl/SyntheticInstrumenterTests.java rename to libs/entitlement/asm-provider/src/test/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterTests.java index 8e0409971ba61..75102b0bf260d 100644 --- a/libs/entitlement/asm-provider/src/test/java/org/elasticsearch/entitlement/instrumentation/impl/SyntheticInstrumenterTests.java +++ b/libs/entitlement/asm-provider/src/test/java/org/elasticsearch/entitlement/instrumentation/impl/InstrumenterTests.java @@ -34,8 +34,8 @@ * some ad-hoc test cases (e.g. overloaded methods, overloaded targets, multiple instrumentation, etc.) */ @ESTestCase.WithoutSecurityManager -public class SyntheticInstrumenterTests extends ESTestCase { - private static final Logger logger = LogManager.getLogger(SyntheticInstrumenterTests.class); +public class InstrumenterTests extends ESTestCase { + private static final Logger logger = LogManager.getLogger(InstrumenterTests.class); /** * Contains all the virtual methods from {@link TestClassToInstrument}, @@ -137,10 +137,10 @@ public void checkSomeStaticMethod(Class callerClass, int arg, String anotherA @Override public void checkSomeInstanceMethod(Class callerClass, Testable that, int arg, String anotherArg) { checkSomeInstanceMethodCallCount++; - assertSame(SyntheticInstrumenterTests.class, callerClass); + assertSame(InstrumenterTests.class, callerClass); assertThat( that.getClass().getName(), - startsWith("org.elasticsearch.entitlement.instrumentation.impl.SyntheticInstrumenterTests$TestClassToInstrument") + startsWith("org.elasticsearch.entitlement.instrumentation.impl.InstrumenterTests$TestClassToInstrument") ); assertEquals(123, arg); assertEquals("def", anotherArg); @@ -150,14 +150,14 @@ public void checkSomeInstanceMethod(Class callerClass, Testable that, int arg @Override public void checkCtor(Class callerClass) { checkCtorCallCount++; - assertSame(SyntheticInstrumenterTests.class, callerClass); + assertSame(InstrumenterTests.class, callerClass); throwIfActive(); } @Override public void checkCtor(Class callerClass, int arg) { checkCtorIntCallCount++; - assertSame(SyntheticInstrumenterTests.class, callerClass); + assertSame(InstrumenterTests.class, callerClass); assertEquals(123, arg); throwIfActive(); } @@ -374,8 +374,8 @@ public void testInstrumenterWorksWithConstructors() throws Exception { * Testable) which is not what would happen when it's run by the agent. */ private InstrumenterImpl createInstrumenter(Map checkMethods) { - String checkerClass = Type.getInternalName(SyntheticInstrumenterTests.MockEntitlementChecker.class); - String handleClass = Type.getInternalName(SyntheticInstrumenterTests.TestEntitlementCheckerHolder.class); + String checkerClass = Type.getInternalName(InstrumenterTests.MockEntitlementChecker.class); + String handleClass = Type.getInternalName(InstrumenterTests.TestEntitlementCheckerHolder.class); String getCheckerClassMethodDescriptor = Type.getMethodDescriptor(Type.getObjectType(checkerClass)); return new InstrumenterImpl(handleClass, getCheckerClassMethodDescriptor, "_NEW", checkMethods); From 55727779c04f3817ce1d504cc62dceccb60b11d1 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:21:48 -0500 Subject: [PATCH 17/77] [ML] Fixing streaming tests locale issue (#118481) * Fixing the string locale * Missing a toUpper --- muted-tests.yml | 3 --- .../src/main/java/org/elasticsearch/test/ESTestCase.java | 1 + .../org/elasticsearch/xpack/inference/InferenceCrudIT.java | 7 +++++-- .../mock/TestStreamingCompletionServiceExtension.java | 5 +++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index c0e3c217abce2..9416113770d5a 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -315,9 +315,6 @@ tests: - class: org.elasticsearch.xpack.test.rest.XPackRestIT method: test {p0=migrate/10_reindex/Test Reindex With Unsupported Mode} issue: https://github.com/elastic/elasticsearch/issues/118273 -- class: org.elasticsearch.xpack.inference.InferenceCrudIT - method: testUnifiedCompletionInference - issue: https://github.com/elastic/elasticsearch/issues/118405 - class: org.elasticsearch.xpack.security.operator.OperatorPrivilegesIT method: testEveryActionIsEitherOperatorOnlyOrNonOperator issue: https://github.com/elastic/elasticsearch/issues/118220 diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java index 6612f0da0c43f..f678f4af22328 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -1210,6 +1210,7 @@ public static String randomAlphaOfLength(int codeUnits) { /** * Generate a random string containing only alphanumeric characters. + * The locale for the string is {@link Locale#ROOT}. * @param length the length of the string to generate * @return the generated string */ diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java index 90d4f3a8eb33b..fc593a6a8b0fa 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/InferenceCrudIT.java @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -471,7 +472,7 @@ public void testSupportedStream() throws Exception { var events = streamInferOnMockService(modelId, TaskType.COMPLETION, input); var expectedResponses = Stream.concat( - input.stream().map(String::toUpperCase).map(str -> "{\"completion\":[{\"delta\":\"" + str + "\"}]}"), + input.stream().map(s -> s.toUpperCase(Locale.ROOT)).map(str -> "{\"completion\":[{\"delta\":\"" + str + "\"}]}"), Stream.of("[DONE]") ).iterator(); assertThat(events.size(), equalTo((input.size() + 1) * 2)); @@ -510,7 +511,9 @@ public void testUnifiedCompletionInference() throws Exception { } private static Iterator expectedResultsIterator(List input) { - return Stream.concat(input.stream().map(String::toUpperCase).map(InferenceCrudIT::expectedResult), Stream.of("[DONE]")).iterator(); + // The Locale needs to be ROOT to match what the test service is going to respond with + return Stream.concat(input.stream().map(s -> s.toUpperCase(Locale.ROOT)).map(InferenceCrudIT::expectedResult), Stream.of("[DONE]")) + .iterator(); } private static String expectedResult(String input) { diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestStreamingCompletionServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestStreamingCompletionServiceExtension.java index f7a05a27354ef..80696a285fb26 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestStreamingCompletionServiceExtension.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestStreamingCompletionServiceExtension.java @@ -43,6 +43,7 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.Flow; @@ -142,7 +143,7 @@ public void unifiedCompletionInfer( } private StreamingChatCompletionResults makeResults(List input) { - var responseIter = input.stream().map(String::toUpperCase).iterator(); + var responseIter = input.stream().map(s -> s.toUpperCase(Locale.ROOT)).iterator(); return new StreamingChatCompletionResults(subscriber -> { subscriber.onSubscribe(new Flow.Subscription() { @Override @@ -173,7 +174,7 @@ private ChunkedToXContent completionChunk(String delta) { } private StreamingUnifiedChatCompletionResults makeUnifiedResults(UnifiedCompletionRequest request) { - var responseIter = request.messages().stream().map(message -> message.content().toString().toUpperCase()).iterator(); + var responseIter = request.messages().stream().map(message -> message.content().toString().toUpperCase(Locale.ROOT)).iterator(); return new StreamingUnifiedChatCompletionResults(subscriber -> { subscriber.onSubscribe(new Flow.Subscription() { @Override From a8a4a7bc2348460bfa79fabc820e7b0a88907af1 Mon Sep 17 00:00:00 2001 From: Patrick Doyle <810052+prdoyle@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:35:30 -0500 Subject: [PATCH 18/77] Fix testInvalidJSON (#118398) * Fix testInvalidJSON * CURSE YOU SPOTLESS --- muted-tests.yml | 3 - .../service/FileSettingsServiceTests.java | 69 +++++++++---------- 2 files changed, 31 insertions(+), 41 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index 9416113770d5a..c07363657b3ec 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -123,9 +123,6 @@ tests: - class: org.elasticsearch.xpack.downsample.ILMDownsampleDisruptionIT method: testILMDownsampleRollingRestart issue: https://github.com/elastic/elasticsearch/issues/114233 -- class: org.elasticsearch.reservedstate.service.FileSettingsServiceTests - method: testInvalidJSON - issue: https://github.com/elastic/elasticsearch/issues/116521 - class: org.elasticsearch.reservedstate.service.RepositoriesFileSettingsIT method: testSettingsApplied issue: https://github.com/elastic/elasticsearch/issues/116694 diff --git a/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java b/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java index ae60a21b6fc22..08d83e48b7152 100644 --- a/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java @@ -74,8 +74,10 @@ import static org.hamcrest.Matchers.hasEntry; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -288,63 +290,54 @@ public void testProcessFileChanges() throws Exception { verifyNoMoreInteractions(healthIndicatorService); } - @SuppressWarnings("unchecked") public void testInvalidJSON() throws Exception { - doAnswer((Answer) invocation -> { - invocation.getArgument(1, XContentParser.class).map(); // Throw if JSON is invalid - ((Consumer) invocation.getArgument(3)).accept(null); - return null; - }).when(controller).process(any(), any(XContentParser.class), any(), any()); - - CyclicBarrier fileChangeBarrier = new CyclicBarrier(2); - fileSettingsService.addFileChangedListener(() -> awaitOrBust(fileChangeBarrier)); + // Chop off the functionality so we don't run too much of the actual cluster logic that we're not testing + doNothing().when(controller).updateErrorState(any()); + doAnswer( + (Answer) invocation -> { throw new AssertionError("Parse error should happen before this process method is called"); } + ).when(controller).process(any(), any(ReservedStateChunk.class), any(), any()); + // Don't really care about the initial state Files.createDirectories(fileSettingsService.watchedFileDir()); - // contents of the JSON don't matter, we just need a file to exist - writeTestFile(fileSettingsService.watchedFile(), "{}"); + doNothing().when(fileSettingsService).processInitialFileMissing(); + fileSettingsService.start(); + fileSettingsService.clusterChanged(new ClusterChangedEvent("test", clusterService.state(), ClusterState.EMPTY_STATE)); + // Now break the JSON and wait + CyclicBarrier fileChangeBarrier = new CyclicBarrier(2); doAnswer((Answer) invocation -> { - boolean returnedNormally = false; try { - var result = invocation.callRealMethod(); - returnedNormally = true; - return result; - } catch (XContentParseException e) { - // We're expecting a parse error. processFileChanges specifies that this is supposed to throw ExecutionException. - throw new ExecutionException(e); - } catch (Throwable e) { - throw new AssertionError("Unexpected exception", e); + return invocation.callRealMethod(); } finally { - if (returnedNormally == false) { - // Because of the exception, listeners aren't notified, so we need to activate the barrier ourselves - awaitOrBust(fileChangeBarrier); - } + awaitOrBust(fileChangeBarrier); } }).when(fileSettingsService).processFileChanges(); - - // Establish the initial valid JSON - fileSettingsService.start(); - fileSettingsService.clusterChanged(new ClusterChangedEvent("test", clusterService.state(), ClusterState.EMPTY_STATE)); - awaitOrBust(fileChangeBarrier); - - // Now break the JSON writeTestFile(fileSettingsService.watchedFile(), "test_invalid_JSON"); awaitOrBust(fileChangeBarrier); - verify(fileSettingsService, times(1)).processFileOnServiceStart(); // The initial state - verify(fileSettingsService, times(1)).processFileChanges(); // The changed state verify(fileSettingsService, times(1)).onProcessFileChangesException( - argThat(e -> e instanceof ExecutionException && e.getCause() instanceof XContentParseException) + argThat(e -> unwrapException(e) instanceof XContentParseException) ); // Note: the name "processFileOnServiceStart" is a bit misleading because it is not // referring to fileSettingsService.start(). Rather, it is referring to the initialization // of the watcher thread itself, which occurs asynchronously when clusterChanged is first called. - verify(healthIndicatorService, times(2)).changeOccurred(); - verify(healthIndicatorService, times(1)).successOccurred(); - verify(healthIndicatorService, times(1)).failureOccurred(argThat(s -> s.startsWith(IllegalArgumentException.class.getName()))); - verifyNoMoreInteractions(healthIndicatorService); + verify(healthIndicatorService).failureOccurred(contains(XContentParseException.class.getName())); + } + + /** + * Looks for the ultimate cause of {@code e} by stripping off layers of bookkeeping exception wrappers. + */ + private Throwable unwrapException(Throwable e) { + while (e != null) { + if (e instanceof ExecutionException || e instanceof IllegalStateException) { + e = e.getCause(); + } else { + break; + } + } + return e; } private static void awaitOrBust(CyclicBarrier barrier) { From ae9bb90fd1ac229af2d960ace5f5d69eb4c352bf Mon Sep 17 00:00:00 2001 From: Marci W <333176+marciw@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:24:24 -0500 Subject: [PATCH 19/77] Update and edit logsdb docs for logsdb / synthetic source GA (#118303) * Update licensing; fix screenshots; edit generally * Small edit for clarity and style * Update docs/reference/index-modules.asciidoc Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> * Apply changes from review Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> * Address review comments * Match similar change from review * More changes from review * Apply suggestions from review Co-authored-by: shainaraskas <58563081+shainaraskas@users.noreply.github.com> * Apply suggestions from review Co-authored-by: shainaraskas <58563081+shainaraskas@users.noreply.github.com> * Update docs/reference/data-streams/logs.asciidoc Co-authored-by: shainaraskas <58563081+shainaraskas@users.noreply.github.com> * Apply suggestions from review Co-authored-by: shainaraskas <58563081+shainaraskas@users.noreply.github.com> * Apply suggestions from review * Change to general subscription note * Apply suggestions from review Co-authored-by: Oleksandr Kolomiiets * Apply suggestions from review Co-authored-by: Oleksandr Kolomiiets * Apply suggestions from review; additional edits * Apply suggestions from review; clarity tweaks * Restore previous paragraph structure and context --------- Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Co-authored-by: shainaraskas <58563081+shainaraskas@users.noreply.github.com> Co-authored-by: Oleksandr Kolomiiets --- docs/reference/data-streams/logs.asciidoc | 197 +++++++++--------- docs/reference/data-streams/tsds.asciidoc | 2 +- .../management-data-stream-fields.png | Bin 0 -> 251799 bytes .../index-mgmt/management-data-stream.png | Bin 751333 -> 0 bytes .../index-mgmt/management-index-templates.png | Bin 130766 -> 203626 bytes docs/reference/index-modules.asciidoc | 5 +- docs/reference/indices/index-mgmt.asciidoc | 2 +- .../indices/put-index-template.asciidoc | 6 +- .../mapping/fields/synthetic-source.asciidoc | 11 +- 9 files changed, 112 insertions(+), 111 deletions(-) create mode 100644 docs/reference/images/index-mgmt/management-data-stream-fields.png delete mode 100644 docs/reference/images/index-mgmt/management-data-stream.png diff --git a/docs/reference/data-streams/logs.asciidoc b/docs/reference/data-streams/logs.asciidoc index 6bb98684544a3..3af5e09889a89 100644 --- a/docs/reference/data-streams/logs.asciidoc +++ b/docs/reference/data-streams/logs.asciidoc @@ -1,18 +1,20 @@ [[logs-data-stream]] == Logs data stream -preview::[Logs data streams and the logsdb index mode are in tech preview and may be changed or removed in the future. Don't use logs data streams or logsdb index mode in production.] +IMPORTANT: The {es} `logsdb` index mode is generally available in Elastic Cloud Hosted +and self-managed Elasticsearch as of version 8.17, and is enabled by default for +logs in https://www.elastic.co/elasticsearch/serverless[{serverless-full}]. A logs data stream is a data stream type that stores log data more efficiently. In benchmarks, log data stored in a logs data stream used ~2.5 times less disk space than a regular data -stream. The exact impact will vary depending on your data set. +stream. The exact impact varies by data set. [discrete] [[how-to-use-logsds]] === Create a logs data stream -To create a logs data stream, set your index template `index.mode` to `logsdb`: +To create a logs data stream, set your <> `index.mode` to `logsdb`: [source,console] ---- @@ -31,10 +33,12 @@ PUT _index_template/my-index-template // TEST <1> The index mode setting. -<2> The index template priority. By default, Elasticsearch ships with an index template with a `logs-*-*` pattern with a priority of 100. You need to define a priority higher than 100 to ensure that this index template gets selected over the default index template for the `logs-*-*` pattern. See the <> for more information. +<2> The index template priority. By default, Elasticsearch ships with a `logs-*-*` index template with a priority of 100. To make sure your index template takes priority over the default `logs-*-*` template, set its `priority` to a number higher than 100. For more information, see <>. After the index template is created, new indices that use the template will be configured as a logs data stream. You can start indexing data and <>. +You can also set the index mode and adjust other template settings in <>. + //// [source,console] ---- @@ -46,154 +50,159 @@ DELETE _index_template/my-index-template [[logsdb-default-settings]] [discrete] -[[logsdb-synthtic-source]] +[[logsdb-synthetic-source]] === Synthetic source -By default, `logsdb` mode uses <>, which omits storing the original `_source` -field and synthesizes it from doc values or stored fields upon document retrieval. Synthetic source comes with a few -restrictions which you can read more about in the <> section dedicated to it. +If you have the required https://www.elastic.co/subscriptions[subscription], `logsdb` index mode uses <>, which omits storing the original `_source` +field. Instead, the document source is synthesized from doc values or stored fields upon document retrieval. -NOTE: When dealing with multi-value fields, the `index.mapping.synthetic_source_keep` setting controls how field values -are preserved for <> reconstruction. In `logsdb`, the default value is `arrays`, -which retains both duplicate values and the order of entries but not necessarily the exact structure when it comes to -array elements or objects. Preserving duplicates and ordering could be critical for some log fields. This could be the -case, for instance, for DNS A records, HTTP headers, or log entries that represent sequential or repeated events. +If you don't have the required https://www.elastic.co/subscriptions[subscription], `logsdb` mode uses the original `_source` field. -For more details on this setting and ways to refine or bypass it, check out <>. +Before using synthetic source, make sure to review the <>. + +When working with multi-value fields, the `index.mapping.synthetic_source_keep` setting controls how field values +are preserved for <> reconstruction. In `logsdb`, the default value is `arrays`, +which retains both duplicate values and the order of entries. However, the exact structure of +array elements and objects is not necessarily retained. Preserving duplicates and ordering can be critical for some +log fields, such as DNS A records, HTTP headers, and log entries that represent sequential or repeated events. [discrete] [[logsdb-sort-settings]] === Index sort settings -The following settings are applied by default when using the `logsdb` mode for index sorting: +In `logsdb` index mode, the following sort settings are applied by default: -* `index.sort.field`: `["host.name", "@timestamp"]` - In `logsdb` mode, indices are sorted by `host.name` and `@timestamp` fields by default. For data streams, the - `@timestamp` field is automatically injected if it is not present. +`index.sort.field`: `["host.name", "@timestamp"]`:: +Indices are sorted by `host.name` and `@timestamp` by default. The `@timestamp` field is automatically injected if it is not present. -* `index.sort.order`: `["desc", "desc"]` - The default sort order for both fields is descending (`desc`), prioritizing the latest data. +`index.sort.order`: `["desc", "desc"]`:: +Both `host.name` and `@timestamp` are sorted in descending (`desc`) order, prioritizing the latest data. -* `index.sort.mode`: `["min", "min"]` - The default sort mode is `min`, ensuring that indices are sorted by the minimum value of multi-value fields. +`index.sort.mode`: `["min", "min"]`:: +The `min` mode sorts indices by the minimum value of multi-value fields. -* `index.sort.missing`: `["_first", "_first"]` - Missing values are sorted to appear first (`_first`) in `logsdb` index mode. +`index.sort.missing`: `["_first", "_first"]`:: +Missing values are sorted to appear `_first`. -`logsdb` index mode allows users to override the default sort settings. For instance, users can specify their own fields -and order for sorting by modifying the `index.sort.field` and `index.sort.order`. +You can override these default sort settings. For example, to sort on different fields +and change the order, manually configure `index.sort.field` and `index.sort.order`. For more details, see +<>. -When using default sort settings, the `host.name` field is automatically injected into the mappings of the -index as a `keyword` field to ensure that sorting can be applied. This guarantees that logs are efficiently sorted and -retrieved based on the `host.name` and `@timestamp` fields. +When using the default sort settings, the `host.name` field is automatically injected into the index mappings as a `keyword` field to ensure that sorting can be applied. This guarantees that logs are efficiently sorted and retrieved based on the `host.name` and `@timestamp` fields. -NOTE: If `subobjects` is set to `true` (which is the default), the `host.name` field will be mapped as an object field -named `host`, containing a `name` child field of type `keyword`. On the other hand, if `subobjects` is set to `false`, -a single `host.name` field will be mapped as a `keyword` field. +NOTE: If `subobjects` is set to `true` (default), the `host` field is mapped as an object field +named `host` with a `name` child field of type `keyword`. If `subobjects` is set to `false`, +a single `host.name` field is mapped as a `keyword` field. -Once an index is created, the sort settings are immutable and cannot be modified. To apply different sort settings, -a new index must be created with the desired configuration. For data streams, this can be achieved by means of an index -rollover after updating relevant (component) templates. +To apply different sort settings to an existing data stream, update the data stream's component templates, and then +perform or wait for a <>. -If the default sort settings are not suitable for your use case, consider modifying them. Keep in mind that sort -settings can influence indexing throughput, query latency, and may affect compression efficiency due to the way data -is organized after sorting. For more details, refer to our documentation on -<>. - -NOTE: For <>, the `@timestamp` field is automatically injected if not already present. -However, if custom sort settings are applied, the `@timestamp` field is injected into the mappings, but it is not +NOTE: In `logsdb` mode, the `@timestamp` field is automatically injected if it's not already present. If you apply custom sort settings, the `@timestamp` field is injected into the mappings but is not automatically added to the list of sort fields. [discrete] -[[logsdb-specialized-codecs]] -=== Specialized codecs +[[logsdb-host-name]] +==== Existing data streams -`logsdb` index mode uses the `best_compression` <> by default, which applies {wikipedia}/Zstd[ZSTD] -compression to stored fields. Users are allowed to override it and switch to the `default` codec for faster compression -at the expense of slightly larger storage footprint. +If you're enabling `logsdb` index mode on a data stream that already exists, make sure to check mappings and sorting. The `logsdb` mode automatically maps `host.name` as a keyword if it's included in the sort settings. If a `host.name` field already exists but has a different type, mapping errors might occur, preventing `logsdb` mode from being fully applied. -`logsdb` index mode also adopts specialized codecs for numeric doc values that are crafted to optimize storage usage. -Users can rely on these specialized codecs being applied by default when using `logsdb` index mode. +To avoid mapping conflicts, consider these options: -Doc values encoding for numeric fields in `logsdb` follows a static sequence of codecs, applying each one in the -following order: delta encoding, offset encoding, Greatest Common Divisor GCD encoding, and finally Frame Of Reference -(FOR) encoding. The decision to apply each encoding is based on heuristics determined by the data distribution. -For example, before applying delta encoding, the algorithm checks if the data is monotonically non-decreasing or -non-increasing. If the data fits this pattern, delta encoding is applied; otherwise, the next encoding is considered. +* **Adjust mappings:** Check your existing mappings to ensure that `host.name` is mapped as a keyword. -The encoding is specific to each Lucene segment and is also re-applied at segment merging time. The merged Lucene segment -may use a different encoding compared to the original Lucene segments, based on the characteristics of the merged data. +* **Change sorting:** If needed, you can remove `host.name` from the sort settings and use a different set of fields. Sorting by `@timestamp` can be a good fallback. + +* **Switch to a different <>**: If resolving `host.name` mapping conflicts is not feasible, you can choose not to use `logsdb` mode. + +IMPORTANT: On existing data streams, `logsdb` mode is applied on <> (automatic or manual). + +[discrete] +[[logsdb-specialized-codecs]] +=== Specialized codecs -The following methods are applied sequentially: +By default, `logsdb` index mode uses the `best_compression` <>, which applies {wikipedia}/Zstd[ZSTD] +compression to stored fields. You can switch to the `default` codec for faster compression with a slightly larger storage footprint. + +The `logsdb` index mode also automatically applies specialized codecs for numeric doc values, in order to optimize storage usage. Numeric fields are +encoded using the following sequence of codecs: * **Delta encoding**: - a compression method that stores the difference between consecutive values instead of the actual values. + Stores the difference between consecutive values instead of the actual values. * **Offset encoding**: - a compression method that stores the difference from a base value rather than between consecutive values. + Stores the difference from a base value rather than between consecutive values. * **Greatest Common Divisor (GCD) encoding**: - a compression method that finds the greatest common divisor of a set of values and stores the differences - as multiples of the GCD. + Finds the greatest common divisor of a set of values and stores the differences as multiples of the GCD. * **Frame Of Reference (FOR) encoding**: - a compression method that determines the smallest number of bits required to encode a block of values and uses + Determines the smallest number of bits required to encode a block of values and uses bit-packing to fit such values into larger 64-bit blocks. +Each encoding is evaluated according to heuristics determined by the data distribution. +For example, the algorithm checks whether the data is monotonically non-decreasing or +non-increasing. If so, delta encoding is applied; otherwise, the process +continues with the next encoding method (offset). + +Encoding is specific to each Lucene segment and is reapplied when segments are merged. The merged Lucene segment +might use a different encoding than the original segments, depending on the characteristics of the merged data. + For keyword fields, **Run Length Encoding (RLE)** is applied to the ordinals, which represent positions in the Lucene segment-level keyword dictionary. This compression is used when multiple consecutive documents share the same keyword. [discrete] [[logsdb-ignored-settings]] -=== `ignore_malformed`, `ignore_above`, `ignore_dynamic_beyond_limit` +=== `ignore` settings + +The `logsdb` index mode uses the following `ignore` settings. You can override these settings as needed. + +[discrete] +[[logsdb-ignore-malformed]] +==== `ignore_malformed` -By default, `logsdb` index mode sets `ignore_malformed` to `true`. This setting allows documents with malformed fields -to be indexed without causing indexing failures, ensuring that log data ingestion continues smoothly even when some -fields contain invalid or improperly formatted data. +By default, `logsdb` index mode sets `ignore_malformed` to `true`. With this setting, documents with malformed fields +can be indexed without causing ingestion failures. -Users can override this setting by setting `index.mapping.ignore_malformed` to `false`. However, this is not recommended -as it might result in documents with malformed fields being rejected and not indexed at all. +[discrete] +[[logs-db-ignore-above]] +==== `ignore_above` In `logsdb` index mode, the `index.mapping.ignore_above` setting is applied by default at the index level to ensure -efficient storage and indexing of large keyword fields.The index-level default for `ignore_above` is set to 8191 -**characters**. If using UTF-8 encoding, this results in a limit of 32764 bytes, depending on character encoding. -The mapping-level `ignore_above` setting still takes precedence. If a specific field has an `ignore_above` value -defined in its mapping, that value will override the index-level `index.mapping.ignore_above` value. This default -behavior helps to optimize indexing performance by preventing excessively large string values from being indexed, while -still allowing users to customize the limit, overriding it at the mapping level or changing the index level default -setting. +efficient storage and indexing of large keyword fields.The index-level default for `ignore_above` is 8191 +_characters._ Using UTF-8 encoding, this results in a limit of 32764 bytes, depending on character encoding. + +The mapping-level `ignore_above` setting takes precedence. If a specific field has an `ignore_above` value +defined in its mapping, that value overrides the index-level `index.mapping.ignore_above` value. This default +behavior helps to optimize indexing performance by preventing excessively large string values from being indexed. + +If you need to customize the limit, you can override it at the mapping level or change the index level default. + +[discrete] +[[logs-db-ignore-limit]] +==== `ignore_dynamic_beyond_limit` In `logsdb` index mode, the setting `index.mapping.total_fields.ignore_dynamic_beyond_limit` is set to `true` by -default. This allows dynamically mapped fields to be added on top of statically defined fields without causing document -rejection, even after the total number of fields exceeds the limit defined by `index.mapping.total_fields.limit`. The -`index.mapping.total_fields.limit` setting specifies the maximum number of fields an index can have (static, dynamic -and runtime). When the limit is reached, new dynamically mapped fields will be ignored instead of failing the document -indexing, ensuring continued log ingestion without errors. +default. This setting allows dynamically mapped fields to be added on top of statically defined fields, even when the total number of fields exceeds the `index.mapping.total_fields.limit`. Instead of triggering an index failure, additional dynamically mapped fields are ignored so that ingestion can continue. -NOTE: When automatically injected, `host.name` and `@timestamp` contribute to the limit of mapped fields. When -`host.name` is mapped with `subobjects: true` it consists of two fields. When `host.name` is mapped with -`subobjects: false` it only consists of one field. +NOTE: When automatically injected, `host.name` and `@timestamp` count toward the limit of mapped fields. If `host.name` is mapped with `subobjects: true`, it has two fields. When mapped with `subobjects: false`, `host.name` has only one field. [discrete] [[logsdb-nodocvalue-fields]] -=== Fields without doc values +=== Fields without `doc_values` -When `logsdb` index mode uses synthetic `_source`, and `doc_values` are disabled for a field in the mapping, -Elasticsearch may set the `store` setting to `true` for that field as a last resort option to ensure that the field's -data is still available for reconstructing the document’s source when retrieving it via +When the `logsdb` index mode uses synthetic `_source` and `doc_values` are disabled for a field in the mapping, +{es} might set the `store` setting to `true` for that field. This ensures that the field's +data remains accessible for reconstructing the document's source when using <>. -For example, this happens with text fields when `store` is `false` and there is no suitable multi-field available to -reconstruct the original value in <>. - -This automatic adjustment allows synthetic source to work correctly, even when doc values are not enabled for certain -fields. +For example, this adjustment occurs with text fields when `store` is `false` and no suitable multi-field is available for +reconstructing the original value. [discrete] [[logsdb-settings-summary]] -=== LogsDB settings summary +=== Settings reference -The following is a summary of key settings that apply when using `logsdb` index mode in Elasticsearch: +The `logsdb` index mode uses the following settings: * **`index.mode`**: `"logsdb"` diff --git a/docs/reference/data-streams/tsds.asciidoc b/docs/reference/data-streams/tsds.asciidoc index d0d6d4a455c63..1e1d56e5b4d93 100644 --- a/docs/reference/data-streams/tsds.asciidoc +++ b/docs/reference/data-streams/tsds.asciidoc @@ -17,7 +17,7 @@ metrics data. Only use a TSDS if you typically add metrics data to {es} in near real-time and `@timestamp` order. A TSDS is only intended for metrics data. For other timestamped data, such as -logs or traces, use a regular data stream. +logs or traces, use a <> or regular data stream. [discrete] [[differences-from-regular-data-stream]] diff --git a/docs/reference/images/index-mgmt/management-data-stream-fields.png b/docs/reference/images/index-mgmt/management-data-stream-fields.png new file mode 100644 index 0000000000000000000000000000000000000000..605d49b80ab1fbaf0bc65706fc740f236bf929ac GIT binary patch literal 251799 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

h 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¯v1^*d49M$)p@`;-dvv4oAqATlW955z+_ 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%0A!=M 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

czuOu@T&B;p+i@?3Y4Gb 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

265Q?awi?xc7l5 zfdeEGdFu(ob0$BiAt2?yP6<}>u9JdA`*)HipU~*^&s;M`g|vX1K%gAxoqC$-K^HE7%vkq!gB=gNx&nzgoOlccZC98UPLC#aa z5NI;0Wyj*dBw@QYm_GR(H9N~nj0_zd!(JzbRI6La#qF;@>*ov1%w`zjJH2(3Cs9g% z@81p6IQK?gKb{I){zo~cCt7qW4o=RPv0)cY@H zLN>`VcNH>djD2_(>-z=Py~n@d_xs0V_Two%3tR%NQJBE9Q5kDW8`DRz0T@1b5IlT5 zRbCpuDsLWq3^`21ABU!uvlhu|SS!Rol%bs74ctQaiMPNGfyOt5x9E=^*W5)#%Bo-&SLmBt*E}tuEQ?8?)sfPZ?eP9#aoSCG+$|X{KFYIw8^qM+$=Wd{8-$}{ zS`!aGc~%G+>Tw@4M}?eOZVubhVzL=zk5lSL)3%FzaB1c`>_O)ypQCDK&ae|vROS7!mnj#|qocqEsI<@HQ?_k2 z9$)1xiqo(_HLY^aX3TT2N1A+AChm95?W%`Ui})&tl!lhCUH~_d{^Yqbc;%4u{cnW} z%9ER5>t|;150CbT289C=@&=8(f^@WgR2nGhn{-?BWrC-VTe27udIHiH650%K5^|M1rDsmlOHUjBIVf_&wJ|`O&5r@82vD53OVhb27(2Jqq4M%GGNJ243wi zf5fYxS&8X>SxKg#*-!N4S441^gAAF{*;eHn=aon4oa!7`vWQKo*hTMs=Tk`Fr^_~U zwD>&N{09Q-5H*HqE9a;b6^#c}0@5D+Z7PjavNc`0T(HQs$WlS8XC$4T z#f%i#VjoPgz%#U#pi!DT2?#~IP7#TIF>3@K=`O(ep^?7xoy}5)r4(J){XMc+3eLZY^NGD4#BfU|wq+o)>o3~egxwkpmHb|G*LvMD zJ44g7<#>Z!Om!ZP0esQ!(XLhv#5Wz$N=$Ej7k!;oWSKy@5gKvq*Vm>Yq|=f-c!0vC z-?U89wEY~sLlZJ@FlI2xruH~d^lh#9n@`DUzUE~2&i>Zwna$3G8U_evc2Kk$-0kW8 zSZH`EBxJXnEJIBeSValE{PyKV3ftt=AW*}89rzqsIiPY2d$puhW^^?n;@eKn*$Fvv z489xOiviSOW`igHdc@MC>3-kIr)wK@wdw2~rtNP^D+=8G?)alm;I(oD_HEY+G_$H9 zQ0wh=olO^mb{;V~FLd$em6@YAMfcrqi+ncyGUF2P;jQsE|M>)#wRuFh_?>09(A4BN z@xsd5iWTPDHeuU=CdOt@2u7JrOkN7cK6%AoB+c{ zj|9~nm`(dcWWs$LI)m@)RpS~xcTo#G-Ic$8YCMRO^^H63!7d{czNgl)E<&q zjC?!}(xQAxhst2q&^^Fi>(Mr#t<`x#MN{%_TgoxqNQhrfGt2g~lq=ne@{~Ntjoczm zB)3krJ^5~+KDooA)yXe_k<5HWJ{7061AK}(EOt6jX{l!Jvz4v&;k2&Qk zK&objQ`de8GWpY%`NN*r7;WeD+mTK7_1)b0`Ov0!#v&_v-fy*jMNUZ7Ux!S=wPa!Y zbG&s60wts+pKp! zw~%9Z{d|J7;y=UE$yz9bO@{UA^|nMGUy0o7b^m5ERjPi1@L}R(L6`8^mGEpi;__rDMKwO=+K*qkyBZ^7{3`VUFHb zJUTy{SzrFzF~8uAB($X}!Q(=AbAGa+x;_;$XB~oB?MtF8gUdk6>_17y?Kb`{?+IN# z=04`7qi|x$i;0SyQ7ocX4M9JPi~%U)L|?>>r(Ag}LRH{Qj->enx|{jpF11 zI>zYlHo+yH=LhWa*l~B4=8G=CceJ@mZs?O}_8>VqV4mEMw<}eaoZsn7?9x<;ML~3! z!&HArT2gtJNmX(kdxO8g{yJBq;oSkv$td=?TSlf|y|2gwiPL{KR*VZ_9M$ zPv#;%gCVuo~4^9@V^f2 z30vgWoqbpd7dxn9&|P?TU#GX3LY2dLE=A&G1V8dEU4g`I1$_hRRoz{C!HB{Fwt+=b zdhb9@_ruOQOw44HS|qhPMY8mh3X6e+VQttoPb#XQLT<_hA3tE2H*22}#LZ=Ma-YVsHhE@6T< zvjW%R^7n^8D?)Ftc*OUilCVSQ_OMm7tI zqK?w@-9TuKBqfw~+2ifoKOuI}n&-S(G1Xat9C2Yagt8aH9O6de!0LvRpQ(SBWC7cH z>8In}vi^tBC%!nvz}U4MUf!1^*J?dWX(N^!lNf`dc^StUJ7*5HxL;t2d&``wf{qpb zo7*+x?1N7q>usm5@&<279u=Q8+pLjYWY-+aMH|A)NeBH_Gb7HuXzSCw$%Wi7)I$uN zRS|R{e?$Tn(DNh?VyQh5t%i;aCC6u=$I4^&dpzr(qRO znTW%e%m4V{_CqG9Ov6s-4#S&c?w5sKmq8!) zbm{%Umv{AXfA=()S@Rm`8$JsSeN@S3@pBEumfCJ=k6$>_rQ#vm8M(Hg*eUCGw(X5Z zQ=oKthQV84F1W*oKgB$p`UwI<(HVfN(aezAX@F{jpm3LW7bT3|FJ$2G@QcjS4EkCr zDRoptq{cWk*IQFl^L<>I@z;wzH56(djkX@@yO=&kIPQ~Fc2`N}em%t4O=-yTG<;m- zUTBOuT6QHw{PShw{$Rt~E9aJGn%Fw(`sD-8283ukmY}rL*>v~#Va-$L#}kKDj;~*T zbA0);q~Z|=-w~l5Y#-n|{0@A44ZSlsXekYzZomYQWWdM^Qz^plP)Lr1n59a>*orma zr?g7zq-3?v83LxAGdenxqub;BgPwrLm70S2Sa|jW2S{G?nkLskR^0E?9vz}T5Tz9V zAAQ09-#G+~-Db+|oSycfR6f7?^Q!TM#M{2H(8ZV*hM@P^c)ndi2nAb&vCry6^ZoD& z4qb`JMFF~w`}g;ZO-vjBaW?P_@;aJZ&rwg+zSFV^_87J1NVZZ)wG(>CWaz$gzo5Ju zrnxz5cI#UC7R!0MrFaM^)PQ0jU@>HoD`L~#kqtsq#saW$ohETnzDDh`o1D(qs5?if<`jekhUJOpBe zB$nCZXqvB4Q@pGQqi$!Yzpx4Uy!h;@G=Uy z$5X_-aYhXuzuIFMf29{7On1M)*hPE2Rtux5;#)d8X?3=_IJ`u8=1Vr-H5Jx-Oy~}D z72c|a9YlnNMu@4Gy-MSs;#?07)Ga9dy>qQ!NdV>hR>D10;)@q*gPJV590>r(V5bo5~6RurQZvE86Qc{>Z?O6@Et8nxYiDj;N zYI%Y+nXLUL^1bf9P0`@Y0*Hk=ZFu}Vc4u!;7+l=-JQ3bp}dq`$;?I%UJOFaMEw|IP0F7Ug}FOZo3P~v z>Nbn(U9I=>#=2EHyf7IeJKSu3M!P@p)Axgx<6%69pC5vv%)7qpr+G*T1xU+m0Z;Gex2_O*SIkX8>b+l ze6G-YS`>5L)Wa{dpGCbM@y@H1lRK#f!sUr~md_U#%WO_Fe$bwpwC2ESe4%x!eXM@f zMt>dCW?CkAlwYigO?YbW^h(_ZC<&{lh=02XIQgf;O4)ZE#;TPM-$^bky$$fwb^F8| z&s(6gOj+{Ib6nKo?c(Tl)7&SWukNqd%71h^-Wh6X>D{$Zx+*l?4<41XU|n(g@I_2S zWKxTd$J2jdR`AZsmjh{JS*YBVWqB7*7cW_(^$Yx1@+`m0$K6cW6vdFCogx*s3y%Z~ zNJ9vBpECh=GnD)(#?xHGPE(?%kE!TZ{jZCPR=CDFkoz4_|8angrrx_z(t8C6NUzGe zs|M>D%e*+xgb5~rmA{%@eO4(5- zq&BZ`eX+?mtO7^lB%lhuJ~le~VJbjBi#`ZhHApE#(y#lhK1i*8`)~mwW1e_X;u=Fc zC(aJ5&a*4GNu*eLuLFv|b?5vwt`cjl}S6K>DU&W=_uS3QtX^@~jW%N`u{TOT5Pqd4uYo#VZroNI3<4 zm0kkmxG3?<0<$>0>prwL}99W^DjG6Kc9wTr$mQ;$}U)kmL?}8*sBG? zA7m824dRjFX0d}e(+&$&nr+)vAX}pQ(7YF2y z71vbZ_i+%@#Gp^5x=Q`RZ+LwD$t0*x-858Vj+#ckWerTM5zSPkPVXpJl|tZ;{QM$q zerd6xILofXzYlyBxm`4lJQ5il>Rab zlL%vakpg*5nUIr+%QTJf%q*Y(4)X)Le zEe*u?xoG#rvu@6a0d5W%XXazxJXxFC%!r7foMaznSG{%m&d`tq9k(vzI2882sVlDF z#4&j;nRbQFi{ricwyuytj|<9yL{^zAOKBe&>=$?)V9M}MDf&W*y6XhDQeopKop1^S zxjan0J4jn=-5k#I^r?=Ug4a&GQuDk=NtH*0l0{e-TUSXtLgj&K)TQJ@R_iYyna(#~ zlM?@WJY6hSQqD)&rwfg9S=wdYI7SP>_7SoC) zsmTkK8u}M1T7TzO8hDAIJQ%hpJ)*Z4mEm6fCY)=3{)Ez>!Mw7A&~n(_lZrmd(NAQK zkEu{OVrY(yx%&-tI$BD@tk!Lji~qj7+l;~)MZq6#^|yv0biOMJ?nAe@-90dRIHsvp z1;bMF%H1z#YdwrGOtabIIitTaOc6S;S?bUp*Yye5S_)Y0UmlxSKr{Rw#?Jex>F90K zAtChMJ5i(~NRR;1f+&bo1?fnnBfWPBK}4km=^aFhN=LdtKnMY;(tGb+dVllXncdl) zd3Wah3o?@*&gY!xx$gVg7X62D%;VYFaU!Y~Cwikg5Q_*?DrmJuo`<9Mji-z?Yko;C zp++HB92}WS{RIA9&(&}Dw>W0S01ek=T*J{}4teCr4196lt&RVu*F!2uzcWQ7>L+)^ zURaMAO8^oJ3{iKvMrVW$odtP&qvK#~A1AM9*tflS3?)csDJ%{tswL38?kd@JCnF99 z_kV19^vk=PT2m0}Sq353#9qisXg#>m0bH^WUtaiY@|O?d!kar;l+p*Z)&Z8X*X-A8 zMuLD#pIN_%tmNo`j!`~-?aYTC;wu_AO%omBV*ryCDxZHm0xgTz9Kgiceku3QmiBJX z&RLpKE7Ht`dV_wcX8q7lOr1kcvxMFJ+ckGw;&N&ozaF>rSO0{0k`;d;0TXuyC7H*Y zZm#?gjSKlLab7K$^U9M_{>P6WQUOSpNBOqGVq&$m&E>0%KQD`a+dNSD&qW>Ua#fz= zWIL_-xwYYV%TW&NJ)knodiloTtv|MEt&i=i-^sR&lm1R^?VVh)X>v17U0F>Bx_8cA zo_4`wAE>8u#2?`V8kKGY*?*wPgGdl3VL{w=Qgi-8qebQ7#V;0kp5`fKx7Frd*Kq|^ zH#hf{MpCfImj8+B{8<~pxR$WSeop(-YoPPG<(eP3FgN~iGRtuqIX$jR2K$8Kd_p~_ z9+Fdd6?-gj!3&dQ8_Cdzg++hPm>U`r<{a{WF#_JeyRjGQ-E8zO7_1U)ltDoxQ=Mxii9r$>!Mjt@(N81x$vzdlSw~vAyc&tGfWaj&``K8u(_8Y5b#b z<*S{|sSM$~63NVPn9D(G)`6rWrBcWeNpa)#;YCQwR?XYr;4#af^ZUn=R!SyJTYej{ zKGSZEBlHss%Ja1kU>3q+<_*P~J%uZW9JMv=&Xt0ofvk#|Ex>?6^P`h+@5eE?(3*Dc z$9+dm&9P-|M2*H>B1MPA6Es*mM_zB%Ih1&q|K2SHj?;4I=xixAwdp8bA?5NAO93Ol zD;CI)O`2NaDNZVTt^b~zq0Hek#h1SAafJ!4O_|bjdKmR(_t_^dalRMY*c({%P?V(O z@WSc(BloEquk_8bc8W)_k`bqKA^qE|>CW|vOL9)()T!V6@IjEhg@v*Ar0$4|kg|a=l=jU^dC9;1{Yz3pDhpBHFrLXY1yBhDb%vY;N&xU6_ zJ<0_$1HO_I+GYR;Y@@UNmt;Nqai)qnnl2ez#FM;<4L7k{n~XRAqxHs^K2V$+M+|YAHqc95*1#RB6fCdE4r_EL<}SqS*82XNV{xvkQDv|D9^y;MLJAi25_$; z>3-}6c-1akZuX{JJhDq9rK;%9xd{AF%@rAL%%o#KEU9k~alO&94M7?_gSh^il+x49T_tBGt8eV& z)i~4`het?orv)_fQrLi~Y~x<}0<&)%i1m39C~}mroJ1e+IzBW0vCejX$W2v%Hl~_d zi{|ScJB(!9{ZMiyC(`bH4LCK9;JgC&=dN66@}6+m*>7q^N4Abd=o-)(oge<_4ruxL zn>g-?fAUO=q|GnP=ZPyrX`b=Z06F>|KRhkmr#J*@O-DIgTHdr=@7?4oZd{E~Vy5HI zIwjFl5z@|t_5EbaH|^ElLOfTFU;gM3jr{u?4y-?%d9E}qoOH;xiK%gwar;?wL)EU? z_e)ktD%wlTYR7wp!rV+EU?M-g>K%H?Uer%-A=fxH*z@*%fr8JbXS!_2x8KL#ZU-VNp)bL`1r60d$Y%;Dfy|NaUv-Dga_XA% z0;5n#=`#~T)qO`m-QN|p?LKm70`_jXK=oQ*x{ zwV++?!~POsu@|fVc>PByYJK0J^9_>6C>dgC9b0MX3u%&_scardH0j7%ueg*~8{913@k@E#dL&23)Rc}kpa!{9e=v)m>_bi{YfQI^c3MW?BylQzNgXBhbNexzo5}8i+dFyVqz>}lC_d% zLb&?F^Q|sCKs6SlB>7=dR!ZtmNr?a>AHv>Dd!O8f-X!Up=hNwfL6k=v0LEB|M;N9LhG;E8qP!AIlo8R;Aza`Ey6Z?c?NlMk@+lz?PEp zkgTUm>Ar`rbhUcK)M2tbP<|F_2!4=S0hDIHoew4UuAgFjM+D)KJ|7&ckHUY}{z-iP zsU}EAill);%1mlU^Y0b19ND9*uwQ?7bHT7CJPbX&I$EwFnSJ0akj-0q8cGIDWJ;8? ztycD+A)`a9U-q4nFOP5D1(G4Sp@FnVyy-j`w&wP)Ij;k`u*Zq0NjV(lvZRbme)0E+ z`%`P7VZ@ZKxx1$EwFc8)b54jDvCdd+>>Ty03Yr@F~se({b8 zl>*-Z)lDMn=vrHvuc}?7ZCo;)0i zP^2MkX7Zw*?_5dZz4}iTzAfCjIpPYo!F&U*Vx&ZKBn6Tg@Ia6)~ zSqmte%yk`K=wEJ7RA$zQ4SW_KgAx54noCL>v<7SfBy!vh{>}A7&l<< z*X_+~r%x}45Q*ttX3*3n+r{j)?(7b&0oA5pv~?E6Nq)ZYDm+p4q2fSYnG9f`C>6?NbfUtt@wn;bhWvw(gzX0n*!to4LxP{{r#-^izKe~!N=>bR0NTQf%;Ag!>JZ-v zz}syRZw?Ic0Qroq?){)dP51Hjcz!K5>Gmn^tHXn+z%>Qyg%XA%u*j#KR&4p3_3=SV zeW}fWA23tE@^mpO+V+qy=zBwh4b`(r+v~)7$XZqEJI72D-XW8b&QNtf(9^K2=$^!J zHh_KQ+&_&#+p*Hi8N3rG!80H;y1Ex>UOD2@d|uEC?()?Lnhw8r#Znet@TL@5zn#*nAl%A=VA>H zO3#zP@fh+O+6E=T*3xVpx8?0CW8<1dMpo}R-iM{x=6HI0vjlb;w#%^;U@Kr3c>?_R9RQFMh5$b3;DbUl^Zi);YZFBU_HXe!$UCkV?hI%nZLt8Tc~z=39ckrvo@_5; z7yR>(d^F8a8Q8FzQ$k=cfvnYEy5o{sw^L&x*=JtecFZ~?y2;fexltA?$Rh>udHuQ+ z+xu{&c4D)esOWX*CZ16K?htO0cVd*U5Z?0JnC!H)w6w-Qp0u_q5C2Gx4mx8=ti1md ztl*IhGo9zp+dAVr11W{1iE}R9{O~k`-qq#zj0SpsG+PY7rQ^}LFfV@CyeswS?)q zH7-di_Q7bmI30I>ZL;GuYPH&kWu8SkTRggF0GH9M*0M%F0nPg7t@*hU5nBhV?7Xp@ z+BiM)b}av`fLI%hj#YS6uvPKRMr90FL7B)doPEVYXI@7xZL&O9`id* z4SF1-D(TW11Ccmls{fMRV^V%+o!`APLi+mS>MEPdH&FlY)4ahCU2`svEJY};R3*2mYC za1$pcPbghzwk5gtl`}>5ufD0zR1MYq^!K{w{V=idMSh^+s!d3i^7_DBsv zSkL&(GBg#^7IM`YGX7X1vPI-5faD&@Kxr-53ROo)AV>(2P7c>%%9WS* zFP~}xlSH;FbGDhE8*TF=x`2YMVnAgPJt;f4_;9m6;>{vGC62` zhR{21Gzea}Qm$4uzSU-66J$wP<(nuS*?|iMkBlXhtl)KYRr7$cC;5jP*`9#*a77+ zoa~q?VhlPMAR^?ggOMK+su+Dx(I1Y#_?twi;j6$@EXX0kR<*A^@NR7_4cDQVp84=J z`@ALncJF-c>i12$q#Jh-z8fa=N*%r%#+_9WHC>VkBvSyni+Sdw>eTgCZI@#*>1jx~ zSAN2GEejhcmv{GeU3D#$o!x@p3+oiDrAsjg>b{A4y}~AJ=iPoxDKzzZ4xRXowY7K1 zO;nyow=9SK=>9vbTR}E=tf5m&4ZjssA!BKDsBT=UakoB?sk$_G z+VtVhcJg5s^{IOeP72Vg#506~9!Cu$>Isr24-P8j;wcw(pzgw)GeJp84P`C^7dP8qaFjr9gow2A}Cu;H2L3G*gYHT6cz+GVmR-(2%anEBOpDCahc%}v)jo#! zq{4UUK=pW`*;~s6lG;NRm(xh@Pw=rrtp_tlMVdM?V)hBk>^M zyEewJW;w-{wIb4re@IutEE($Vb3!G&^{VQ1_OuncPNM~b(FmR9MKL8ewuJ++%)E6~ z-GsNZt%ti7P7$rAR=9pOZP$9C72ZhGj!%!RZTcC0C46x-(W0@4t!(unXVX_vO5E-N zSNgGblVsT_y+v3I$?M%8fa{5#aXx3^;tR#YS0JAFSM<*)Nn%7S&PWLFxilNi7h?U9 zP}EN=Rm&d20R`WEbo7#yI0pxZ`vmX+P=%ZTw2-)hS|sflquIRK+|0^Co@5`aSO%wO>6%Ohk6soe(!)+cw*ND+BIBBN}Yi1^V2 z%*bdFr!%;k+DAT<9C3`-RXc{-Yg8Yw_Vc#3<+?l+0eF2@N4OB$9%@}@{_(2I?jb-G zka9TUyiVKSN%SOe8X(7OGX3EQI$pOwtQ#s4r?@tBf&mSO^WeIhXJ&-zqJot&2knuR z04l3zq?2OUO^uU3A{&9R5SKt1BPEbK2Vi>Z>=TQ2^O8e9SZ ziBrEZT~P$Q>v@6n8X$)8m%oUR8OZdRvA<4~Vg?lEf&ik(++Q2>#egT!9rs+`f6QD# zdK2#8_h0C(a6}hG(%?ygCvCC(!VkSEFf5pl^(-fhO;)96w$`C*bB0e~&z4tKC$0+J z1M*ux$VnYT$oEZLT1vzho(kO~*19Of^)vl&G6uv?z}WM0<5bbPqkjmx|gW zlPpFZufV=pU`8A*T~2q)!d+RuC&x$uED_?R(NXrGNw9Rq_sw4=s)$>VS!T6J z7of*sw77b$Kny^T9Va3%?$xKtz0MuHtC?Bhh1-n0Ao+g#iu$5`Efnvv0Wkd2_!&N8 z;!QILuB)aB|KD8z0XR02exiX$v3JctZ$4hWw4jxMaZl>W#caTm`KU2D`k|$O5u8<* z`&w(D%upEKS6p)SB6xrt*c0ZJ!uscP^la+Q4~4+LWmqthE3T#U{Xg}8r_x2rN^byY zOKm$3EN@^s(|Lg7Hli0Lm99bZJnG4pRwC)+vfc(5`(e?NTl9~$SK5vQUTYIWl1T)g zhx(36>>hrx{UA|(od#*7wzgdtb|m>4?ARuxXNCr|Q`I^*sW1HqR7(^C)`#-2nn0n@ zUR9@*upO2u!D0!5X}^iTu;Q_D?{ICe-EEn%k_uKuaiMJSx;=f9NI@ZE3w9|(cICPK z_zm5LcQHmFi3)<^xq0p{b8i2X2c@tJ@%-9mA&sq)=aXkeXN_m_R!v}dA!VV}?}i4P z#gt93&_BYo%%m8{I_t1=-*u8lQFHLi3MxZgU58VPcrtI$W!sdPp?aG_^xG=l8;g0L zW5a;21qI(IlhZ?b>Z10rl0oK6T(_J-VAAV64msI~5N9!R^N4~2S zDf6=JE7QjVx2d;s_nIY$-G3+^aMBSxD5%`9|9C-i+-YuI+9v-<_JFsVH9#%S35%hQ zeNwH$d%aw*qWUxV$H8ZXr$kOL8wh_^Ty$*YL)ACI?{}LI{PAOPo9C@63`H$V$#+$+ zY_BxPcEVz#IW08PWvGLxPTN~T{#|N+FH1`8{q0iFAuwus@l&}4L6P4tu@`U~Fi>JPFzxSzPB6qYOP-`Nml2MmI(A}K?TM+{~T`&$r)zN6F= z6e_Y$cxVIinB2*X|E6Bmi$Ob9VfL@zQ3z6R`(qI9+s>Aj`!-~-#g`G{P7kt@xWlx& zeV=Aq>Jo|r@h6JdHPsv_=y5Q<0|0T>esy^m3;zvF-#@o~*FyQQa z#9bJT`LY@fkwSekQH^ z1_|NlrxtzSKE#V5^JD}s0Wr$OG`r(vPq;&A?;;<0p~yTPO96U=?LqjS4l~d( zqZ4(8|CQra1(BZ1r?xq~mF`vL&&@*Jio{$2N=#Htt)#wpKnUWjL1J5y7<`|mNEJ>C zjf9BtegVJPY-^JbWkWF!XYLu>@PQH15jn)I?$j3aPw=6T28c&6A5w*8F+%Yw7d(F^ zb87!GjaT{iG`NBsu$1Er{*xEi9G7?djod(30ORTJaJ` zp13y0A>JBsnGQMSN4XNz9}8_n=~*v}>JZt$ox$PGfck6RdN9p?>LCHntT1Z&x*I1i z^ChaZ-d@<;o^k5u zSPqN~R|}#C3v!ZB@8ZcyI|TMT7SVv$Yr8WQvrcn0qa8wq5AundM&;HOzHKsOOfC2~ zOqyxiHvZ{)M%&YbDJc_hH-ROQ&~%Mm^D+rhd@O~+1k-aFMs>d%aQvf%Opi(NUT1sL z>$;Ia@XQoJXp56_X?2si(GSL}&EX&8ONr+KZ@L-9Oa#y-Mum~&VBJEp*V@(UJlub5 zk5Xa#shC9D02}?WtdVQm>HVTz{<``CeZ*t0AK-J`hexIJ@)x8%^0Mq!v*7P}?H#m) zUe0PtrHvj#{Dm>|XY6OU{-Z?zL|cUtgR3)X@P=%)DeA zPGxkJSSqcdZn&r+@Y#5qENecu`=}u?)Y}wLw^>>GYy1_-s+O9Ts~VXj!B+tMNKlsk z1X=8l6V&go(@QNeC>_8?FW$9Y>yt*L$YcHfEB zR=2Ss#e(APhFjiNeDAfbEurlXtFK+7UwIpj9g-TRT<+6j;>yM_S|m8G#HMd!=heVH zK1Kn}a*=t5d|_*0ai#@fBs6O@L3L845Pc{-!XtZeO+Lvv0lt-HTw-P4g%ORY4*$bWP2n{~GazujK_90TuY$v3M-b?p0aCXeG_^OMU&HyKd z)tu}DMNuf^$l}~H5qKxG9EXXzC4NBo>_Z6QlcR&q?38}g9MBOJ1-PwooN#3C2BI&) zRUXQ}mHlV+!sP$_F))H*GG)D4>KP=apHN&bN?^mqN1~#&&)z~Nw8G}RE<%4 z6j7uJAI(1oHZ(T&)G#pKmxRPi6#?1-NjZ&HJyXJHkidN{?+?+RkoKm`A+!U?o+D)H zLFEXB5?~0z*Nhck-f`JgZ^=D7S>N1*nRWN|K_?bZ<9O+P*(Lz1!{kw|=D{x@QakW( zhGuVQ`24B1v8UzH3^^nKCFs`Q)uR@S3f8d35>5w;CpZ9rCyDNal6PuzNae(cv&!d) zfpCBTcWRg?NBm8E_%;0ocwV8Op$v9M!e^ozYTaXQkKDAl_<7;`X?`YwVb(Y-B=c>M z#QstkB^tM>8+08I&^>k=&{HzdI(KtY%jdD&7uX**otT&iQ#|~P@g_W38lt83Xxl@= zK|%AjSMBrnwbv85^}0lEBa&|*Blrt+U62RSNT;o}5?L08eklUoFniM*rkr!@{53-jbj=%zp&Rj2UdQjPqyL7` zl*o*YU#ro-c!4QV6dmIU1qC+N#R^VL$@p1U&;%S)2BZNFZYE$X7A=9S}?)zJY?0%4iN~^^{6(t}j!TQs5XOg%39CJZ5p9z?sfA#@5!^-+h~0W~%$WI2rK)#**X-X` zX$)UF3fOz?)t4s82-6yPZ56gM8%1y1pCQ>8$+a)CzSzepYfb*#Ylfqd-|63%Spj&E z9Y4f*^Otw8ac4SZ5y$Wv9UoQT^$gIo?tLXqX+YQ!^V}~rliCal5qrk*KwR8@X<^Ex#nRz!)PZA-`ilTJm(A<- zyt%lWy>hp#ikm~YGZe!U`?!R3ux#MPg@W~4AzzP3bBBZM;_G31N9*9x@$s+K=|tx& zAfkGnAs4|ZMQEowk@4y!GtxlIW3o?(VJGbqCij_q^iK3#zQl-=`+G zE@JF~`{$5a#n={)vpvgGP1S9G|Gbn*0dc{>QBkAp_T36`w{V2jw&0D3KPr2#Q#DX0 z5O%>5l&a}f9P~MweLdmOP}iD*m6NmL@^F<6Y6Lo7G||oGJEuYRnx^}`wy^lTK?aw& zj=K3n)LMk~-7Y-c+8<oaZL6j?TvH;3U?;$l0;bBoOcVO5iIRtM; zmwX9v6(<1aK+{8@ci?_bpwF2m03V z|JkcJXNA?%;m_7cr>Dt&HBM7CX??Hs013&dn$Yo!`8Q(_6ViLpw{24@Lc+Di?uk=h z^L`5ZbHn??YMA=ipZiyDum+$qEe>hH@|7oJ7c4ZVs+Ea15Qo#SH2Ot69!NNIbG>Ko z-f}S#lHw9URqouZ;}N)uIg33rk20t#Fz^@t0zbj>Iq8kn5$JGv?ip<{M+vVNx2_u3 zDi&Qn-{4V$x!3XUWu20JknLV2cS=Ha5r}bdOIp zNgV;dh}~Foi|eFErKu9Oo{+!Y-C@_wQJ&Z_zWk~R>@@ux8B0sSG_=OF+S72kTi-Qf zR^Qk0%%$Ks@K_Usij|%AvtHiaa`w2M0oD)27Z;mUY2e57o#g<##Kg`uH(?!^TJPWEQC(Q7hzTU+v;5NxNc-=0MfxiRk%| zA1HfEMfK)g z`;K<<(mro?mr;j=#3orqS*3qb%!nP+Q%v!X(mk(E1$)mm(YK&}y;x}EDZUrpKCJCx z1=YtaB;lVl@sqZCtlabbcdTgRHL=)6_0n&Y6A&G+v-zPiUU=$k(aJzCrI=#TZ1*VK z#wWj7Id$Oj)t*w9BbFB{G*tt;vusn)pb<)B|BY2tl2lA`eq4_PFI3ht1J(m&(d5-%sv5gp(dAcr$G69+%j zwL~)tn?K-W{p!alUoII8LV{pSnAM=@HBl8!^Ct9 zx1tSzO(_8R)pvxGhFCyMxt!cmfVCM()a6Tp>roNL@B;{dWz*c_^A(+3)eWF}y05z% zt@_n06n|2#V_sV;%CbAY<3aTL;mBAJxs@ZIdD9F`Bt|7p4y6=tfcNLs_YddDPQFxU z{AoQu(?U)Yu7p1GW$(sMN-;96v`2*S#aPAjaj1ZKd2#*Pa-rAM@jgH9D-lP3lia(u z)2_SZa%Q@^vOe;*11f56AFUM{b<4#GjubbJH>~(}wzi~9l2haCTlYp7f6N2{pVg2K z7v2mPY|0_K35Gy$RSZf5{3q_Z5OPX(7>7*O%8(Ys%%_}(SZ#0VC&^9{DjpOncKBRp zg^C?FFa<9XSYMt%q+DBPx@m|RdWDC120!e^TN4BWSdjFkYD`+opd)~z5}$IQ!4b17 zYTFf%?noDS8x%VIR(aC7lgj!p!;=PMH2C-qIcM;_n|nKd=jMDktytK|0yS>lH!twV z-;bsE7)VUR!0MW5_wa41*G4kyXWnMso08D!&sCu*s3)VvW|lNGH_`y|xnuWc$Tge` zUUJs=^Ukr2#$C$?JH0j>Ii6H_E3Se0HyD5j0CJ@pXYi$mqx`@gWH6hYh$H)XGx!t2 z&U`W}1E#K!d`0}UOZQ+$@M`L^Y&`MjPtWsCCGMr0g@YA33{l)2cKQ8B@1j3GN%*L< z17smwU$NeB>NKS{x!iBeApM4#O4U>UbV-TI z*xcNt{JAPOuM<=4U(;!A;=MclX27u_kc*vI&*x%R7Fd!>4{RY|7WcM(8%6@#UWnkT zlz}2S#t4gfc%=HDjXJJ2_I&gqJ+it2v3$Kh0SEpf{@U?t3i|8qHDKb7gXbDk?^cER z-sTO)$`KE6ohkGa*R@YkvgooZv!3n~AV>Vc-zM=&;rvw%H{^^EJd9jSzu*9>D z3Ge`qRaC+xAy?xyC$OQ@zCO`_MzRTgBe90dtVYe|sFI_t7eT~x-=06}Dj>NscVzHt zI-L!0-0Ak5h3|@7%7^5Yx~Pv1Gs|k3*}2TVd!=9V7wa5ckh!5at~t8EHeAMX8LBLU zmSk5c_X+GJZzu;2!0|a3yoawIvtxG-bZw$gHK01QTs$ zec!F(N(=kZ0hPKTbxCOQpS*0}N+9&L!CTWde5}MmxO^w!I*tCpYOeV&!BC3ZanNM) zISIQBtL=X6#PILx4HvaEms}tA;jxn2w;ItRv4%m&Z(-5Fp;{Tqd(9>jH*>*ToK6}P zR}%o|=!W}jVO=S){hew1Y5>b)T7V^N>ZK}ni`y=+K|7u2m-e$cCDAap-#Amvxwg@q zM%Gle%X`Y3ouMHI7Y=1pQ$&swLxz~^u@V!9lTR80XM8s7E4mM|vi@SZR^LTYGK;+) zWs6_tO}XsiZ`sQlkTxy7X~#lA^&@ghcHy$kwXv7)dMokt?|);yTQuymhUADmnRJeZ{0J*j+tMyMFkTSj z3!S!9|0WwGKmIPGv}UA$%_O`3bJ+qWe+W)RE>i=0i&^(_{hlJ8B>z5M) z-XUlH)0t*+Wv!IdVUF6y{2{b665-R0C9V$@1Be?wTeMyZReA3kq4^Y4I0{s_$eO{|1RAEmr3 z$1OAF|vg6*w9v3x=-PmS_rtk6aKfg!!D=Wc6U^v~ub>F3X>|*}K8tVR- z;x$hQ7p{a4hQ8DRxYr8toDNecB|WV5=Vh(GWn0b)@#|ou04fB3pV*NtqNo{r1U{e= zy8QPQ%1B;hlvddD985OK@QgU#v<1rHb48z`#adBAdJ2hy0^Y_?wC} zhl`EwA#M5>jrMP-I)1ZljoLqdGTa*p-RG);nj&e7#q-yGIBol=YSlJ+Bu~!hlsz|Z z`~&uVrLyY9*d6oJz8WQ`^yM^2uSS3a=I_pnJ+gc=$8t3E) z9K5g&<>Jz1a?7XAPGx)9!lIu^nAoXFviw?L-=J*nzM$t*uzmM3hZPNZX(^)crn3Z} zB2ydyfAR%d^YOE64s%PVYYfrrV@HSufAi>){nZfmg%$u_Btu?6+$-s*)bx*rSd&Gi zn?27h3@VEU5=(SWJgW_gkpb{sw$SO2)IT^*o)5C#WN6v3#HU_aQ1@g8`XhZCnMs}e z>VRoUAI&=Q>K$PHmf7`)ezKhW@?VS2nV4S2-BOe8#9XOA`L<-sy55Fdj)4~4nT>yU z`%Im^M($si>ZjHRJXP%1h?ZTc@Jh8;6P1wc0;Caw-OEXJnWQC#(Bcbj{y%u2C4Cz(2SM>d+QhkhvNd(Xwg~nQIFIpU5=qJ9hUgx4gD+=ug zTReL0{wg~=)wOD93H6asMflG#kytv+t`zW4o|q0(aQC7RTG{DVqQskHmFyR znFd)lW<^AqqaZq@oJrp_;Ci3QZdKlg*LUfEQFs5@^%#wCF34ftHreOA+-DvrTfLT$ zUSPkO69d;viYgQ#_%L>Ou0qMF^Wp4guR8o}6<`J3q}!gs$HF3?5acBrzn`%`R-I=D zwp*>#6dRDyNjM*5lC=_N*r3DTau z7=ma}QkT|5yrjyZ3)a35JnHw-QWDOU+C89+`V|Irb9n18?SM)$Uo7GFPzclbIMML% zm>4qp;YuKZHiDWIOLdcziyU`e7}0ZTR>HYWgO4+!XK>S`uHv29K%f zxP=G{-%zAp;sVuX(DGhHS}X>ukFX@o+K9AbC#(5(gZ5jH2*F>_z-1E@>8=TNUt3;& z1dRkb12_p}FF{3T^R3EBCDiCkW6L}x148WH3+${}<8CbsZUfh~%HR=}GFsEhR2n+UZB_*(8URx#ag#$Mf0ThI&2dW@^q3R)?M?8m8 z1W)$OLeIMbKkgCDzbhkx0}R75(0ehQR^O@TCE=jSbOnMBMD4&2BpA3p0zh{tgC{at z(Ff6wbXch4?%!D5qZ4O!VJ`RRqX}`9so%!3yoG>`LwR>^AMFEpkG*^&;<~9wc ze*f+1};(%^bBhY;qvAE{T2WK|nl_#bZ4_==@~<26$u7?J_gbLp5fULK*#V@VLs{9x^#)sOgUMcHITH?Y=hy zgmV|Xt|sg5f?zJ?#(W0B_y~0-z+to#atHU7R!zWQadVI;=$(yaJHCF%Q3S3%?n$0N zN%E1CsOMwieeM?_erl<$sgd4Z-l@^XcV`HM3aJs1&P=Y)uL(Yk<67nR(c2oaj?74^ z2R!NM^|HjN8Ze|5sYC6~GsdO#92XY-_?PD5iL7&SET6CHp%Kg&@L;H=%=DZ08ez3t z&@9`++B=^auR@rL{QBL&5770oDL(6MP6+F~Fmbw#uLscbd+*f4*LM}Eurk|!2%;_T zPi71k`Q6_M71I+8*uUNjnzI6Bs7#qYKwn0+YDV$*{K+0Avc;OQC+f{jOdy=eaM97h zMlcb5cZ(|e2S(wOG=|LR^zG&zpRNnVdx-AJmqK|0&=ppv-_Z!(GV^u9hW1~{el7hL zUH$2`j^0+QPgL(#+OJbhWMl{J%E&UWM|opU>x>>q9nae?Yuh_I{>AP@){t(uchURL z#@AH5JJ=XhZLv(R!-sWXJ%5tLNOgV{dBxBZSxQPurfC5&*N2L?RZd@ROb@*ZH*d>` z8!&8GvET#pA&(2d4Q)D&9r-qIIsp)EV)_0?HQ=v{(JkQ9y>wBGs3n~jsMv}F&~{y{n6iAA201O3O3BY7vZaIxDjrJ^RDQHn%j)xulYM2~ zKy{>32EhMK;`-C!*tcw3YjA4*#1+<<#H%)%h%?h!ZIEsX*Is{=EXnAwyth%p`&S#g z;K4>bWmQ`we7&x!eXFx`nvzrNIR#l!dJ#xbS@C%%u{o@)qwb@bVWW3+z-EMYs<{^r zuJ=9cLVkXGDK>uRGB|WvNyXW-vY`Qc_1mG_-E>Z;>^$K}?}YF{T5sz*n}cj975VO{ z4@#udax_WSXZJz6(tGzD6X=eGcHhPXzAs6$c1p&Y zEQ#kR8rg>3?Dd2gl_P5rgyn+4W~iI!U;>+@q|)*&!1t;_GR|cZY$TljeBw4MBxHsf zzk*P#N(Q$q&5c7Qm+%Xf$q3j-=&T(NV2Q_xHgHT~yFtlZJ1xwps-%BtZ}5K3>q+-) zEUM}9r2C|jD+LiO!p>Me6_vXlJ+=_`Z)d^~b6T#x&g_yoFLwomC4Gq~PW)?YD9n2h z?sOLC)ag=(e=B+JNS-|gv7yu2B+H|x zy1FZc-L@n5S^J9XQA|}PTlf8V(&lPX`opu4l?q>IhN^d!p}GGmEl)R?CYY+9P`aPvqZ?tp zQ@vMJz*R-3rf82Mk(JAS75H&?qmG#zG3gDZgr9LECdSB8d&a9rs0 z-cUwH0vvv0)f{ljJ(F$S&*DHtoVsV-DAy~<5;;(9@{^V%B2FfScsEr!Fa9SF{&6^- z`T4V>tK=d4C{lbL7YA4_5I2+vf!}C6WUH4wW&O z48|!LIXNo@;&dMs$=Uy6?5)F^eBAiojnOSgODLtJfDENY1!pJJp-#Pz1*R_APC-&U;{eIuC_nY9v-$&$Ry);Dp zoZ^~dH&-#}>a#3eFC+b$7?DW?0}g-S-;T&|NFBHt_C*t|;O z6O4ZxLiU?%rD1V_n2GT)B3$3fpKk2wL;C*DM~eL;YLC2@Y!0SaH|tf0d@i?b%rS!1 z-S_U3QgUPOMYrHiGF|@KAX}3f5nNeE+TU z!`Ip7uDs5F#NH<1;aQ<4ak6W$(Xk9X$fF)zW4H;#_3WAdJsJHB?OzGWj(4^sx;LrJ zrTXA z68E^MfQbCT8YO8~(uNLEX*o_2?Y4O&(1tZ)<2R+a@NsAqvK zmRE4q25HgaGId`oXbKArSXB}gG&Dkb1^|}>1U->Zuteb=dC)ATNY zG*4|Ee%$Xgc-HKU2H|+RIkUR)7rCag?J$doSksy}{dvDzK(;(uf&c zWlS^#a~KsD>e))<{>Pu7S*e85VJIBDiR0O!N;KjOH}29liy(k!;=n zzD0j2_wE{-DxO8Rbx-szBMl$B{O&&qWuOx$De{|-s@QRhMc1h!o4j7|zBgOoQIM#7 zvWV|nQTR(RmD*c9;KR_p&NYcbwbRf>AU8>8vJ*G0cwCpCZwMCLN|iKJcMbVPj8S+9f79Fg>kCmmgQt$F z(k(V=&zyx=)6me6=CPK#QSixeNQCLquDdLcDeM7YLsnW~pUq3l*>2{T=X9JE6ZHep zquKrjf!tY76_if?GVJJ*@vM-St`(Ot_lpxq_~^T1ESDu!pA&N*ee|!Zgz1>bwLqz$ zM9|98Y_zbPCbFYX5IAQ-3zcXG(gI;7dW8q#_d8&?k&<6abyG2~+I2((nzZKz7+9te zu)gcu?+oGAKnL;dhHG{5znNQ{M{LT8`b)K*(&|d3(Z8`QpT7`?`5PbuK^sDe;q*L% zi!Q(tPno;xdX zUl@;@cLXzD5e*1^bi>c+r=>5Hm-qV)xtfG7KGSZx9V;Et3A$ZKp73)$#J*jo-B})I$uV>f;A~Vv9O#H+1DbVJ*BzmgXYT~miS=GbO<`CE z91%V|Ff}C0_~@a~uDa#GF~g^kGadhX&%oo0!7OYPoy0ZC{5D+*EXY4B8d{G2r1D*s zVv5RFXo31?6YWZW?Q(uv#hVXT4ATf?hWt4)j4-Ow{=lv-*!8CMa);({-$jad<@B1_ z}!%t=6{Sm6Jn~&EGrZ11+Om_Q)T*o;& zU0qVncGE3?UIbuGP8#2Cvb(9<%G_QCX3_3T`T8eNpaz6SXPG-i+vT|68_v<_&1UcH zV^;rF22}_HOh(ACx@y=L?6v^yl;V>Mj4l5SEp@f7a-;M`wZCWq(5s(nj73 zuZ*$-Bp^iADArINCk&MV9d%2RqP)Z}^s8T?4*Y5uZJg=X^h2=SYg{f?f9Fb}GUV|{ zo|yJ-PN|^odk!AGrr#=k54*-Ci3#dYm0WVgQ0^`lk{&=+JE+`t@7ZGi=lt4h_KU@j8ym!Hs zMLK2aHnv0Mh?MzXfaQxXHQJGFqpb2tW| zO03_-{0?Vrh#&Ed5Wz_Yq@t9U0GNkm=xG2GDZeOJ2!NWVC+_~n;5{*X{zpsd89Icq zK^lN7>YLbq8uxBNz6uw2F3FlpnHdRvq!UvEgoYgjA0`nW6+Kew5Uu`HwU03`AoQV3 zq|1S)--}7;gKlMzxe{Tw(Wf1DciA!ksf;>gseEVZy_y~DI@M7ml&W|0mcp#N)DDWO zeyld%uGGxS?4%zZAbGO>nt~)gx-ispvkxZVJ&^_aIwQ>t$qD|W${`ry)Z1~O zKL+w za&*}h8JcoIH)5Nudkb@=fNsfwpi%50m?XajQDXMMejo7*DPW3KUA2PVg+_)h41Nwc z`cHau;~rTH3EW@jL0MnQ2?3_tK{&H$tkK6)M*b~~O1Rs_@j;}-^{G?lq6Hb6tUq$S zAv$J6x8jfXOp3WfxtCq4d%*5iF)_E|b^Je8$-|K6nNR)P&KsZlhKXT8&z44C9>^N#8Y2Ah25m%B(o&YunpH|-0jko-JbR$d z>P_oQKA+UkmFDEx8kTs-YNGV>U>I`TlG!XHnnR9y#h_F{B( zP&8gSI1Ak`Eq^fUmG?z`;yO2oXYD~~+rVX;t#8Q1w@w<8ye)#ggt|{Cm*`RHtp33( zvLvS4a4jbGr0;$OB2iwEH3fK@`@A3PO%*J3+oTKe$Jz(C_v5mR$#*S84@eknmy_nl zHp-p%IBRv#`TU?+O1?#T*EbI8aKdX-lvjP%rTvQQ2g5eW2Vq<1qKUi31AZlyKI?_j z4l}Ww-8or2a7>#W5jN6wP=;GU38g)p*jsYEz_`f0z}z_ODYssiRw~@^o-FtW^B8|7 zB@vJ(zYA(`x4179TWh;)pC~Ju5$u>a-0cnRi#3FTdZoxgg{I>IA%4$@LU<4YjcU$g z>~rs}eYfh^j&Aqs&fmi?c;IIo_X2+hAp+f6=sOTfIJGk3YUnT3mEn`t4(SV7p5~}tmW6R zJrbC9@n+Nd4DTtf4ZCwec(9H)&tVbl>-to69TV=gfF<9_ zmi}?cJSuZ@1<=|^y#Hnz1A#Lw3Zx)nmO&KsCe=`fQtsw2BIQfK2+J>E%_#<6c`qw%_l2=4xU&dcorT_SYQudv!({o}p1@$QC-!k)Az_1yXfd8<3e7^Jw~XYaD-p3J&vL9Q=|+1}!dzP}3l{=jZ2H zR;=!{p`O6gP8E!c$wWx%&~9ed%xiXioD+6~!h2`-WXe6@#UgD{7^ozzz5NmRopnmR znU+@v1-MLX2@!aV%sdgk*-AM-nlerwSjC6RKdpOF`;J7R@KPg+bG zw4q|LKiHQ0;zWtJjoY~=_@U9XXdq=eDK_L^Ahs5u)&-!elW7q;4=L#gSeqfKoV}eR zC^|;&3zSkF_w4a9j%S%`mgJZi#r6L!+htpqT@`_q&8|=12Hf!0hAjGrNOhff0w0q8 zVcw1!aQvP9Dllen*5=TAS*Xczx7o`WYjkz{S5!>fJqwE}?3`Ks7T4X!htb!Tkf*u! z#qx4(PcpRvY|+F4s0lz%%*F0Yd%EL~w!vIsk2I8P;@#F6vOS;1$g*gHuNNq52gN!syr2cBEI zmEGIg+jjk+TDIvw_4M`XZ4b#T2&=>!vvnS3VZCA(Far3kB;Ca$t~6z5wWK>>O(-Db zz$NL+d1OOaZH(}4-4jqh-EBvI$C2y|I*R-Ay=B~j|6YWO z4@h*w&BU^ z(Ox0=-Z`arCTt^k|AYUABiOC`Yl83ZuE~Ya?i;X2P!`plN}p%obHHIeNEK^R1zvZk z?F<8_MQG11FF;NF7V*5r*9AG+VY6v{zh}lyv=%D!5Oq0{$le2r;C+gdjydN`?Adu% zD2Am}zdYNu_)r>cN<|n)Tt14ov*SM}JyP4|m>o8KAH3UfdF<0LV|FrOb{n!UtcRV{ zpQ1T?FxI{&Z~T(1d_2y49ML@Gaey_dyqSwGGqv_tYG1aP)NDYJ(HpOryuxl77rvXhqu6fr3yEt(##!7D+gRx3mZOWu)Im1*JodVlToBBj&@@G^ zQC+SKeDOw(vIi9 z;J&(@f~l^JEc)51cWrQ|XZQPcJI;(x2MhJGj-b?*4R%iNWYt$@!ZC236M}fsRVy^=ajLZ+``<4Djj7 z%yeg;ouiKl;gR%p%oQ`6+%ggv8mQkwvCc69jCMH^nOi>pe^>zTjy|OP$s(-I`nsZJ zI|8MU-4h}BbBSjA>_!&ivwzv>EWZ)KzA`dgWLj=!Wk+Ijl`)KVZ9TY9#HN8ft?^H3 zuc6?kDLK@S8}8lI5Vh^NB2_0`ZVO+hSLqPV=Lm~aT@6EOK1#T zDfHYW0^5FEKyZdy_i0ZJzahdlTkO7c--e2wu(4%|MmqOsi!zV4GIH$WFMjeum2=#7 z`7LX{on~P#-^H+WgLlqWR+PHG?^I6b_uh}7IPr9z}>)9<&T^!x*KhTjpIauD29o&sYD zsv0E!$AbXA5c>X{aLuSr;+r#dG1Um*s(lSY34Q|(vcBQAQlMRFFbLRudr(V;666sN z(vohViez0Ok9tY2b)n5_PFX*_ldMSQ0kOabM%>zGrhHLzpx`@F^CLVsDSZQnt3;$c zcYErZ(%vO8D^6v?_I2X4D!2`Z63>03)4*6MpXg-7)WiB+yt+(Cjh+rFQ>6Ue`;oCK z7`@j}Q~hx_dE=;jws0vmeA!#d?9a%DyB7X|dQ4NBC=4XA>tFLVuE_b#$5gn>&B&f* zBboIqnca;`0wxr>(*8NYS)o1$3M7=i&>YjcXxNP}|G;c;qTVqS{I};iieb9gBk6H` z_^Sj3jiUsCsKYdBVq{rI0B-m2%#c_B)v~=#P{`|c0HPd6)7cF??L}Z=OQ1a=8X#!z z8-ip7H|tYK(kZ&&-_eSM2I}e@H+}p;t1G+g%<7p&R`Q>c`(?WHU8+AhEwO0@L*73~ zPAh9fBj)D)@#xRyHFm(XUX9?w0&K64l<2^OTtsx3=Ej|2c6rA4`>IcvANst++BqC< z%dbFk&+dfMD@rlqrRD?Uc z;<2CDWF;uCs(;4i&)6e|GR&CoRBBMRz5y^OtJdfG? zz(km{+J|h)@_>7M_+sgs!^-{-Y9g?r%;P2@^QY<~H?Z1%ltx@X&LXbH@#dgL*f_J1 z)HXPHy8*@r*vkOC5Yg9-xz?jMlk@geZG7ch?-G3aZL_*Igy(nR?x=F(cg6E#1>}Ri zKMW-%Y-hgqom*1;YE7aX-C6<{(;1?P%idPyub4k-miqqjQj%s!(!^| z-xVAI$BvzF>HTy`0x#FMT1)-{zD37jRi3+qWww&e0Z0r!$Ex6yEJ#Z|5Bbl zo31euVWPR`l<@u=MfcX;wZB zvgZ6}xvxsCLXDVwRBv*}RrubQ7Pq(5wjSYP0rUPtQ#qQw!XeWgZr4hqk9|)68U-RU zPf442AKwSy2WnvRxki3i!&vSo?A18c@Fvmzz||Glwzx+UhLiWJ!X%wfwuGo@72R;H z5Lz(Vc>}ytrYX{2cS*k}A_Xg2ySH1y<5{uc(OR@^WL|VM18wgZz`~ z<>#eSPp&~!J6eZav{&yt?+$1xbJhtT6h|)Y1y+_QQdg!L1ReCG4K9ax86B)T3giS! z8XO8QCkblz>9d=b#r6o0x6fE}PG7LGmEEtT6Zli5^HPoF(ee3cu*AT8&%xZDF`qUu zAY!J;-R>mn87Mm7ZiN2iQfD?(sa4~!@ZqM_72T&lYnC4f#Iy<$CDIl84ZLvhp|B=r zaEVF8im>r+x%*37GCft8vEYVtr8&E0L`BXcWQccZ!e+u}pHgSCURL2^fMa!O`G)`H zae7@3`>KAU>l`P9{k??E&p!nv61mTPxEL5!f@umjnE8H*7ItJixxtmuz@GT!o6YfF zAzar@aNWJeZt44Ki8ojfs_T}x1(Ft?Zl;^P#sU9jeRj!4LNDnmlJdbBXKa_$->Wo0 z1)zQWA;c9teR8JruJ@giqq`eJ{A%~-xX-fa2icD9)*KmTA_dC-{7hI`#7*@z<=N^t7 zufn@umShGZzY|*s-BsC9I={TQc91KZT?A@cT@}97%XBCbTJu3!^j+XuhGQ9YBiu9s zL`1NZ4)ADM!z+UK!-9)h=0-`WbL1VIQluDx@6{xp{jmgp{uM&SEeI@8+TWHJYY4m#zZ}5a^cYD}VD@=yrIkmX&5+nR2h7?t z$J2F+z;3>WV*H)`jh9ZIaD5}Drl-XyK>=-YY(dK-a*>Uo^teMR`kL`;N%GgSzkNup zR^_JIl!($ekFOmk)gp|&rGwG&q|TXmUv@Xqcw#Kw?%X4}8Bgzkyk z`t^>2Kh-Fza<@u3Q&uW>p$~z5pTHaTIxcnJf z-KuQxUNoiREK8RCFSIPM-B{8=qi*gkbEK~%S0Oi4l1u3!pOpgEfq8hO<7#*B?`R0= zz*4gjL)%4Z8Hn*WgBVy2?Igkw7CaJY+S>BnRh77x=F@-vIzH=)60p`~9JJK|a_ea+_UahAgbb+^wK;DbLK!~i8_m&TM8rL@YO|-1^A^OP0aZm zcSzAZ2W3&V z>#h;{b_~%9?FT1k%5IELKJ6P!8hbC*&O8o)Tngl6@FZ9>8iAN1G&RUtopv8Z?B>jFGIF)PeP+6|Q`)U?iMYqN^eE^; z)cQQk>FE4E5dhst?B$}u-^90+#4ZT4pxP{tp2g33h*_22!^cd@9cfZ9aF@4lXY{}A zi`_-;E>uqb&u-ZhRl9bxD!=C{Fx?t5;o(%}+p%t6c4tS_S^&HR*(fmEtRJo#_&SVh zC&+m5sQYZR(uv&cGu)VA)trJ>ANC!lO1QHHW9M_|dc7>_+@v2CeFzXn)lBx%DLIBw zbbqn>8dkaO$+jzD$uEZ#&Z>$Dapr=5b2td$ShmBn_!!; z;HA{K9YS@SU=L||2z8&fMUIBeh)ab1tRKJ&GN>xfiauTPucTVAy7L4$DQ*E_Ondw> zaJ_G-U#;V_uF@=mr%gF1C_%vC=oTNoClUkHLRT?1vx*Tp3_e6 zx$n>VlEhvm5X?BHk3LrP2jaQW$qKBJl>PH9FD=XG{C>0^|6Ztpo-DxZ_i(Vs>-)eN z+JvK0QP4NcqZXZQPKYQh428$3Fh4%k@pU`NJzT4C`9Qc9;31*H8)^nPF@+fp#n<_F zE&_pCera|gzW4&_)4`N&61J)mggBeg3XgeZwnD{lhO#o>{=W1F3#(cQ7Iw>udu}HZ zleKnlNS1RYC>VslwO?P=1gu`vcg1#kRwwnPqGZ(5MBl~ed2PVg|K8I~6K~N^!TaRgF9Yw2yH)Ac*SM(S z_#F)5n1QHuqj>+HO|4o%pArME1V7i@;!^;EcN9&g+*C|Vpf6wis8<;L9e6ikG)E$g z;#7i$h!J@h{Ep+vy_AM8r+O&^iaI!-t(A;vrj&p~^XD3RlTF4yUMKh;7YCKR|0`2< zJ#S#;7x-)jrNA;33#jx@PB_OS^NphsMsi#LUH>AdgH$6>@ajlwthX^!WXVDhcv0zV zi~gw5N-X^&7L@RhtybcX9c&RbUa_(~dC2;`xIb{>Co#V)z~CwFE!$IEKTrSFaaq>j z_E1Brl`1Y#HX$?kzeE7=tEypj-^1^PSMOQLxS>CNr}aoWgB-hQshm%+NSRwj@G zR7PkYE{G;HLN&?8&p3^{g09U}amHnKpsdI^mKDA42&`d6I=z+!m=9kYVjlGBAIPQ> z-ASP3Tl7E>0s{WJoYe1eF>%535Zd)7K{`ACWd9p;u;zK0GTmb zrgx4x8YOFaZs9H)hS!a*bHZ`0*OKfwId&vFufgXt+>Y5m{C$N9W zP^t{i$YOW$qt~)xE%+zF`x0~FEWHv0X=apN?TaXWw zo|IS(UDv;Yi@B&>{$y)6KxEMux0i`jZ`UFKefj}Qsnj+lJhVSdSqvvR}o81`LMV*$m0m?L?bp35q^3|*4_a>CqPe-Hl-ER8|HKLz`-%PIQ}i*) zEcrV2B~)J71-%X+XYA{`G+I4}u532zr6>pXnJIbiysv-4r=$9M4#y$$ooka}`KWoD z)fh59_~YmZn+8As(k>NiEN-to<8wr{%&OWu<<%FP$-rI&1)+GA1M1Z7_wv3R4j9NV2)wW*DvzZ?U0U{>6^zT<=D3=;;Ma-Si`q^LScjR{($IW0)fg1xVXEi1pjkoHgLadGl)3@gut?OAtU|B`ToE=I_u*uCiA}bOjGW=&k zGvmf013A(wM%k%@un^|0I^}%%TevO{3WE6rQ^NWd(oH$|M;+~`ibv{4xM#Y2S;T%T z`n-}E)fF(kJFAOiscKt*iUGfJIaavTAy2hKHe&{IS~%ga!!?&N+0~AtR-0)hETJ9p zfwJP@unM5S!P@`{_R)7a^4Q_M!Xn9*mKLWT!@Q;gij`CE(1v@Bqcv&0hK30Qk9z&l z9@ocp`WcTUlM!3xJl;S@XS!uR+P1*9T6$ zT)NrOKMLF?axG1RjqNB$f2b%?WE}c~g~l|O!}PUGpX;@hy0-6MMmbEZ=^9hGz)0GY zB7z#cW$xeTp7zRJ0rYEI2({zmGLMBFF`O-Y(l@%oE`HU(d?Y^5&bq4DS*`bL>AL-Q z+1ZonQQj8P73DNKbz<-FF6*+gU1(c_!|vICv=z&04*tue{ZxmI-u$uIvd-@UFriM& znEaXd)yMbx+sa*6ZuuPWBVJBgNxzjN&V8M@W>^~f$;ny%^@!R^Z{O`bzl?qn|0vk3 z4Be{{^8DO3ix;pr6^GL-@J86lL`TRXUv$t`i>w{|M~_@ynigA>4tzJ%9k8ev_yI#S z$4$(9PM*Gh1ETcPhM)SWZ<|$aqte77R_Lla)Xd71j5{{b;-+EtMx1eeD}#<(lETT7 zbw{p8$dpJ7 z%SvqU?KnD3VCa-k`|bIqqf~M27r1F#_p>H<-AP|J3KCL+7@~ZBlzeB^*@>aZ|EguA z$kfW+!7xF=?u3{=ICFWD&#Po47rZ}kY`CMq?_NwN+S}lziQKw4;gX0gD%v_54C}+{ zM#P*wM}fB|=I1R7M7_U?#o%|5&U z|8{Lhu6P9&l;cR{a%RPeMSu*!rk3UAz{!@0CHn-3_-MVRwev6Uj5O;`iNEoAfBxhd z`HJ$beoTP7^M%0nR0c0sS^!J%@4ebv;{7ASC9wHMYsB$`ml#N4=iFLwui*q?R&rtA z;&-2|>5Qv8)s-#cEX1VfjKyzhk=Hr`Q9Ce+aro66Q-9z|W^hBWzJ|?u?oxPXN}#S& z#dTk}y7#n+z%+{LQuY?*f|Jj`PLHTR%ql(q&b0-3Em5|O-r=?M7sNYg42#yw(tPpa zk^IloI>>n}ud99!zdHw6_dSjcKE&YGo1-c%jpV$MCAJz;S>BHh<*k*=?)e;2dPuK3 z67+<;KRtE>Kg5;O#B)7Cu3uP-z2BkxZK1_ghfcR$p3(Gp1IFI@qv&DMZik6@YcblP zu-Uj5$u>OGEw%Q1?Z>gvTaK$baV|UhJNJEPZiuJTMR4Jp5S3VN;9H(uuP zhs2V~GyNYA7MF!W;>7#sg=jC68OeO$6@ou6ap(|CDZe?~BB>P0YAC3Bvm|-_{c@c5 z>Xq)IMd@=^M@s@1_NQBywKD9#tnc(>V zIaXWotp>hE`-~nttjkkZ8~F3Oa4VYctVFhoBD6Q@*P2gQ2SWmEMl+1RKy zQTZyZpN*G1R;<*~EPxKMMOgENMXC`J6|qge&Vb#D)lVX$Ihx^X#X6<;PF@zQ-z{Mb ze1UTPw_4zqIN0_bA-XXCn#UjJYkqOM_U7cJNqL#GR)x)DkKST|67}aQ6h@tw(+4uZ z`%|fo2x~5z1B>G)RpKf>HOFn$9noC5_Pv%-gO=K?K>%14)~~L$Ccs)17r5H_d)^_q z6CN^-4{7sDc4iYo)(KQO z$_>*zk6$sw@|0jK2V>_;6q|Oh`98`(ssd)yU$_AXW@rakzT9eXp>}J;NmEL%h2x_2T2mn8`+#F`tg0@ zZ$F8B0=2<_&mF~25(b!=X#C0Qyzw^4bipg&0Y#w`zB>Ul^g}9a$jeku96gM9eeomn zNxZ+8NaoG)6F#5PjvR%QcR5;K#|fK5;||xyuN=l5_=koEp8aXZ@bAvGEy&+lFSldkSVlCS6)9}Se4C_C8$@FAtW1u!E|$L$4~)hi{(AqcUMx6FP1 zyHar?gZTr*%0qitudzK0pBCC|Cabk%6ZW0}Q%|1w~veH)sGm_*%_s@C*vuy+?dLRWhnLj`PcM3lC2Y){JA`+wlr^i=T+`YSwSE zMDS&Xy4z+KS85zew(n6FhdAlr-+!gubr9Ks{Mf+NoRm33va3%|(6w$_A5TU3Dd;rA zPr86Rgp|!B0-555dF(1d;gnZgzpY80Wy&*R zL{4r3*i|rU{ zlu-fEHRuE0UpQ8+L{sFpJYeY&7Dmn13fW`z!q*ETfEeviG=TSxr4b5JKeL?_O@7(j zXn)R-WaGndt=f9meW!v%)kM?wn((}r^*)GF*<&i28chN{cM8JJgx(3d7W%UHIG~*j zS6)r$X4_%7258p;FBB^d>I>{)UxjPF#hZcA6#-O z`w2jLsDE>&zgl7>%nAuz>9$^yRb89z59a~s@(ld<^G!XN?{gEwd$rL`BB47W-)b_j z?i7TouxzQc(jS(yyNLD+24w7==OAm|{<`R-QMr%3n8Jb|YRx`;&@4i%{5o@-1~00W zjOfVxzq*~^X(U_C5EZ6*bb;S3>HeYtE}ogLQ8ELJ!!{HApVSmfJ2u&UN!U|`y|Kv% z3MRH3J(ZEn3|$(TLUfQ67=AlcTAt$P)tX$paamwX-1bP0zq##3VPx5KtH)&@6TmZ3 z*wQfy4q3Yuw8waprLhU|pt95IF*l$$c;^gl_2q2C=hQkfMSYD0C4|0dKTN4gmLyM< zZ9i^1pJ7I#MPy4=H!CPTvAz&^kHTJ4t%(|RUBq+b9W@v8)Q`c|167~Rm?uJWD}b$8 zRp3exQDd@u=GXU*?tT=A*@_oZp{6i5Z?dD9c|bg;_OAE}`D8kO;HwNXi$+P28GHK^dB>4x? zdOtkA{if~BNX@C6J=rJeVB^VTT8&sva{^|LVcOsb7ls6N`rC0D7(2+hLUZjbY+4Li zf@~Rx8_g9p_pl$<(QB7r{Sd>R^S)T#vW5eS5S|gAfsDAtEaTu)^P0zqX?8w}Y!D1m zRekI8@a7NV++m5DD|OA1Ctd-Z8r$Y*dH17BZNy?q>E7Cc=N_(QPeOai^i1#+y=SS> zaaFpEC0uE!IO_roPSL?vXqLJL++q+^sSWD2ZW_2ZB9}?jxwOk&#>AUxrU=81%9dyq z^#YabHH3eD)}_+@arM}StEsyfXPq@BP>qyctE0yl!;)yXTdtRtz35aHLb?6&aLCEJ znT;EDdu1v}iZPG^b|gwi)Yk)Z8ejF0!;298W^eU3uLx`ka!T;77Lv@rnh zLWHumq=mnFuMxVgve{c;trka;y^t)+49^pp1B0`bv{RErruF2W2Ai%OO{r{faS74e zI$PMS^xr~ST8sHCcsZUd`^#Ln2ErG{*2xRESr9?i^VMpIkVB$bu>xk<;_cG5rY+fK zv73rqzow=(Ys`@8QxD+7Bd5D8S^68m$H#{Ha`B%$H&Ycml+icDI^7t5!rbP9ZYvEE z%C$%$M7(pL_4QslE*X=l)Mw+& zc6}b^M~Bg>pWOP#UE9@$$;{ZoEc-F@pC+)4q(2KZE)wZmD%~B#0z4H1jyqIJu^rAg z^Mk0AaQE*jXB^HyF4L90Yd%aZAKx;66Ks$7mzN~T*%j&#S+>wdMAT*zmStkdSW;tT zxpKM_dk>4g^CM8i!M)-f)FJGJ^UrP{kr$HItnmsS;(VoBW4(?vFz1fOcc1KfJW7hi zQpG1pPm7oT!#sbO@V2JzqRi9mlxI!GFpQJh1f~-AU-7cQb2N_u3=7f)X8ZdYJ~5 z%fQ+PokxF)zVHS$T_nYD$xZow`SjcDwm)dKM*-UUOl*BbU9$B|t)69x?nrfeY-b;2 z?U$$ahX~)G($`1}I;{=X9Ih=0vTjjQX?f;0!+LbYt8#1*r~sPxZ1Y~G3;tbLoa`!b z>b)Zm`B|Bx=q`Qrsd?+SunOu&W&PZ+xVjmeLSLURD7wkP4vb5z&=oq_rJb=~WE8FG zf+p!k_NFJ{ACV zcg_50@b~!tr(f%TvPx&4E(tGJ{9Nc(fku|mzM=H>>-f2KflmUtVo*qNa`uNLqeLfG ztEUu3=}PeL2jE^KFbnh z5M*p_0a=mpA)B_6X^nlK-|G?e4YL9GR-^46&G7Vh%iP5#m^`XXOLsajk09kMBNZDTxz1&A)S8|Vh9E66AH+DL|*&vI1Y>0GivUDicW3pdgHf&{?ijgRgA=GNA|KWZ-{%SRPcyvL6p4vzt41jvU7P~6 z_zIFj3MdLxDAGPGlc$(76Vh;0K(oGRF=q9+`w)D#800WoRBq>iTgB`fu-iO~&6xoe z>@};koZQ%9HsFF^h8_f?VD-t43&0mx`p4z!*Kb&^)*HRT*15Pn@m?_?>nqaXG?XKa z=A4wn>&g}eX)YDH2C*h~xj zE|5^xuhEB4J6JFas-=)tasuY`YxDnN>^;NT`~$ZCkfNw+t5(gnN^6!@?A6w45v^Ub zR4LJ@6+2PC7DcV95vx^u@10P4h7y~o5qpymBLCdS^M796$MNJ-v7r^Yb~Y zXtwJFB%$cKyyBVfU#C=tR#X*Uh;pnpziX@Z=-w_U1$qBP(>5fPnOm@tw7SM3vil?^ zY-~7&GGA(4-F~U{_URM42Hr@uo+_oRVaNQ4Y*s#6}Tb(;Yz!mBN-@ROy)fV*VAlkfqrl1M@}DfG@1@N2rRtA zKoa=z-)i|}@q<(1gC!C+`jR##O`<7=x6GZ46f^p_|3oy}>2BLm#K@-cNk)P+DCo85 zA27G&+3%~zq`Dyi^mE$yH5(erQgeK286J$74g8BWvo-lC#psJIs1tfXvPS?STu>aX zbbS|3S__nb1^Ua~-O&Ytb-y34El|fWSqBb`KPzf9-1u?6eU4DE)$uN!Nt(4G^DP`E zn%U^fR&|S--+fT_NtVkb$2NHEftF#5k);`(FsGUP$pI7l(b~(O&1LWIP6QF>EN|b! za-3?hcMFBUR+`R#`$QQ$ff)d#-34v8ZpF8WCYM;&p-fAGDhSSw%T-Px@s$=5>Q9^e zT-X1~i8c#f1^hZFseATrk?!H|=cTPx`VDx3`jrIXf`@aEYFgC(pgp|^Zu?xHe3)~= zJDcS%-ul!d&}i|#8>dJ(gJemM{<|kMxYhPnyRaK2CzQw+eG~bPDflog@?|hp<+*SvYIE{LwNy8 zV3lJpuMtaA(zscNEsX4K!cq8jccu=leDm6(wi@Z98@fU;gH5Cny?RvW&0fMvZ?mr~ zg*tf^IQz}&5y5?OxC1ez$2CjLMASkMY7$nTBFjpH#b7tJGyC zE?u44s(|6L0=2i6>*SVF`2CbV`RVi~KUSR-IpkZ4vG(LE zPf_?%-8>Ly{OTR-gspM$5U;!Nb8b3eb^lA~I!%S@SwN-)%n|hj z1CS}>2Gdk#^ybnK7hPNbfl$>{5N`a3L-)6L8E341hhw-7;oX9zR8)r%gWMsMOd=IHvT3^Y9@kMG}ps>uIS_77!qGNM+=g7dvgrWx=UTzGz1hou@f>03IbE5wWB7}3=}K}?R4=_)V&VKXaJVksRdj=<$QA{*G9JsT}+$*9cx?w%#Dr;Y)`ziYtAhzw9OgUvu z*0s(*t{fm9A^Ri~)zzibD~(QvA!n(ZN;3hIN5dkGotzg&plbN463)%*zBEXFR2ZOk zYzynsAi%Jo2p05l9j@89QKA9nk+Z#Fq=XW z3km4re-`?6O7JaNgnos&W@5EFs&^)?v2m%cQFKA{+s&j>i>g|Zz`?Hu1g2I3#22A3 zDWxdN;h+?lx4jy$b827GQz8&0vMIZmGhu7%U*Y0Y#`L8Yxk=Krfy7n3FVR(AQ|hi9 zQD+tgXBe-zo?CM?yl;l%OIC0N;^vR0-ULQ`PAR zk#8F1Jo2YdYEk%NzHMk1Jg{Z4{*_8MEe-VezZ{P{*?Y9kMhLrV5s)ol@1olJ<=Ik$ z8v~lv-b=+*7ZlQQ$@t8F#;tC=K9Z$|&>`|zaR&0G$xDwKvP=)hvF=oh)Z|`B$&z#VMcP8soc!8x|s^qF9 z7@NGpyxu4+`*HEN{V46+?=sM7K4id@3m(0zh-)5^mJVex2RIU`M#aprI+ih?6;{On z2YdR&S)pGr1Lw)dJ{h;`XU6zVQzWcP+}ih3V@etTM;EX`lS zG8%&#;Uv{NVdj~%(VUsx3`*oCOobWV+CI?7WTqu0tDvVPhj4ndQ|hs+LNrC7!Mn|x z*%)fwC#A_eY1~Ho+-=?HQ0~I-UFmj2hac!yGUsU1-SwoUsg8l6ed$G&j{=r>g-O2P zek~zSq5WUca?8+!#DM*KB`v`ZJnw+g+-f6udj|7B4h3WSUicWn4LO|4IA*Tjs&n<- zV1~)DV^3@w_S=V;5Bt_{-|*2Msi^V{bnr!TNE8NFn##tc%hvr2!v{?}etQg`Kkslc z5}UmGfz#7ftm%rL-0E?__8AbGp&NL|$8GhzB|r}8cLe)?4p)RiV(6&hiqZ79mLoKo zub5`}hfZ~mxQ1ZGhmWrgZrp$K25S#$o&HoS^oUF zD3-`oy0?7Ks7Cu$dliC3i2k*oMN}yAkwK^{}LEIkC<(<4LH&>*+EzU zUpZFU#=j#U!cK^%%rv!kq3nNB54{wEB(ZPEsubkB>`yDSbFkgoj*N#n$3YlK(O_|N zVpm)WU8}m_74M?`>LYn=Kear8mm3slNAfc~5?;DZf(fnuYj?^nbISH?@xT}dKX7_Z z$Rlr*CfMuluyu9Sb(EEbBe~z^3URWd+ zPC)5i*|zwB0UJO^V#r-@y}yIOaekDG0{VGNlc!|53sq#FnrK3|YKPX^GXTEhV|%{@ zv(pbvGoQqlPf0&U6fpNJd}Lsjb=!u12P=aspXKVX(!K}F5kl(so(Ck|CcogGQY%vt zP|31_W79mgKUp|VT#P!Np%NUAyXIE56L*vJEq#pb8g@TtJMOa-98^1hNO}0-Z7V%C z)z}H6?~(r`NM`kGPw;v`0PxkW!7DeILg1Pm_pns?lcC@s8WWXQD?N`idF~(|`Q<*! zGm&IyvAzGOG4F@isx8OfhbKG#0yz8r)`5PQ%}<)wZYvuS_~c(3XYeWuZVP<>>e=o9 z)dFl|ezbSfdhtaLD!u-zrh8S(%-`y=-K`U~mA3vrMU8#wnO264dHSb-<>1{GlY)a| z8^KeD_XakMX%|h~U8jyCpHdtrTlpQ=vep)RnH?z=%#O#oz?IKAAy?<;Ui({JYA8dy z_`hXX=C}T$4YQ0rGq)or*%;WLoD%IbX+84LA+u%!DK_{aS`T8`YAVZ2e?Js*e-pGT zrv0~0WN6v@Z`>f_2-l;G%Wv}|yB9_XJV)~ZUhuxiS#rpGf#c76j?T#I--7%wmxsTb zdng?|u-Ce)f8z``*(l3Iw7hr_Q6xG|by*wg%YQmGrdgIU?x1@QbQk*DR-VEWxCE5*D7D&l6Z9$jASaaEf%c%k3$}F&&i#hgKOLd|Z^7LTqg{WY5%c7vP0dqDvcW$~%NmVKrV24ne z1zQR8UuRuI#(bLHRzalM#~V$V-2L*AWxxAJt0sa~I?6~*zVpS-rV>e~8Gox#jyCPy zcw0THedt2SB%iV`Kq=^YAgcQ}t|5u;ou#`YT|Bh@;>$Et?>6Fc4_RyBZiKrVR{|!6 zU$#*^qFMbq_5M@V^yZ6#+y_3^6@i*1pK4)Jb~R2USFO_}wTq?ZE)r3!xmKbap?U*+ z`fXC*3agA9lwYLE%BQ9$6O@~~tTn083TA90aa#PHX`69W(W0XBYuv@tjFIEsyia+Y zVwXcj5D!|#*lRWNIH@Yyw0)p2(WqGmGh=Qr+blLLX6-f1YD7Jy zvtjR!uGv|)X7_cy$T?gsO8^ArHf;gqyUu%NFBkWKPGJl|&Ti4cvOXGc6imD|S|O$# zO;c~OENAu`rmjKW!L1ea#UNzaTd6&upSL{|N3ylCXM1d_@c(vr6?lW(3LhK*@t%V! zseR*+-IORfsKP_j))&(v2Bd4tsOH+vWE$1?yF?U`d_avTl4|r-mKbvBir;JOC~^)a zZzuZ553p;z*wRxPZ?(hMv-AYH3O425A$Cm)A}t53*JTtcMu0#VOOyn}v6tVaiKPxA zsQ@~x^{nADvl?m6Vfs(Wv2S@pB#P4w-F$MHK#$N|<;8-XN@sb@b;j2p$tkk8$V?Ef%rCq-oN!A) zOBu|Z-Xru^seS*NJ)f!()^j8FKB4~IKiMfg#inU53`~+OOu`aaljk9hV-~H8`;wu6 zPOe|PINL2_Z#a8ksN-wHL4@}ozAz`A$Kh_blDQsB z+jb?L%C@Y+Ad`DA%!C+djkH->mi1F#jZNemct9=pLo1upfrk2stZQmrHH(1iui4D# zpiG0nMfVbuB2zQCTa|HilLtI2$5Z`0GoV}rGXAe!`&avv9lPCp^NXmTQYL%0T;w!4 zxXtHGm? z=X$Us9X2zk;PZ7byzvj}Xgnwo%#*g&I687sOxc${4^`&fibhBNjIM$ASW;h!B08fj z946M$cN@|I3(;6LE8)x+G$B#m{`%nNhA%8{n48}$6y#-;qOzx3CW%dI0Wdnt)j_0f zH`=#iJu}`LW456@ptyw}-0l9sn=hT`H_v(SgXLQNzEk_Bud|r9>4aY60gmkH-|sUB z0TG*>C`f-fL{OUEze8)8a;$nj7<2IM4%GKof&mY4AQ9qh6*#jvnKTtrg*0FK+>wUA zWuKsr1WlaIwnqBDm&BjeWNumY&yKwt9Y@b5^>xi}j8QXb2V(+(I?w#(v`Y@{J;HfZ zC|D=Aoj(p8O=KyE*7G0Z>PzOL?dsFyv^oxD4HxJ0`l&JvdQg2QW9e|fq4T_}!l>C1 z(g?u$ZJk7HJe^D=)UW(N_Djg&Ub&gT%wXKt1AfjKd<=iSQM$hNO>eh>m@ygORB3~) z`*71&HtUQPe$;;8fmbBO?#u_DEka70q)|Q}jx+Iw^9DLG%_?oL+fYZ$$i+j*SnQ;F zg*mlo>9RH4mA)lT<`f8LZf4mU8X!W7IWVbpqe+~V^tZJhi<||LD4rIp89Xx`-Q1R{ zn@3ZbPW+V)-GtXKn@dZypuGj(QmWg0WiXGxEsuAuF^4CeykENMj(UK!>fm0~g7W;U znuCV>Dn|Y$sq1NF>_{0b%JF88f)MpCv-qAa!`&zg*J}cwi9s2vfmwHhM-=}5`WA)u zh8;2(J5phWelD7h^Zwrn_H*GICzFWT|3(aoH|_WCB|swht;Reh{;vx__mWAK#D$Vj zCZL_;!M%Ekffw%2ODzOVKmLK&O`Jn1+i0wrOPwIBMdza2{G7ZCr`U$fiVjlQoT4v_ ziX8u}NB@Y%l6tP|Ry_5-KJml_dE%T<7{4SDyTC18{#uVNTIal|uU=l)4kPr2Sfswg zAP`vo$12`&dg0cz;lCmwbD=|tA(t@13C!uUnTM3y`x}v=qGR}}>5ii3iX8SemItlg z)fyguaT3e^c*0cAz@4zq5Jvxqhq_S4r@hjVU3T;lo^FZ7-|YmY>=J!+IqEZvW&&*w zDn32bbsu~7_QLX)i3dGzZe`lWE7h4`)VA67drU1Dz8YM+sh@OlX?9#lH<;U^J+hrc z!~%Qwnh(%J?eGtBCBjxxVur|Tpt*w><@Bdd8xjK_He{Tpv2FR^ZuN=D;B@&@2a87q zk@CKJq33g;!GuA~&iew>2EnqC^bDr&|89}!w0vB3Q%fv~Nh`Z<60YOB_X7xgmjk^8 z6Tf4yRkm=&8~NgyW;L%4YRoXrJ56R3K+I}?goED-xxvU$eRs(*uuusX)a||UZH6O} zKWJf2rLPp2M=L2H##6^j&5KuLYk7RWAJPP3GWBR1(+qB`yyce7g~v;czS4Ahweu(| z16jv+Uu>e#ph~VcIy7YRs&HJe`0B`CsGa&wNVX9P*qoj%oGHdDaiK2{gcc*wIi7`=g(c_6H_s>sy-3dQmSyN)yz=p)D`7+5sVv!# zy7@UnPzzeZWH=@LdZbQoIk_}<-PrcwzdF5jHK|?bP-g+`&?&8eas20 z1vQ}7plnpVyZ`*Q{-J)dnHl5Fg|DJRd@JJDp0pEiicir_-fS=N}3OBx1e+Ri!Q| zvi77qA(g6%^!U{tCJr+EAR0vnS%73AdL{=!Vc;L3+JDE7z%uV>Y7PtM*7~Q^sRsLQ zfwxu9)ok+i5>{$;CV_V3cg&UPK{0W#rnkeaR|U%8dqTnbC zy?@hGW}@a*W^$+%JR$pdCD>n!@uM`*T|2GHkGhQ;7Mobl+7m>jQjDjp{ey7Ct!0LA zVV-MDt@czRTDDi>YC^rR0Dz&V0Jb}cJE|!RDKw$7wkc-Kz`JCg{Pj5P;2T-n0!CyX zSo}HFjnV&#jtq=d^3XZl+@BAiX#h?a=rw#?>SE%^zja?1Y<4TQsYz7|(HPN0>%njt zC^u~DmJI|Qs_eA_WeX%tFKNdO<_OA_Kgm(vXWxr509-oZyH@RFxH5j&ARMkgpzOWz zy~pc0Il~+ytZ|Jo*~AvW-PWTjRm;}&bqVm>uC86XlJT~C=&Eox&HCMsP|JcwnoY1K z0dR)@9a;0s%Ko+8Wik!xnjDf&oqo}E?<{u^xpAzLB>h`d#hp+DKf*FO$L3woeyDK| zPxjnStzjRdZ=~Ukk|5DEKJ+meHR(05?ELPQ=C@rFDcO|*6zia`b#{QbDS2vbr4Bm1 zO*zX@^sekG?XLWI`O1i#h`h6N*Z3zo!EPR^)S;oI8dfmmb8%60FRLuAD}A)yE;M?T z>6tKfMbPU_&P7xLOsfb$qCf}MOeKrIOu=DYymY2Y*a9X5ZF%C9WazRnh^ z751oj;)@G0(~A};0lOV3%0XG7K}9CMC`;>bgsQK>`D$^>bkj+Uyo|jb&b2_N_$U{0 zgP&SxqMHXnF8A;y6F3Wsi$rCVdNaNaoY3r6%ebw+du!Vbw>Jv5b8gLWbpV#?dTV>W zBm$0o`9b)SNpYX|$WfX z^JGHjphW*D5&F)C>DPhv!O7BO2ZmvJ9N?C(l8{;B4;g1d_)hQ6lN@}eAtejb#&1Bx z(%So|*5VmN1~4Whl1yR^Y|wUnw6 zGL)Ixi!7}Fgmc0SeOaBQSb0SVkO0dzDDlPvf9lpqMO@@IB~aAn0G)(!74--d$?X{N1s{bQkVq3(eqBv_Rf#SKsN_-_IMDe>a%Qtnn>h{&*f;dLzMHRj~zaoYhJy~n{tBNX&gyXy)cp0W--(*yaOrm=8~ZpTX_ zfBTn7=j<9sQeAvoGm|q*-iNoF?g#TB>-`K=p+k!97JBq_i@)$8eRfF7Z@0^lHQoHY+%pswa4VtnHah68%! z-62w=i^-awE*sxjH9in(Y#wdDrkcP2VHvyu z($w3N=~|NaglB~R9I3mL`D{B(y9g}k%9GdO1^KZ*08^MbS%RE@8XZx>KqarP2`KyO z2<}jo>h(FMngSeWy|vo|$vFPu(}va5K^FjoxVgd9L++INp|sPSJ+#|1cm5=B?|E=z z9xf$es)KQvqdSF%H)inPnN2UAhQL0irz+~1X^{CEvy6*o^c0`?7dB9(tqC3EJGB8^ z{Ly4~4XpFttR>BY2r86wW^dkgG19#0G6%9U>}aPaE?bsgR;yWK*%|+++u`Yy1=92M zi9?*4w8)!7z^1+8#a~`<^Z4A{q%x|<(|k#U;p0%RGL?AU(Z+AnyX_}_)q|E*A<*Gg8 z^92VQ54J9C5$yJ_GI-Vf9q(Oglef<0^A+Z#cohnNk6WDWC%d^uZ|^wlD&+`FIReJc zJZniWw*0Szu}wUc3ZUPAuVMYy>dDxj22HrGKqSiUY6MGO2l`S&-zbNQ#I%xeRXLaV zo2^5)ocD~6nNK-P$jJZbb*BCl(zyX*o&omq6T`lywcd-YxXL`YllS}!=Cs-8#ZF_) zXD4EH{?_;M?^oik)#PuDfhI-`n9<93bw$?KQqnjkRI)c4lQeBA4=g6C^2J{2nsWGr z%$BYb;}fnP4D(NTxMY5IkL-Q>FQZhq^kRp7uqp_x`r1B@7T1LuaSYy}}zrd!M+?X($<6F7y|1@syTy)qo_LXH-&aQ|%di4HKn8Dzskd14UD3xleps7jCzA z(bIxWeqMwo0H#xy6Du+Y=|URv40!qEVXsrL>1Im?+?JH4>#*gdj%V1G>X@O^%%>B) zdX_}qKG$_3zm%m9zd`4Es3S4so$`3c3p=0kCzcIexOBh=#?_AwT8R>*HXdr9kLZgDW(sbGJ=h(h1Bb)k1x}<{mGb4cum5 z`QnPtd2&icP&yin$L4M3U)L(e4vzno!zh&Ro_ZX5GxvEjm+8>*_DC?^bU9ztxY#KW zC4^+>_1E%+$WK7UcxeBdD0F4Giv9XwOT)G3jKDbiI8cXM8lqG9HSdY50Tv|^CGb+~ zo6av3G~UDSM~PBQqJ+}7q`?`&rE56uhyGB&f0LMmf<~mrW{^MK7ag_Z4JDyra=47I zxu_{&!;G*sYvZ#!`+6utHSW`_NZ+3#r|;l7KkFO*qkTIqtF#RnrNeVd{ib7YXBrOu z=+1vvtc)W|B`3s_kuJ09A7W2Fdn`XItk3m0i!3XZfJ))g_5s3brlqJ~N~=-EX-qvT zGEj>iUwIx&PvIqeXVQ;Er;MOSLH^~Yd;-^J{5w~wjN$_PQ)E8s4E+pYHb~?P*@kVY z-Y7qJpK4e&RPZ^`QSiTSH{7ez6%IU1~ENGqQ=Mmp7DvJ0_9@$PCyCS>-ONO4z0{p<8h}w()eT*w7`&_6+DK3jL% zxzIaaD!ls1=mb}yYZ_|zKW*}j3D;MTgO85Zg)LYF_g*7Og+O7tc2+7?y4m-@V zJ)1UjJuc$nq0b}fPQ-7mW}210PTwtf-K65PqaMv-kgjM!zC*nT5tUYUF+NO?GWl0i zR8-V;LtT`8B}3QV?y$~+3B0I{-FPdz%F944FOg=_KNRg*`vR-m}$+K zEcT(W_9YS?njP)^I#ZIm@%JfFC3{i>BKTuurz~(qEChrycl23bxl6j03E(;kF}iZO z!#jGeV5ixYSeja|8kc-y@1My|{K(=}cAy4Rmh(bKbI0QaSCWP#tJ;wcokXYPf@3*a zHC~*7an5&%Md0yj3sLgZ>^eNQ^UQL-tWjQ(XkNKYxOEm+26BD@^pGk+Fd?qrziMb0 zXL)7qcy{p_;?v+)bwJ`aqVsJK`FH}GY%*i)O9u-__TGqcTS6prUzRdGe0`u0^0EVA zaF95bv;bd@({`x=sSZ}*JYEQvX-@p;$Kz|39yyIWRa^WmP}$9s`So>GJG*XibiT!* z?w3>-zj;oCM2rv$(h9hU;r|-a#s-jK-&srd0ZbmvOh;v!M0{+K33Rg8HuTxGY3a|m zI=nbTP9)AmT|U}RBTcyCaY1rrEVi}HU8xI-COkCPRUKvtn-g`kjLSdEsNUUA*0~HQ zXWKSOn;-!_R6)dHXXg<>`O*(yBUbdB#!ek|)LUuE+5iUbP+QD~G)f9^XG& z5}?$7QlDM}m>1auSS)xrAhv%psDC(|g;J-iQ|@k;p^@?Xje^)uQ^`Kd|c z?hd75qfFd$8p@^fZr$U?-pH=#+(^tL`!a#A^tn&1uipyj7P4KJx6e#@5Wa&^rs&x z1*{xI(%TkE+@a3i?2hATrm8zrV>8uXIM`5tk^*O^nEpC6Ii0B=*u%i!QY;9wZ~r`% zwot+d!Xct)a;b;<%&{6K9-*VA!CPHfeHVwTgJrIuvS!kA3vOGq4CWsGXh5*pl~baa zRI5t*T?V-565sRK%ZfyX{d(HvQZ@9}2s?I?r@G(JQWOZHpRJNS(i7akj$FsdqD@%X z9}n(E>=U-8tc&*J#SGnF*heo-J+pgs$(C8XdcA`S86RzVq;F!L*w8!ouCz1KX?H(jGiMyGXC<|7?;crOyqTgj{AyO3#AP(abu zn%dYhb_#7!iAbDwO;H0?k~b=rRT6DL!0~u>6qF9un!oUfe~OK!9?KMe6>b_risU~G zIaU=JtUX;ywh5Oj3_jVr7lsQ;W33(2NJ=LS-6|+NCzaH!P)FaszgbS9nEYEkE4gWs zK_B*-)A%;YGo-=o`7oSxm;#v&6y{+lVu@j?ko1RrE?G4ZkX8Q3ZimI+U22=ns<<7| z{VPnOhx}P8P!cIcrOz8TrvpP2{&VSCM5$I}BCLI|$)g#!-zR-M{`bfiITTRCM8*if ze@%;QmOymP?4NiM5S1ltiV$7;c^cWryPK{8i&&xH)S;*{~k!{6g3l~|5(Y1 zhk7|^PZwh{;i}S9=wGcI3fp@sWgQDZuQ`#$Y@J}EQk=mM{xPShZB+7T4SBIaezKuyjrQ#$g3yAcYK-O=zM>DRP7A8ZCu%Epa0op+siyZX)odE zhr35@Y|W-?=x@F?T@z-?I}JXJ)m!zF5Ch zQ|rb@1I(Q_JHZ*+e+S`8_d5A@mi#%QC6*O?W5sJa4^PvDfPSylMc|B(UQ@S(sJ+9# zn%5>0JIdW573T1I8)kbQ`gkX1FW1n|{#k&O7dy2xf3SZs z`0JG81S?WgOmSphy^l(KQ@$WE<=R}CWKdo6`Nlz|+_2Y1hGy`oPW;tgLy2BK`8v%QgHtTne~6@?ibHS)8wWW0N+(Y6xJxb}i{Fk7t_V`Vf0sSzk->i<#SN;5T0DgMh$`jDyq*4S zy$hX+dpo=#^wa12MbIl6iNvxpi-%?6^^29ppKoP!#7*3VCns0kZq+SIqh>-6iYB(j zH$hwCO`c_Q0R%Hk!3eA|r?M^Edbajev20ovFU{c5Ts@}A<>SSI$~iTY{9kHE+Y2vc zp3DB>3oyE^40uIN!JnS>Roo5H8QoL~NYf37AGjq`c!gUv_X>}+WYBmSBtVVve2_=1 zJXn$p?`$q@p1w#$@Eg6_XK@c`d}6My74f9(s(>05Z8;V@oSJfU7u;!k-(PSv5|O;2 zZ%IhZftE^9+Yf5nv_2W)NrM@$@Ixs-MB}n6r2#ss3-&o@%qzz^Vy{g0(tCJ%f@=Th zB@L#=JwLWyZ+cVEqw?6F=i#FXRrkGTB)lEz{sIma>Ig5y3Mk#Gzq9%ofNwdo>Y)$I zD2SbrW3;mSccT8AjrJxhIB@{-nCts<0_Pb@)a)XBO;!GN`q=^}8ZU){t;67h+7$R1 zIB?7eoJX2w1CL$GqtMgs!xuxFXc>GZYWu9umF?ICf$~ct+Fj`U_$!sbM4e#TYsRK@ zI%sxZQ>@DZyN)HhPO$q>+SIYqjq{T4n6zH8Z;!@LgG|bP(5p^*etgOxAH~XdJ#&1n z#Q(TMkzjInwqE+0l_0X6riO3y)%x-Ov@;B!UeRn(p6LDp$O7GKxI&;|3>d7hf5tw= z^k6lG$)~)J9J-D~#nh)-k0RTE_)DP$e|u{Y+MPm+ zqKbf2%lf@k*3@5fWtz4^+c-GSSiU zgA{z?er73v<`m5>ZzTXI&~R@pYudfgvuO7H82!|`-_!LjSC*NSbUe#{*0LElX1PI9Oxm(G-Dl`oN6A4m*gLCj{+?Hu+VezR$5rh8|&ZRU_ieGqI+cc{;8WpP!nM+VBz{nR#oc8RIcUP<0UiIt;Kx-)np<+js@^H_5<(G**z1G9e)bR=#*vGB0N8fv{=o>0;3F z_C1<_&Ek|fJG{$D2P^)B$iunlE7;w75H+Zp6UV+D}u`fs*k-Q7}@A~vtg+PIru zKm#ted=x3NX@03pe{-ks5Dbb@K4s%Ej=Fttl=3rfBGK%fB(ed7xYBy%*}b}aa_6dq zH`UWr`t*bAUA3igxS8PnS{>OSPlKh97FkwdHN&)za)Fb3jf@>gF^Jydy4nZrR%6M+ z0-(5AlS0Dye}|khB2g{q*v#c$xM0#6SZp8>KS9t(DwcR@vLZa^pHa&)i7dC@zko>1 z{PrMxVd2^2oo#{K`suVq$I~}YGtg!}XFYd(whTDAb<^T;@Y<7_ngHSsqd4WQs5<4b zClbOxm^O`%)r>4H0eNY8<%oIYjh@0kI}9$J<~jENFjE^opZWaS?|b@Pm|z3f&zJML zx0KW;xrR*@3vp3w{@~j4@?$!G!VK>?$oL_72!=8`{svwUvo^D}^3=~-p)=DQUnc)c zS7xw%@%~+jzS`KW%=6`u!be9; zcJK6cniqGCUTJW4xIt9TwmfTIDWVsGNm00pH<2HzCJK}PQWyHH`dGh`39F~wxw}pS zJ;#R%SQC@;U|DFHq4%_<^;)y)HRL`ple{&@ev3U|@;*a&Zl2fBI|25c)xk_oG8Ey7 zGK4I+v#5hIovhcWvm6pUa&E<#H^5G78c}G|_IRKw_9&avK1Kf&#sh}vEXbW&<~cFM zeB*mmJ0HV$U2gJ9u5GmE0ZM_VkUA4%+@_rv)`;*>+itu(#bZEL_7K7Z6nafbh-zGV?ZHf&xi5Dg7#Cd$=^_Y$R%q zewm|R^?a{=Wkg$`qyl*o9d)tI7zKS>z*>0oFbq>M1G+g2!?n~bg7_*-pOa#goMV85Ny)s1sa^h|O2 z388aJ{c+Qu5-;%CNh4uT#iZG!?j3l};b2);A{aL(|L7fY>2JvkZ`h=-G9M<> z1XCAmFR@ZUwg2EkSN>d&`og7tBfm48hVegs)4Rh(mgHvYe=$_N+h=8B(NN?v5xiT4 z$!m0VGR5~%C5d^;(>_)&fQ09V~0dA3Kfs%sH{!cLeQpA^Q8fupknC(K=v zRLI+<0-Dy-Nj8ci=(iIx7|lImSWQ(!)BRxrB9w|57a* zKe&J={!pecumg?Y4W(c-Mpl*dFV0J+uIq_EHcwtoi*)_$+Y=~aBIs8sku(x=F%O>~ z|AtTL!eP>%%Wl2XYpOje^U0BsS^&~sL$qGM7Jk+;ohT2(uJAquP=5hj5(Lm(R{uX= ze_XGx37MtMR;#Yr?0=1zz&l ze4)SXfB%~8!;~b6bmvRvzQ^TPQZlp>})}f=}$Nb`&4=W+>N1r}9&9<(b zzL#xH9!WZ7x-^_r;*SuqKCra?RmiG%Rp5A~0M$GZ%H-xErF3A)y74#k%h9mG_L2Gv z+p#ajq+e3aj$p;=$PmI>g1mFm{E?w7UD?|0G>A?nzfMo?@{ye(rMQ{&(^?U0_VKSU z&SC5Ks6b_k?Jc%rspMx3sPJzLE~HQ9P5drjTfD+l%xr=>fRPF0gPcH)S9m_JR91hx ze_@1j^%%pOM*;6yM#6^@!awF7L1B!r;(%K8m0L76;#9fd8d6lCFa#o{qWo#y^CiN!i z;G}mHUjs$I(U=9Hae?md~d^ zH9JL#EOHA*tujm~1`?dBT7)B@iMBD~H9Rhd-RIa3*1^QyDtvTZ=IhpD2Z~cCS^2G} zRfsx9T#+>O=zY1Io0$AOp$R4xHDV2~r_eC_CJi!hoSe7P2C+2EB(SVcAjfevmx-_~ zw-xAQdyd@J>;Ykhv|5dmZsDk&u##n!+L?S^4Nsk&?^&3S zG;8SYQbE|-`rs@<%M}SGio2he>Zdi)gUNmnXh$&Qqgk1y;UR2WAInr6NVI0|^0#jr zbHX&U!gf`hcV^*lm$J!zKsOD!GW0uTw3EYWbwe!!L<*H(HxM)lX~7Ih6Uvw-k=@5W z;_ywRrjc4jo1tJUhQ{`oVDK-zvT)1mOzQPz;z zw#2vfi)Tcs_4y4tbm^vIu^sI(xDB`drjqvac1cH<1vFqh=*v#~P4}z?6F1MY-VG$d zIXmDA9m@l*Cq(lNE7lB zqa9p_FKRB27E}zYgw0orfu_BSJ&r9||6%bjY+t#ra+2Sihf$!v})rn z;F<7|N}~Pt>RNuO2J}?kSzIVj{Ze#`fh0+!@z_+K&hUP$#>GZR6Ta+-(hq;HCS0w$ zxm~gN3jiP+%`o-+6;`_`PdHSg90u#cgu3FY<*s|Hcz-n zj%`!bSmPB_)G(r-AsX#-Yy^!oQh#~}a}HkNDm;WXq~r7-@T@I$E^kd@T~VZmvnEc; zADK@okX>y1|LFSec(&W`{Vp989Z#z@+fsX!8nu$PYVSQF($)+@)gGbaDJ`|P)ZQae zBX;OCf*?kS)fyop)Rsi}c|PCq8=vp*Kd=1feqZ-F*E#3D&XrqY?tM67KZ;3SW%y5# zQr--1f4@*Oh7eT%P~R!M;v!G^o5?HpRFU2ViI27oquLZ>;aOY(@lfAKOJ}Hb#N!C< zGKWcZ)mh+YpOK)Ib^ciIk8j5sapoxe$jXf75#`V}K*4ynHjgj1G5k_tBNkgSmPC5} z_qCBC0rq9~&*b&*oGsxjHty1qvstPGtK50FZn8))-!i|L?#hp;09Rr^&}HMtUZWJC zYk-|!_nAl@<`C`MktPcp@gM;M26>OS*M@S^w!nz^>h`P|j&e#bPrwlvC6S#huhem{ zxczq4*w$oH1W1d*8HR@+Xam|0!q9;sS)@3N7;pIha9 zF^kJ|uWnz^&)HGH$Xd?lH)cc-#$w_VV^hb9#@%C=A+RC(=nZI?AAeH##89)1hw-E% zqxIpE1`7`@q~Vt-M%Pvau>b9x`Uz{o30gTAaWF(PpS`;5aI0Z&0+Hs=Palk0mHGwT z*XU$2UAn?8+#0qU-{F$!T$i>5WwJwOLnhaJsOwj=@Bj6;bcY{i(qYh9|Njj!LV`t#v=FU|f@Jv_AjQ$YGORzhkWTVojPg~DKN0Es;7rNM1Q zTPD-#NIWx}a72oM)c$IbSyTw@>e3{L#rB@_w0`($EEVMlsSyx!14F?Qw=9yeJYJB9AJ6fEF z6MzEfL-8a=aws4Iz2;EYoEKcBaH}D!m5wm5M~?yaA2ChHENtNodD@eA3%B0W(+*kQ z#_tEw<1<&iUmzvOgYkMrkR>oR%OmTmyF^4WXir3-4x@GiZS!})t0#Dt2pAS^m4^?O zHE>d+YF8S=205=S6m~xLeHcz*UnKr`;$cRxrzITd9#6QqVw35D+0Xyl^T%h0|JW~J zh^e+a6b~x)^tv_xPWI1_qg{>#L)+EFAN#w-SG8GuVoDW%?|kB`)bUpdq|AVUwI+kL zYh3<^c!kZ}RS0%tor|indASuBJ`w)0$iHXUQ`}oeQo9$3y4PebhMH(1`PhTUmbsK3$|SwY?WfFHmySPsgbW)PsT7Q)TF? z8nc<@*>8GwNMY#V*_SEXdBgYmZex+(qUs5P2qeto9FR}D znWVLvWDfjk=X)!N7ToK(Vqd;h-)UHt6A=#=NUz55H+cugHsef_b{M%zvwwVxJ3&jV z6P9`HZ|W3sanx`vLKu@;oww?JQvz1Ifd}e)-{cYjE6JDoLCTcLgcDn0;Uj{OoxS+> z;UQ%chPq>A=t<@;9pK22g3Wwogt{m)b6ctC;A3z(gI#uH_Ncac=_I!+X5`Tlrm6J! zHf#IgQo-4Kevsl0$DhU#-ek2(-<(6sODuW44ZBV4;PE!Dz?v)O{o2!k%l28gMcYQO zIa;{b7=9&v|99dx=4m?_U;@x9J)F7hNZz^Kc%hh<*KxG%$T~5Sc6DIu&Ee0 zfb!kDIFP?`*Z)&O+&b~v`Ha`G@yD+p0+ZygL99YnnY;UD;~g*RW%!1Pw=!#2)BVN7_fq8*0iOAfG!m*oukYP)_=^!K*t-u?^%HJGSb2#U%5Jme2VislCRd z@8UjJw~NUHk9wqgh6Pv3yciWG>KlRfi^9kqaXSh6YLBg`8jV;XfvEy}=)N#tQq1nA zF}+A><*^mCTN7Alq_|yN>sN`~eJYqaZgG7vUt;B3e2dzOcNp=R4p=N6R#yGj!)FKE z7FwL8>?wQ_4>Z4C3PE7_Y--?3UJz`l(jCy8@yqRRyb6|2-xDhjrvmuIUlAwlK<}#U zE5CF;Ir(L8)i&XSUu!C5{Pc6l`FR9IN*6&Uq#xu@Mk#fUn}II(tFw7xJ_AH=5TC=< zGsdHny@h=VyCS<}XfM7(O$ zb{bh9@jb+xcK^QU*|~f$=gqrWU$Cq0&6J_2;udXN%tMQ&YYj)HxS_yW*WBr%sP^Eg!AR` z0Z%RgLpJsE82K{Rha9R+!XSL33<2D zlV-3;4QBdMmY<%fXw7N#g@!Dd^ac-A{16G zv^amig}2MGoXCL|NbdNsXm{@L*YK}W-vY1)Y;KuYuv64%E@84{wxrJ!lzrcF=_Blx zTF|f=*mYa@F&TevA59>Z`u2TemGXZXt)i>a88XAf+5wID8GQNH^+7p8sT$ z{h-xoIQ3vSLtW--joQ{n>hl6iF3Z)>5hOL9L1!B03HCe(j!C z28ry!;CkBP`oIU5PP2S#(A)gj!y58|{v&Oyw9{-$Z_9Gt!fX1I{`{pb)MYS$Sb|^J8>_)uBwm?zT!~^QBWs|zA`gwKbNwukE}DIQqqg`{g^8hA z52(6Wg8o+%l|2vCzVb9KRjf(N!$f@~+5lvqZdP7Vg%clb{3F;OtlE>&xTy{5^C9Df znXG58q{~E1%7jR1hdyo4`gd_ECEiK;`>GQw{`4XOs_3i_!w+4Ks9Jj0I)W5mVEQ#SsF4vQ+Q(PsfyhX3-E z3g?!A)s`Cu)Ej$cLaQcpidEmd_(_OZ^-xTj^)b`|rd>Y0F+4V7K0*67p<;}b@rgxO zcz6=$s3k2Cx>I=d=+*OO=*@<#tHf==dsJPA!ezcc{&}~6=poSG$FFsP)JG^*Kg&V1 zA~TQLahQIDKPX}G^^mL=`(*@l!r$;%bLTt@5b2A@?bQQ*H1arZeX&mn{m?iQi3l?^ zf4CFO0qyUOs4G{NiBO0_muqHi(Etx1);}L@84+MeT{1pvL>W2ky*0tHkQ_i*ni^J^ zu*F8+H09|D5x$=)-M=NWE65((OlP(oxzS!%TW3owmZGu&1aKo_gL6Y?$#&QSWQG_U zQl)kEW#vAX(1JQNmGuT}6}7JGZb|wWB!xx4iU(`uD_8pFuWf9r=k8{lE4I(lprg!v zpDeBT#f=sX*OX}VZQ!(8N{4GYt$lF={^1;hfr7H@!w(qB#04kod2)!n0QCVXI13R^ zO`r3TNxaCl#F7nxf%p9lDIG+4<6g=OTPcLkq2j>BltW|afmBJT4SJ8ZLb!(@sCW~J z_*pXmr(yY#6qM;JY!{(&E(4;%RqQcy6&k?vyuiO|w`T3THuHrwe6%EF$1v<5lttAF zluhuvi@)Kfc178HP9VI>E8`U-l~6(1(6aO1ltibWB^w;qY~WIfMT@)|{Z_0z@Hs}; zdIZ`wXJ7gf77lz0Hxu~&Ec@yY?Q(kA<`1>l4$A!;hcqfy+hb?=Q=P37>^$Iz?U-rGta4u*rGwX+6xpP!;p91*yhis2yW)G5&C z5oH_ZFy>9W@1VMAeX8-Mc6cvpEq)<=lla4m`M#s0Ej#rmJgQS8VmLbRb9(hLaF4*A zdq!k!c>E+Kg-(LM_jfE0`$L1lO~m24)}Kcd+8h@-HvdI{bj~k-QmFr_;8qqV_g~G^ z|L&uH`Rg@Epj+y}qQKM!i<9kj$fZh2{(G9Ld>+P-CoB46+dtvmX(7*V9k6*E$tl4R zJQ3si4O4fkTRxjo%)tA?wmrJS+uEV2<*2C<>4g+`wn_S2{i?ZbY1@K^aGd^4_=E(1 z@yurd#qXt`Z5yoHr$&VR(iYt-Vcl_9=llE(MKbtd;HVhwbEP~{yP|HU2m2 zemhFDjM7sSY1O_$Ijl4E^}-V9-m` z0rhe3Cw9u%qgutchmvajsPANI*M5CVol)rPRwX~45|Lc|G`xJsWx!0`1{9)Tyy~~K zQ*tLZ-KNP@*VU+`msvAkwmAYwsX&Is2k1ExvIeCerRpl)*tfCC+^77`Ra9^C1q4*p z404LrSLO@V>qiB^5=#oVp;EoyKS5F?cP`2|4QZ;MqUFK!yzH;NFO-d;bl7y=I5WTf zOA#6T_0O-9>}jxYzJ}NM7E=&o)_jz#X3Fsq64iF6^HzKU`UAxWpC6SYcFT}6T+l|x zaQc<&s)p~At#3|?<{@Rsxf?@O9kBF+3zQk3aJ4JqlRqowR+W79G*}}R zNqW`!r4z!`rM7NZBmU^O2#GMBbuAfuB|1xX8zd6;nZ}^%eg_BW~6r3R*jdVY?h(Q8(>ERg|Do z+P7jJco^V=vOYUU-yW^qM&Ar~Q@bnBKC*PON53%_0go$2Uw`S&12hmNG@W|`eZ%3o zR;cFVYH%wp#uUStQp@F*#5TjuU^7x$(yunZrcRbSk&IZQH>OG1vn-kylS^MnZ9;b{-f z4}l^MbUrk0F|J?r7-%gKLeZcc!m;FGG+H1FQp2HE);IQ==;@fe&Miar5x9#{u>#v4 z5o(J4SH8Mk|517C31-pUSK=2fV|;3vo*N?5y5h^tkk83F?Vf>885oJFmA@{_50JcB zmLv~UxjCw@7%*Ddk(GQrYR+YVnvXWOA|~2GMpmBu*DiqPM=Fssl2s~9iK`u3sz+!V zb(Ri`NN(*kKjKcFsjc=%I(MD))P~&e)km!yuU^&+jZ>zCTwslL4m(m|dh>rdW}kxivzq_~|JwKxdd!bQ4}%5*K-Ipi)Q%>7H%|KO0ee7?@xwo_k!Erz;J5lE(@* z;nQT^e2?LaVk#V`uc@G$DON(Y&ko7$2~RgXTXrRcI=G2Z(oOK{dbIV^V1+Fb7b&TgTq~4CV~H%;v=%(bN55Ek~Nz0LKBKQ_h0m zsH;jnowEYv=SVe`zNK8NdZonucrLXUQ@HPk^KbPbOJsXuhqoC|u5VUguu^*1)D>+e za~LT#;rjM?#`(hF%t4FnFOyHM=PE?s{9i-QfAY4$m0Jm+Cybfrf@o2JV6LttvBe@Q ze^UtJ!h}vCfcT_UE;g)NoUfK=$CQhjlD2zC09wyR8?3IEjF}g$oJ^;t5U$r^qrz-U zHeX6mgalfhur0OyCJwd4LijkxgmcSd0NyR|B{Yms9VZ3}Q|6C9ZLutK0hg^*)v3t0 zpH8Mf1}eqA0#&n5S2Rvf+G?J;n=Z%7o)G7d9S1Jc_rKxXj7`p$yU+ru&7GXM&*3c) z(B{Bx;ef3){3YTVnu^!g&LZ(ILQZRn#nT; z!;G`Otp-+KHmTP6u&*B*W*)kTj*{*b24Tmo(ik^NpI5t0>bst|+b1z&*z zr*o%p&f?8CPAjFPYDY}DjGJd)(`jGoh_=kBNalQ|QERof*$h~1;+GgM3E^XuG^pUB zobP~RK#eP06uOk(h>WeL&sBZ9fZt($_}eakn{M2#LJ7ocMM$}<5O>Q*1dl2c7#=NN z{H)esSV1zqiDS{c6OepGDUVXzsR~v~ey^`n(fH1Y1w+nn&@zJ!M?FanUkvM|&)+kX zX?VM!onluJeWi#ms;Vd{Y`^l#q!WHJ2+0)-j_AT-Ob>PJlTm59VkX^Us>OAydbxSM=uFJ-ZJ(frzrEs*iESTGl)mVZ1_{jd8t?>;gg{L=n zv>g{W&Cf^5+r`k*R#bU4(8h2J(+9f4!rfSbRYahu>iktWTFm3^K=8=G-P_*g8 z4~i_s?^_3+vRn0b@Vdq(k4{9zxMkR*u30(fa1KeERpPCdjdt(mk_?q{@Jo?xx{PO< ziuy0XAools$NY7(e5BvNj&NuNtJmgjTH?yFw#iV{JFJ*C*}-fj#UC!mXU#G&vvm*G zl+<*VM8Y>kvLHu05bto3YeeVFM@HK^|9Ka_7Xp1bi$&qj*aQlz*PicZ!jlDv3y50$ zD0b;A{%XsUyRhQ^9}3U}4w!B{u_qE0i?2)!eC=6nnVuEYYo+ntuG~*3@LM~g`}IqD zjn?MSaFyoSWSMM&uJ-vNc_%FXTQpa1;Oyf^W619}tQ*r7AFDOGUqznmNk42z<#7qS z$Tn@pb=*-z_$qBkH?nj%@*0-aC!Ql9=o!VFHwWO-2lsezGCWDgZf~X7D&&#G<1}m_ zCois6@3CUE8INVr6_$qZ-GQTTvsfl}$)V;TSE;(4UQu0{?hsN=o1SG(lYq?9S+ATr zr9%Dmnf8i@p)Av4HsF&lk+G+;k0gLz-=s?HaV;6}5#QTFZ*@PW;zOjnOB!@Wg|C`I zH+#e}QveIT+NAA%*6%JLp^1WfXkGea;e)`V1Bct^T!{kTe*3O~I>7bT;?4Ct&T7Bv z=)aAwZwo0<7-i8vN>kdVX}>DvNk$(HyCjV7NSvoVK}rY|EpKT#=E;s^Z3zLw%%Gjc z!FckDr%fh0_NK|?;3xB2(9OneDe`7OnxXcP%C!_{X{Dz^lC3~ZOgd%U@-FX@AgCD# zdxmx5V&*O7SZ5q_lQsq>^PnfR7`yl>(NYNGF3}1#1_b1UtQW4&7>@=+EA24p5TwKC z-^OYFur*^5b6?gYFx2ID@C3FiJLktF92bB+#HKr;JtUaACF{nu5*@6wNzdvKlM+%) z6Uc}}<2)EPsUQC#bh4@>DHYc?Nt<{()vBp-OF}Bim}oYeRQ(mSLu{EB>J>SD0`pqq z%3!mX+q}KPlXw4C(Pe17dbp;XqL^@#;RvwcV44bW@@u$c#wD1G!*5rIEz9(}(>5RY zJ+ZB?jPKq2Y}z12vE=V6_FU!_XQSDbNXLg1YlL9wvNy@R!#pa2$ervyA6jlw(%zVwlFuiWc}?01ujujk+rsU!TJ5Eitc9`o?x-q!LVR1Cx{0#& z6NHY2QF%kHJ#BC)#QwQ`r$_CZm$+1wx@dILA5Hq>QtTa>;@gE`wy61hsRcopbAb=| z@}lq!TVs#r0)XGx9Xd+BU1szxKQ-SRh7Bth#0pALoCJ48bCW9!J<$$)UHoz|_iIB| zhv}etQ*IC$ba#ej#QK^uLrx|;)y?I4+IRP&+kE7Zs>hSEr8u#twt=nu)$2PMaRK}k zyP~6SUeENF-Ufu*z(oq|P1Dt3&TM@CW6T=PpO|{W!-;Yh>jwcHN&0Ld?zsiC;@%Yd z*B2WyO(~1YFD-PkSo3@*FOnrj{0`IJr1sv_Pz^4CzmP$QSJPQ`*;xE5H|Cvpv1zOe zRwkreu>=O^@do!`wuSa}?_nnA(FRbyV?XBjXHAxuz`Zh0cUpV$Go_}|!$^b3zpBiN z$;$(w{ZWJF-1=KWWcMY5^BK6JhW;Q!5J~&Dw(L>?W~Z>yKi7C5e68Tuy zmTMDJKLRF&KK_1=jVW*mA$WiAV}Zg2H(W_Df>gIpSX#5K*y7crtAH2nK)jPeO7&h& z_rq*mIzH8@g8@LQ2Ia@!+ge_jo}=}{wccZE=Gnu^b){;|Zq~WBqaVfxL0dc>G@kx} zWA{TE;%#o|wtWbq5Tk72A%tVMd%?7{4qEaLHth982b&Ijz4V9wf%t&4dIh_J?H%rm zCAb-PAtU-}=8sbUj-sqwb6y#M{#mE8P5ujQC>zmv2qeXjN?1LM4V0Jte+i%@Q zj5Sb@me#^QGG?|{qMRiImWDQF-U{V7J6#jXF0ZhOxaJZgwmNdaPUq2M@k&rA7JHBcL6M(J6w`d?ccA= z!Bd>G`|J@)l49`}4ExY)LVWRCgA*6F^35Pu^p;@ZgFe=RL|w+|=m%-VRvH;#iBv7B zBe9^Zo;MO(n_v#kD&_M$6Y0iZ?wLxlXsgxGM z#}FDV*5gCFQaF{9dfPt0{a#N6Bpl|tDS#LG$&L?=nQfy4R8K`4QXeeG0&^MpbtnGW zP*cM|s=(ND{%g|jI-DmStM-6qpKp2ktS$-;7-P8iPsbt)vi}a^7QP=9(G zdu(iSnEBUJVuO~4D~4HQ!_pXvsd^&B3*-Bh$(3TC5*IV@XC3YlK3M5)Q?_ig@ORS|0usCZfGPB{#L z(oTX_gY)H26M{+$)VL-uB*<47!IJqDu&h?eyDXAQt{r<$qAsR$_H zcgN=gr4CwrKPHr`UmLKQNCF-$9o5nLXZQO_LxoZNqT$ZoG2t$crsxFewW*;H?CYuG z%eDig>5{nA%<-gI-8@f({>$F>#M=IZ6|4B0=}fn2P(Ms_M9(}3F`;QWCip?rtUcn_ z-SRS#6l~+B?Y`*>sIxEy!%k}wv4G6axTS_SZn;*Oss?tRY4IP7GgJ+L_J``Ton;Yt?hrAAL*6bKzgDIUo?pvN+t`1RHO@RTm5?JldRKT&%ZNi z<1_Mv$Yt}=DSJh&LZPgSyRu$CxwC{fR}SbTcIf}+DIvWltq{*JJM*&j>&Q3tt(s+z zKZ4%ri<0XrwKzEeDPOl>UOT&wfhpKL1ta(E@^EeERN;KDW2>+SA9sUQG<|?D7->$; zdNkm@KR%W3JWwxIJ$LxgzG<}KQu5qw#xS+)-En#QS@qN++z9&4*HZVb+W+~)_n&Z5 z_4t_2Sh`Vl8xBt8^$jQpz)IThWmNFgURANQCh1QM-h|qgDiWhsBC*&8Z2W`;xIW&( zxzZ~ih_|zbw^bY#rVG#uFn%rjc+6i?(fVYbWMfXAzUisN{Py5faVxTCx!Xsr%BD-w zwO)g{o@FMj+*NZwGJXf0m$wHx=Px;ohE;&?wqP`TyUF8Gg?4jK_6491uJqqQ_Ra?- zk1!>#If=~uo*q;dCO+Xl9EFy}ZdR#8Px^%i+jt^K1~%{g5OuW;{vlm_=HN? zHdH$NrZ*zjZ;VsT9Gp}=xr3JE*R^I{!1)KP(ywc}rZ!E|Z{W>SdkB)mp4L;D?)M7= zqQ?3X{*3#>bpk4?oG6Y7Olw3fYV5b#{gIH>0&tN(h>rk^bc)1&TT56IP4=Q5b9>JA zj!mpYzrgX)E}B;&&cHdXV~Yf+!ahiQ_F6p!$udF@oxc4$VZTc$b_ z!ytZ}W%Zz10sP&Gu_MoF&yK#RK{Bk?y0SiZMQ2&sSx?oOmf&d{VZKSVx%hzBe}<6A z!P!&q+#GwO!oYW^`u54um1G_4#lPq8R@d6AHdbL~=8zB9WDZ6l3XuJXj=_VS*#pC^ ze>QXV2evb*q_v-#Tlt*uGTIL02u(^qc0St^&@KwS70L*#5(#B%4Cnp5n|xL3^;kse zLW`)!yJ!Q7wP}NHL%k)b_&(}u@u5@4><5CY2A3=Ky#|DO+%oUGzKL!85P4c!QbK$(3YV|Q=5Li3-fi8{o(iakdwyUq_v|%` z*s`o08P@yg{AuA^Q@pM@$1yi}DsTo%F@t)qWLaPnqyDX*HFDfGX>(W?F@>2u93Gcr=9-y{muWWiOAm-s%x&k)4XECKGu`neH?93Ln)>}h z#EnG$&na+;mNvTx(w`Kqh|$2?_D(xNKXQ}y=ac@p!{W7=oXM5$9PwJ$*2@d{G$ug! z56d1!zn{y;)Jw9`3zSa=z8QKJZ)#pp(9{RH5F}s+;AGqoLU7Ar}-U%gSSH{ z(?9h79wAF6d_%wgcuzVoG-LTJbnjOW?`Rn;DOOU_f zN{ex_<;rr7FfmB>m*w%uijZ%b!P#*JKW4&kFsI=GA8b%-_`Jo443E5$(47cOo39#u ze}7}16gnG;jOycRK=o2HrFbPc{~t z9J&J=FNC;!EwwAkC&26OWX94cV|8pu5%+|}mcug38p|Enz@R&Vo&)m9}a@zMQ+q#PN4cxzD`$mEl9pWezNF9C!bpkQ30ZOS&x!GM(2FAB zO>9dA!yf}>bNf!p8XZ#VZXj;ROL%qtObwKC$l(s0zniJ{0zUn$3qRF48XIm>7c$Lpqkn@Uf+Gw%9%}b&9gTA?hnZ$<{twZ+hmR>&bIwj=ywF z0d81PtQ`4o*VFI#dal{<8lwKmzuw=!8g@K6AEBqN;f$`nZJthP8d0#Qu2rTkE!%2$ z5IqY*`$$8nrRzURr@0S=!&`b}37y|RY@U4lbiAxG-^q5V_u<+*Mbab6-{Ss**CmI; zJXRbpF6CYLk$AQ!j~fc+@0!#BO-^i-`|3-yvVSiTJT4X7Df2?jHbnK$t2$x?zIR&q z+ov2q5tZ%bD|*|g@88s-ysXQ3``wGQCo?xM*S)2OjV(L=49KslLq9lrEQ~KlUzO9F zKYWG?n`r3cl;swem`n*?2N51sXSoA+9s*ZNi0KVoo4<#T`5xlf+*=BChRCo==>{D&M^){@x~TBu!#gV=hMH6vdxl@nGx zolh67q#~aS`gtZrGIdy7{alPk*&ht8@Rr3CD9hc}Z&7{??yG($%bOvh(iJ;l~aJ6CtD@ao0dihL%pu>A)&&<1QZ zWfIZCb)+#}Pjko1|4MTMNj*hB)YoJ!My5p=uLrzSWjsfD6Z&mH-WaCU)-K3lxhJxQ#fD#^O~r96369?x%xkOemY-uY(uk5Fzl0a-Ldk0Ka+Cnfs0LOsmkvGwu;W` z3wM)=;~MnxvzpWG)%_YDBeASpT(!=#*mlAjj4V>Tx5bibT|xZrI*_A4oP{3?h;D_p z1|KZc%rZV@XDhK{!?u^7`4ZkVT}vr%T(&E2+!>fE|Dz8`4cBbue$rmWDH4e-KpE&G zoP)DDK@QTnwO{O2(e35)EV)jSz8?ZqE(#P>8Ch>22_1H(fE>a5_>{S87pmj}D$5*h ze19$-mE)w^kHW`$Ianpja@!Gm{(7gZY9Yd;BXpP8j%i*`vPc84;pwo*^&fC&scNd! z)Lzz7P6Q6e7r%P$cEHk8;Q1k%O4|+lbEX;ZQbypMiGSp`e{R?i8$9kiCh22KFQO_! z4G1kEk;K^X8)9Bvt{2|%n1WGnS)zWrUwnLiAfY*pBiW-vLzEO$w&53tEW-M$*LBh$Pr1@bsN>n#sH* zuSehu-s`J&gOGd)G*wlA-WhgNBKPOBstxf)%iOV@z-!5kMh(xyMwo=KHRIo=fidO4 zCjQ>YVr(r~Y?zeJTAxI5)^inSH@yb6#2gHU3A_X%D^Vrtd)K+sk6`1$i%WkBkglpb zF@PX2hf~1s$JRRfB@!rf>Ad9CK+P)<@+aA@&R5QX9$y+a4_XKNJbUaLi?`4R;HT4A zQeVbbqX~EB%;VO_EVlpl{O3QSJ+H#Q@E#P|o-)&bV1u-IO+GC4t}xbUH|VjhnEZZQ z9k!U%mw_?#la>t*8)bTvOPM@D=kHDbN*l$wh<&~LP0GWW_!e~5FE}vW#E<3nRj3<# zMdH|^PnZF8I!d`+6QNUR2>(Pn#mrLBo2)B=5~+p*#bTLbA_T<)StoCsmrZ_g zvA@&G>pcbFVvCkm*c5;NcCYFH-$^~|>Gm<8wM!YFbLC|l=P9dUwR^Z4mvet1SG1dR z-2Hn=>A0)u>BlNx7H`C}Y-}<>6*AuZ*n7?T7xa@*8%nM~k;)>qVsP((+x_ZuXYQA% z>Z;ENd9iw=F}}0&74u(f#M^74Dp66&eEwRymwk~+9a>Gtf{w{|sDxpndmspM$oGx{ z@K~D~HCUZ@#XFPP)Ki6J4}LIu>CV}*d#@|$ksDItnQ7Ws=@&6cDRP~P`V$N zleX{leWh*d6cX&kraZ@EpHz-)8Hvk4duR2&A!VVur{C!gfe}Wz3!nYiC&uf&HK=0$ z%%}gi2KT@J{9<_P;jO*Pad;b(jRx?v>!lHk+{lBBq};iCMIL&9mCjXUrOftBQ~lvC zyy~<*zuZTKR)s}hW%_*Fe}U)iOE2wX@F*OEXPM)F01y9nsWj4OLyxgUGn6W8Y>~#9 zhOgye-OYba_; z|C51&r&$*7CV1FW-!)vf2HndclgsI5d-ymPSGK7Eu)4$R_4><{4~Sr)9B1B8 zvJ&aJNBZ^ZA+!-SrZB%E&itbhAyM3sZf25HJ$REcJRq^yqp4K|md~z*Fw6xwSG?Z2 z7s7rbd-j?3uW%R?hqqdcCE@!gj;>E1Z;l{be@=l*B#6cG9ug^y*3-3Dn;qu=qB@uI zHVR##46b0Y&PGTRNQ=T))sC;hEiCeW=EIaC0IX*ZEbEqiw@%>H>8&F9uw|c|I$^yC zN*Lzs3Z34Rl4~bk{K>D+Hoe!WWHmJ$mntzSIY3ain<1%dRgDn$%x9cup8RyqEWWRANI~1>%VJLfG2SewdnP4pB4G3&L5h5%PD!|< zVwy(~xI})XQupoqYo6gfn2JhzAu!JN&Qz^ciTZd+qe#BT1@grnX~Jay+A~4yWb|RQua!SJJ z;#&t>m>6G-Yipj~WsgZknN7A$O|ZO-vs$i1Qu}r_AVqFy&8qnHRKZbeJ1R08Ui8*u z@NCB?uHR7u@$$L_Zueq$E~yAF;m<6&mCCoj)gdur56*e18z-f#O}0gp+)DL(yeZo$ zdq8!!?q!#|c+F*!ak)#`KZ3Qi)33fgc3Et6*AJI?%w0lKys2Z?cm_C8yBrT1&!O#U^F{3D5s88)cn%Jr&CRI!H&UYbwE0 zEdepyo>K1LKXhNaq+V78!Z2;PS*vsx(f}5LOd@J6dr^+-TIfv1$kTUGrCSL|f zYeyK=J!x`A8ian9GB(y4xDq`xknllXMZiMvxNo*DlF>Ze&!yZrw#X%5emrLC6j~viccRw82fd zJ0xT-Pjn2Zk|{R6eP2GRP|_*Vo)c^?r$Ie##m$&Z+e~x@Y!N?(f1H@G3f@;8CBSL5 zNWip(y6pAM4kdn#B}#tnHPxBAnj#o^^2$*0s762(w#cp93q-o=OZ)`!KX81ABX&($ zsfhB(H3y5ClPsn9TwQx?0=)*Xs3kZ62swTpxAZQd>#w{A6$+>f~^p zz0@W*q0#W#QYOy1z+FYa&qqrTwA;{n>KN5Rzrv$TTeRM?r>0e_2Z#{@ukKv&{#6= zh}K*&zs2Ly^A83hgJiN`8qHjlT6%u%geI1|$8(WvHbE-sudXl~agS`o^BQ(Y8toF( zL6~c&j&Yq`=l1`QPL7kzi_^zLBuwce2?4y+ePtmY&V@8IlHviTtd#G}q|Jl6JXR0~ zF9+v8XxM7C3*I{p2P*#=4wgL=eNX9&?mD?o99NXLknx2FH~#g#qyO`J<3a!Nz0>Jo za#b--gyBYvRoBwcr*7k|Il~$~IunL|BvbNGYNg9D#A0GtVY~&>)Mq8Zw^b+g*}!Y{ zgK}{_rc06LkUaCq>N$H$=wHbL^tLe-xv_MIW%=W5qbm zRQ?!&sFfotD(;gA2;D5=oOMKPLlKT%ake{DXcji78C|pd`H;c9#-81#h;D}-S(VEF z2rc8P{QISd4E^<#h6*Qm4A;x2GAC)3A^yoa@WG9;e{Tu=vw`CQ_o*{4*P)`=C|Iun zM-Pf&f*wPoa-0FV?o8yo!rnwTE2JTJWh!HpU>|q59P;5suU+QuQ~Uv}%Kq_C;@<{E zU&YF_?9#h7WQ+X;Nc-1ecVZQHd|1nEBw=q2qJz7uE=Kp7=RzX=6IVAITGJmGRhQ5) z6pC-)kwxxJmFk9_SCdi}F4y@j1gKNGYTssIvSiU@#GQNcfh7y&p!hF(TOn*73S+~( z-qxG258o_TIm%ffM2Tp_!IhCQMc+)T&r(L29|BSremyH!UDdwT$nCeZ_mw09dGODr zQayizr>_QjC*nD-a|*HPqbIy=fR1kwi+Jo}L|lR1A9L@V3f!0yCIyaf&9~eWF9r|{ zYeau&*O^n7#?Rkm_nKhde(RiDvZ^&{75EqLJ#}FS$NOi#!}m!&BrT4@M_aRmR{+mi zV@XKh=TxbwQ8j&PTSJ+$Q-JO9o?F=PA<_u?eorNb6HV@fmF@m<_Qf?KLYh~UUs}=( znT6{ABMCckhU3xkuteRogUb1F@9$uWOWfMC-6xy`c-2to4>@by>Y=~Q8`18^(b2Q< zKceFcP*2=$=mkx0F1f_W+W%<4bft{JGv-0{ALNN6T^j>(1n;yv|AfbM`hncI!un1< ztaa2UL}9cSZlCL_{$=KD3w^Q3#Y=`r;4k_Zp;@qcYvmM<*nQovWM;L2ju`M}JJxzCMSS&_=bej7$+#Y28 z2Krr5;iV7q69y2Kf_(5h(xPgl^l{Ue0^u5(ZQ_upnQig(L~1bmvRjP%lI3NicY_HA zQcA*CRY)tm$?=<~i@q{WlowAqxOwjR(x1s;@#3wJvY)4$sOy|th(a<%C{iprx zs7C?xZAcvtkYFp0r<&jP1sntaY%2)^mY}$+9s@t5u9~v7`s=fpE1?Xrl}9&eXZII5 zP!~St*lK7wVD$)BwK-+s@=g>D6L#;w{R%+^fr!e<$z~z~vq46oj4Ou?7TxNTjR>TpxFwe^@2Km-09YP&Bob?9P>G~Ur^rFy3~K3 zVXWD8vg0>?-qTcRrAl!vdGN$IF5%$its;I#hJIiCCKGS zpD7r{-%exWb~(SZzsGUCewy^=rcnCX!7)F|yyKi|X@}!s8mu?0yEMFX&!-F2+}r3w zuwR)U)F6ud`gHNw;=O(0c3j+WrP%TR1P~B^;fs5_qCr!Xm;SvjOOsz%Do>>azf1me z7`fv%@SCA=uPX^9>SL+Ay=}1j9icok>y-%ygIvuWYDpYEROOP1`W3p$nd%8g_?tIh z8E0A>_N22nnTsV$DGxaB&5{iD`-=@T329cNgg9H|`+40{eH*%J7LWgJ8__?N(5Lgq zW%QnW!f{-@u$|V=JN#vg*tInogI*_|+wlYOD1f2SZwYRF22_m*zu{5N>vrTLOeBL zywxMdvfYL5lB=289l3nLSx#3Hb8<03v~X|yGdHl}xgFcPYaML>Fa z+)HZfK*MY>x}(WWg_L-M_og13tiz4|al^u~l3lnpvBs%c`t+o_CwxTz+NJF)GhF_n z9@2i|2>E^DMXX-yaPK^t@g^R=njw-5;cNYs>%Ha1jTk3PnUaW8;#S+Z{+-|{wt+nL zp~e0yr>j$EVNLx`#kP(U?4JoQvh)mxJn|YD$s12}(R0hq|AF9mjwu!>Zc-^xSqFYa zI?*)}+Y$oI%qYidaa1pbZ5fa!ncr|gw!7*NuGMn+_IQ@O8J2UI6 zV6|TBw%gLi&lHHYVi_r;FgjfjeTnhP#M}$hJp}ojsSXvy=VZGxTGOig)22)2m-Eo$ z66sWppMc;;Tu{#j?-vM-WzLzg!tc7i@yHf$`l#dB$_=MG_~9WVYkVwhN_G3h>BG&q zhx2!HilJ6{2ncmaJ_{cWn2VMh7AfOZ%1cRfMvT{{^tUM^J!D>)8dWMU-r`#YD-o)?8!x-BwPxj;nOj_ZJ0@T4Z=Q z%Jgy5w;dB%!frKkyRlS=)Jiojp!pq|VEYJaX_<;)?mNk009In9UTQj(YFz`U``v1! zt={k!{5=M9^c?0R}-ii>;V6*dxLJ?Y`681(v&zKQ)oIwYaL&pZ3C zTFc)T2%m9)-2+C?^1;H!r(nf>N&t!q&)=9kc0S7iVM@3jB4N}FsD1ok z`dREtmDo9wZB<1QtDIk}P~s~cabRyDkgOa#eU71*_&~1sV@h4 z@F<)_%1Q?qMuBi?me_L)na3kAPNck0E*k4<;9Y&rBXLAardZrZngfAxGMYACRGmr< zwmfg|***E$>VGa1{IAk7OYoVpWlUE)#o*n|$12^_$OF%pKF7z}XmQ7tC0%49Sv|v5 zO4sL7VlGQ+dX^8iw_ixv;L0)_av?L|i6G#Xts0mOced^-70H=VGv}NnAG4dIwIWKAeyui|D2j_=VB`-O~O8 z$(RaDs|fJO)w{}hv!pNw$lsnbiAZg(RpEU$% zJqTz6+@0;WjtY1<1FuuphZ=TW_AZKC%XUE!`UL)*(yg7w22bZn0kG9pY)hZcRY`Fb z&Q$XUEO>+VAwq4;oXD>8H*JsGaQ`} z2b>HbSp0-&Punbn?y`i(Ja>%AYIwXCtB!rrAMtr7Wh?QCDIqdQ^DXsRC|3tNsU1Ah zOmSEQ9+WTf{UWRQIrCu3{Qx>ug3kRyL`O0wFC$NJ1Q5Oxu{7Z}Y=!Q8fCO@Jw8}kJc`GU<;Ln8LeuhH>_{#dhcW8($C*)JwYG*E0_6c1>8X0!>+Q&-71>RBxjCU;A}+>D6sIu zY3roov-dxvtq<@3H;6Jq5Bb&Oa-V_sy`h|)@gzaIiG^WrMO#WFajxvO%pMxt&J_)Q zzwJ)9zPn+$>q6X1XqI$j0PLtYKWhe0!VfN)KAW5ssB#jB7t@6}t=Y*?HJ)O@65NgW=Bd-Pb!87oKTz9cZ z4#Fzrb*YbbzTO7xR-f&H?-u4BbSUV}M5yJi0~e%(FaR(*;xWgJ$^d(TNHTFVl3k1q z{t1@8ziQSn0`X|^*w}ljYz4);tW-Sg7fBC%I43hor+ludaWYF{D|`$m^@tuOaU(@J zsqa&DP2n55i0&itvp&2lDSq$tQW966I^Yj07=tB z5BK)GSP$&FcMZ5bfTot-^F9^H6gihUCbGal;!o%Z*l*-A!b`$dkWs@|UaxjPfGOI- zdqQ=f`6dmKIu>_p)5<=5J^C0OcKA{H<>g|_2MyoDTB5E;{niDL9Qxzh9zr!6YYxAk zbc7T40j^f1ca!JBfKP_J$|itn-|RJrT^5h-5!3QD|MjMwFOjGrp7-U^`F0q-dy4cq4JJz$WjZ%(bS$62vLYy{K0da5vo9C1@qK{w=` zAQ6EJ8H(s$Glu@WcT*T75yE4ylcCILTawLlPn}5`pJv~}r!I0E+-?hVmfdx@6th3H z<2mErUaS(BlB-*b*^kc=E;@akKpGdrN~((YnAJg06FT`MR5Qk zEh*7CuejIbF8Y~i`4cA>pkajO)3{p>RnLtfkmL8e`E_ZbE0yD;eip9ty~T<;dk8@W z7sJXVQA1gXA+QMKkv4Rla3pM*&nKX?t1KhANGXV#=dK9Vt!)9?{jFw)L{lEp=6RWX z+V1zk?H2|GwLmcS0l|IG_RM^fOL9m^$okC<^li?VOInm%YbXnIe~?p&iZk4;D5VPl zNQJ$i0&Yj0E+j~ud?oj#ZyCs=0@|HXHlJgQLg`s2p8 z9Qko%+OI+Wl47=UKKC*jZFMTS9``-i&CT+Dff7Wo`!E+M%J@0X7h>wAT3;R1euX$D z9)TRH=>DvqvfKk2KG&I+voj)IN@b-&OOFN(_t}&BobU{~`x#2Qn)>dupb$sV`^dhS zIdh^3CST@A{*Y++%7~uG6bYA@6?|CJDa1&ue6a?@weD`9vm!)WR`i8#NfSId;IT{2 zu{=~qt4n=1;v6yC$L>^2RIJfcr8GL!@Q{I<9J`+dp?<}+RIb-r z{pxg(J#C*WoJ%Jg_?(O}1G1)iuNKKc`ua?h)PCV#^I7-Rt6$x&;6Xp+e+FYaH5)=MlymEYG< z{&I;whcCQ1g6fOBq~U!-ofJ1!(TnJynMIPJrrQzB<5&dT4uck&`?a%SZ@#0)6Q^lv z^b#@Ka+xm_tbLWN>GyhyaUe-eGtPY0;$u!YO~Jh2qwWLCOS(=^hP=5Wji%U_H0j#R zvju0al8B?UT-L{0TAMcg)Hr9K#`-=KPZYO>(KC?XM{6x^f76b;fw$^i=yBYTMrdns^6LhBb!)tI z8ZA<;JYU}NEB@C!v!fV{9i@YdJ#tyOj(k&j^RM#l-ddZA%VtvnRIptv^e9t6{wMGI zbaVtF7AOu`9fGnW0DsX%p0we7oB!qY$;mo`;9rU1Kb6uxNS^U>g|D?{Sag2UFcS~K7%`XB{)LCl^5>+?c7GU;R-WMyTR+H+uCjb=l5pyS1|O+5#Hg(} zq%t<9j9zyR1J|gnghRK#!#3+Qx4?_OI-lxtVlu}F)@5Jum0O;#DM#bdgzIsRC!(Y5 z!YRuHTU^d!Q(xWjS9ncw{o5$iUUBx|s3FD;OTqu~M)x%@YofNWR7}Zhfl}~8>i4B0 zh6dJN596_a+;{k!^ZZG6{F@JGPeYDj(`F~IR|Q=%LwUDE_MYS(UEgaA5f!6FltV}k zgiPN5Xxdye&4f(n589iO?FzO5_Jh_mw5Eg3WY4U<4=wFizfG8{3=@={=Hx~^O_?uQ z0b-oh$fy}D*0#gATFGZ{Ukwl2OXBD|^+`LdXiF2mZhnZ696IxCAPeEo6!kwW9Ep+@ zKfOlUKv5`C=LHq^?nwFh%hCisBZS+!y;9PnmlJQtxt{DQ;q&YHwaa3^IpwRLo7+p* zn?k|Zvwn%P%9D))3!TE)A_V@tw)!)l2#0~;yY@mhb)3Sf!bUiZDYFc##XIz^{VfoE ze}w6{*q9Xed}k3gS}=i6dYJMwP_Ss;dz`WEiP*(P&H1|hqK^BE?^+T;)Cen>t4&69 zuuZ1B`q813C|_@+&9?A<&=lT}U9XO)p|^6a1M#;fbEbUK1AcS&=BG9CU#J^?BvIWs z&v{pAlQxoHCQ=MuRD5qZ2vSlTy?aKL4a2T~{M+;OAnvubfdQ2X7VfR1`{r208F~L7j`;x|wx~B$~aOyu1^v}!t zt!E|vx|`-77{;HmXjcSp6@!a{(ue+0)$Dht`r9r9{W!lwk#CIi52XLo1F-8S;f2rE z$quEBzlL~!{=Yv<|0go=6~~5eC-y`I%71F+&vO26cmrw$ufxMVUEc98Q1;(^{qJy~ z_)$$n-k$kI%JaWi6!6D^Lfg}W@n59^zc8^s?9>wWk2*3U$mSpM0lWeAq5br<=KOqT zXGh~SdFb~~vns+nqxRer|MM6BVGV!&bp{14l*>EZoPWKu_Yk{&+>9Lxgoj#xe)j*Y zmH*mxehN5J70W+8{ntqKX@3cqqKki*Q&W|1(BBR{DCZPMsp>RU?NWtLxKVaPX$BoTYLrHk)_FsVFH%ox$IfxDs zRhf_e4;WYc(XhcOru#<`*MC{rZ}R_t()h!E{D0E;BN+cLq*3Spn~OSuyQsL6gdYKK z5DlCOGo|lc{~NO-juAn$55hroL_&enpke$EVy4z}`ZuwV-2~z`IgnSL{9GA}U`TYy z?p*r&4*yPf4WQl`XKGk*HP6hIF zS#-8kpZ3uo;pb)$GSa_cycrr_WN3Kxl){ffkTEjU2Eo`GT&Q_{A&TS+RizRk`8WJU zE1iM4{UU(j>Knm;jDSGDIg~2QRO^ zb65qlb(8(@r|N-e2Z#_&qz6P|6zT77v%Q2ovb9ogK^?5#53g?j(Nq42oZ5HE;j4Rm zVbAYAzfX;RCEyHtJ_JcIX9IrQ=XMGj0&JqT{dTf+C&dSRY2f|6>{x3@Q&Ur+0bY|I zh{)2vOHEI`=H+|Wrfz1G31F=cUm@1YD>U|Nr$Jm7h%el@pg9X~0bJde9EUc!lr?yN z_ksb71#vg^O>Jz6c38#9&oRG!BK#vG{DXgTIYIr~KiD-PONjIU zFm>l?{mQw_RjiX^~*2fLQ{tZpY?#fy5BYI_KJj)E6BR5kSA8trm`=8FS%Nx zXy~Cue1_b~Q6MAln_w0d2g*xstL1W=5^z81Id^4rnKhU%>MV6%ql(pqj!Ps179rR@ zB=Dl)Ph5BV6NEK99aFDDEl=fVTQM&{KVd7l6+K?9*j)-wmM~36|1w1^)Zg{o38Qzg z0F5kgPI{hO^|D$)XSDmkKDkkx_D6`|n=Anz;$s=nZvD`~!0n#Pr4&%u+nv>UJM~@d z@J<-?8PD7!hxpq##qkus1}X!&$RN0mIPC%3VTG^5y!Z@t7$?l6bmpNSs;4G8Lg1r0 zzJ3jGq2BYLoCGWFpe}{$nNp~-&zymuz`Im>zp>C?$;{B-r*-0}to8;{@;K9X`QpkN zVz7;#@Co^E+Pmlb0O!3(1ms4rU5LQHgtw#B=9SoVBs8V*z$hO)0pN&Lf>h&z(aGux z_?C7=t@ab#Tx=$u6mm()k;AZkv z`WWs)fmkMPWE-)JG{=6Y)VmniRC$1j$ANqC27B0b8oY#B2r4;>M2mw@>!O2jQb&5I zzN`L8^1ZZ*916ByGbk<`YegaYc*xT8h+#RT3gBJg=&-bf{S#{AN%MK9SG~L95n0ZK zH$ByTA@b1!o2;fKxXUXk*z&fAUsF}qDnY2m6xEcJNMK$dK*0gWX%HZJW) zD=hvdHk-hKvt|x?&7SFPsSfjz$v#a{aeM{S`990nS_vu!rA>J{Xn%&>b%ZXLKuy6j z;U48OS)TJTB730TQOuvHDf)RvnbisP&{FO)wSmbco{7363z6Ck3v>!k2wd{p4bZ=m zt?%vQ_l8Sg&*8_lXOm5pO2)PFiQvls61Xl5ZJ2F8Icn)l^|FgikbU$)s3}Vmv}Oi) zAFWzCy20PA=CZHvxWAm{l{4`O+$Erq1$^Zgn3tzq2LCN zuWJL}Opic5?z7$OZB`NSvwG@_+efMAmP~zFrLTm?%F`_`_`=DoJCn~qf@;|3@J^Y> zXXxV{5^Et(hKPR8iL#GRdRkDKOnzda@cQr7U6)^d)4tQyZOy(%SD~Ef_fLh`x@#dt z3T->|x8%Qqk0-?OkB@?h{$2S zxdDyz?m!~bA^Wyz<#%BZ^DEtM`_x++kte$C>d@yZ&od_D*5<>Pshv}Jdh4=3%BeG` z_mU;5wKglNFTAWweN#)VvrrQDF+YHkS{W*eeQ)uYUvRhujKtx0-;SkojdON0ZEt)d z#mhZ;i+8+EpM}7AdR!-VtLKfpv-BvN35GSfI1sEG6;=;G?byaL6j|s`(P8L5|+Q^FcQT4H3atgd7|BZ$GXv@1Fs+F&Y3Hmi4z)&XeG*Lb?%` z6|1UbL2K)0Pk;WQA5VIPd7mHv_?0xsM4^;BD@8qzx#1H56M?30`+SIqrgvKQ6q^Q3 zFz{dj9YJ2cAJ`mrpQj6o4>Im)@i=GZm`ka|dpH}TBlRh#xYVnK5B?5DKNYdgIzr!N zu3?Q3VC?jy;}xMpNW(UdqBzjiWs7b$8m(aR!09HF;(X+Na?SowpPV+gF_6F>M^Qnc zfd!)x76>isJ{rmJ$O55??}bs~t^Gn|k)>8~pORE=aUkGr!Pp#hjLQgYEJtMasbGV# z1wEPDr|;%H(yu;CZpTAKo;6Z3vl3Zc@@h+aR2ftMO3@W0@(?(nm;*T}Br(%KI6V#fVIytI6D2UuY^uF+7 zzx0Srqj4m-Sqbg_!hNWIbLul7Q??{pRa3N?nR6n=d1%Q4RUqPAu7PG7>7WiwK{VN4 zX3+}q#4BGONS0K31U{aye##x2>`&pMdE4kD+E#Ovyv>?ctz0rHPG9UH-ll@@v`Byt zfJ4b~Ly>3mSxHy-2m;(VmX>Qe-67x#(sR<<1L>+nT+ zSYzpz$@eI9SLvIL>@&DD%VI$j+`i_;G9Yd$q$yZY0madho^BNJs1q(qB?EXZ% z>t!Nh;b+GxWW)5qH}xQEw)uK{Dkiw9ojI0IqTQIFMqN;|2T6Oy=!-ZLrp~9kqs>=3 z{(O1$tvc8i?s#g`C0GhA15%}vaJ?TQi|?W0@{BR-1YTe_H1C-{0D5!0A#gPA9F!&= zA0Kx{{mv{LOB6m55&lhXDB&IJDk)CnXPTc-+ix=Ofe~lAP{j8<+DNh<{Yt(AL-O*; zt~~JT)a)oFvP)4MWSghHrH9p{UQB63Ol_l$5)C4TRLBXbxkQ#M(8gGdhInA|5;sGFTWO7>iIPg)!d-F?3n zrUkeTjYQur9pnh;dymr)Ao1VmFcj}T!9r*}tg9XnkU8AN8KUAVB8-^SrFsJMnt3?c z5^?X=+mw1X7>c%a1>%o_D6X$b=nF`tx)M7n7zZ?$9_l8s%#Pbq3wpWrS!>JNU z{m>7X^Ehsr*I4KN=3p`pP8wSqJo@<_>8GM+oP0Cb$hx}l;+?Y4FQG<+I8zDN;B_u< z7Ui#?_NNcCAWC60l)-nrAK24iK1|B998-}ODweExvCDo&JjnP$+QSl0BK;Wx6?Z$6 z+)1V-ePgM~DPe#@Ku4rbK4&#*-j>JNNP@_{(^8ZR0 za>M;c13iwW)H$A%F8B`8={?At2@lHNI%x#J6{({C;xcgzsQmdjFHtuhx%zD%b9Hb(}^$esp_PbX2)1ESF0d!; zYfY`)BjH4g;A}2HoLiD9=CN91p}%P)&p_E&Y{{ZIu|gVDU7@8HMceNfncO8r#d@=O zZI+50&~W0Rx%Bc{wfb`l1WWAr53v6`!)K8AjzwP2)mU4)x$*AKH}UMwR?)5ju18x$ zH({l)jJVNeWqpv4J@}lwyfIc2rZE;{ukE(lXmiGH%+=rZ6jyUbg7a-pSm?85mFCd; zZ<@J0deIt<&(5f2lToQSSK9KYJD-Q9^n^)_(h(CnC@Co9fUnPT^qQP!FX}oHX>Rs_ zkmcoFe*RipvO>-YOP?^m`F9uor$n3BQNLa|P1rpMWf6MAW2I(U3$}ff`K2A#3S&%Y@V%cy9sQ{#!r1esk`2yLsU)dA*Qod0 z_$+Cknf_#7e^Be5CZfqEpXG!4YuvW53Hb!oT9!SA@pvyP_jxqab|W}y0|EkK;Ka@# zzSrU=+-62c5AACN8NE!x^9ZE|x;T0Y+P=ux{Y17UrsvP(lRr&oLpkojFkB_H+lB>s zEwU_61?;2KYlcnBY0k83Yx$L94f?p+L_HexAavlk%gf6f-`TO5Yw>`iCZa^)o$?>n z_7}Pr#5UA1H|%M#GPj&~ud|aTY@?~@o%5IKu*!UI)yMj56 z%5VYDS5icSL-{D9HjRTK>YP*OlpD2<+%sI^ZvjkQbK_iF+@q(FUaBi+f)4mZ+*^?7<~Suf8;|X6rH|FMc<>{tLIw zl_3#eBv3#5*8%=(X_3HEy{?i|cKa54#CDxWS$11~O>;6T*D;TGZ=YS=wcR$jaisT< z1VYbs&}@r^)=C{9n3Wa+6Oytvw^oI$t7ol&d=JAuMCo=lbXl5fOG=^Fa`bM$qp@HT z2eQQvxYJDmGEXe~Xf`d@}`B8H5QdmvnM)dXB? zG_iwR#I#S}Gvr0SgoYj|6sRLcu1YBCSwQOND#MN*-97?cs!<_Ss+EV@1~>pp{B}wv za!Qe=PpeDXSM;MZxmDiZnKqXQfy=|VEyP)V7V#%MN5Ch1`?{{VS>OP)(h=Ode=Nhx z_H8~iug+E}1)rDet5Do+a#E%1Sx{bGPKz;#cBcbrU@ph*wk@_Kl_>`72XNewTtKBVx+tNf~(IUe)yDa)zFn;psZwOu7W zXF&s}Q#*+WcQep@P;Sm9&ByBr5l&Q9csi&cLMr)d^2f(Rm@6f)c|rdf5u4=VzG&=w zgg7Y&FjWL>fCfILh=a(cw8KnZSHo0ZqD1LE{rXHCYTV(eeN$aQzoL{vv&%e0J*oa| zn-TT#o35s(`784haFF@SWk38op34akO+@HOK~!Q#ni?8oYyI)#v5cC~_Q3QO16|z# zsn-l|0#1+CKzbUtjV7_X=}Bq0$zy#Vqryqyp*N6~2ktBDO)18yv|Q82P2LRkJjtNHb0x;v1(;Fn zo}2|$xO8_Bk>!@WF;AWYk{Kszj*ihmajd;_b!BqoV`=psWpk%+oolSd-n{BH$WfJ= z&Y^Hg6kdWn(V}kiEyYq0uTY>lEeNo98c1b~NyM<;d*w@gOuSQDZ7Lmi)+$n`I$_QXB8|;9hl1shG1OZ($ex3QR#gfRGmI;9w^$nETd;q zwH-7NtYdh!1)n3ibx~n#xxceeJsw(rzO#^ew!4^qHaKMFH?S2->pbUrmV40Oj~`Vs zW4QnsaC~FZ>->!g7)>( z-Y5ymM21bMJDR}#N&6E#T!b~tnYqBYLymN@Vl~>U@=xv)d=uQwx5vtBAr+&uS4}48 zT%j$NI!$%S@%PWkZA9iTHJYi#i+GA}TTo%m;NqNCn~HC`K+ag%OP{8`X!6<1s_^^0 zNl_oAiR{UeYAcD9_K$&ZbAprYimP<*PxcML_Zs0oV6#?TiYOd#NhE6^Z)T7dJyKG( zlab7dFxNy;?40zzk)?E0?D=z#XDA;$iscS2wuN=3A{r;(l+48CB3G;QvbW~+L8-qK zhOf_|yHD$Vg3RYW0_Q9kY)|X)#lNW=l_aXBz}fnPXZSryD8{?LJNTHP^fpST4tUuM zxoK{>`SRsm>c_^VK&25QyL#gw0ujKX5T&+&OsC9;=%#N$-+0!7SK{O1p!FzOQoY>= z^i4+u6-=0>iJG`1j;t}Pp|bu&07x*DgMU{7$Uo}1G4LwsM_|R(2A?ihZN5*+pXp(q z^sN)vPloU8+d$wh3r25WHUVZ5eyhhGsejVuV8g)l<&bhDkaw#ui=?j>p4%D`S%kw= zAqtwuM3f_DJ%c43A4;+q@f9Esr66k`TEb4Jin9aIsYMv5uynjYeS% zd@GAkV zBz#VEyLHidY;R~v291irr`wZGYkjd!mq(@>@;C6f7dQ~c_R=dKD`k5)NN@O^o*O{B zgk`?QXuRMvK-!zbq#K_E%4)TUq3Ge97ewmbbL)G1y%V$70^A8>BB(Ve?=XL$sEDR; zX#(WWNW zgHFjm298*$2m-I_1Vvod1nDj$1mPZT>9bfy>*pD@>ssEb;94bZvPAGjs7{#>yPsJI z_Q(Q;Q4Nu#8j*IN6j7ALxYept;9DZhIhs1^+kCY*a&&-BpQKll{`K$QpW8D))D<}o za8d(IkH}jh2Dk}bBUk}c#X0h{A9+;tce?1w1hYfLdbe=mKvr3c$cxr~-C_K)efKC7(*#fH+dQD!^>!`w=XVxD{lx3Zo)-tRJpX!hbdayV4-lIz~ z6X@9A-@nxrM&zg3!nPDhdlPKD85V>oE@hLUA$%S?{O;8J3wxPD$Y9PibsV(TzAslx z`Vfo9v`d@cW#<+DuK(UG(plbvYpiVXAgtVub#wFcIh1w&aZE*4o+Fzca5qfhlAdFE zU2V_KS0vb{%QlgF&y|-aH2K`Gr_82`#s+q&8y^T&FeQCTto7jFJcfb?rU99lkL4+q zl9bw-bE3EGtM5B!qIGwj=(mvsKi1jIaFJw(m-_@l>rv*-k}+#+ZsC!KDSD&k7<>kY zJOvuh?wuLMynZvlD{3x*gOIlm#9N6*_&pYbWBu$9u$9s-SmmLaR1wkSJlCzqzmrwB zdj=y`<*&^HoS~8IG2G~W{F+K_KM;`y^FEC)fPJ#F= zscGX}g@fMDDHsr#I%U^RG+XEfG94!ui#gC@~bo4)N8%Sw*EY2&g^ zPSI+t5!tQ$RTFiqR!cuOy<1onlN^Cwee6*tV}GVnvf`o+WJttuKbh z#UD1^aZCDQGT#@EH1-7}9eowIeA@{cdeD)gy&l|3YSwu@UUXw2U((imkpa>(*qX@s zo_|BZ!zhk)@;#<|WxIZDV!f@a$rs8Ki*%DDU#b{;Gszn*v$eBGg`ob~J)MBUsu_MY z&RTtWp@;IxPa{h!WD8+w53LX8MSzTJ%~Jw$)art&bkd7m&Ka$i5p?X^k>>_KB*DG<;mj+)%8}aNe=I zV$|F(lQk9Ap$&O?xq}uSLDxZ~d@121{7INPu7=-j=Byvh@7+}J?u)u&%+q-e6xv3T z>}pI|``Ji+_^b+qHh*<_;vRCIhZ|Qv33`JS4qF=!FxAx8>$ca8Z!F6jT8YGnD7|uC zkf9d)`tFb1&ol^3-CP|9ZzHoq%KXC%Me~)9w zuLkDq?YQ+NXbY!lvBBz`T$9D1V2@@=!ze1tYyP4ekx%v%NgNnv>yW!kx|C6J9W=1d z?@71ovH3cw!eDcz!bK@*!IZ3B`Tj|*$bNI=ICU*k*;Wnajyz92H_gEzO!TentZ?Zv zz#TL#<)EewcZq0;NHi0l-e_ESX``#R$xh1RU|moct2p$XHQ=p$v24LFxb@#}d2>Fv zt^1Z0NZjbkXxjpA)K{EdnF3eUFW9aH9J@oTvtG*Rt6th~krS*ePiMpC)41u;mvoLE z8RtAMGhT?D_BZNnqW4y@Opk-gPUeM`ZEFpe)e&8S=04`8X>N3bWEW%FG?GkC)~fWc zYF2sHrcLd_q`O<)xexV}l2$>nQE32pcf~r|bL-417=F)D;2@zf1=GRqiST>}+S zlP!`E%h5bndykz@(j0)2E$)7ZtEAUKmxs}(-mJrRdQ<7CZisC$NJTNO^SAe~z2{?=@q4v&IV#cz!ayEMqR<0Ym-9Q{a*FtnA0` za1wdx2vUWnUCv%Xk2CZ%jIVjJ!0BTuS|{V@Lj}+y%`$zEAJFM=L#Q}Oig^0SR7!C? zx>jzBP;RWTlt(VhfePz{nQmXG{Jg#D?z0~fNgI_ksho6plJa!sg9i^>?Z&)(V5O+- zH>yrnkWy#eGWX>Ihm}>Pw2y2k3m+>cGq)$6y9l1ztc-VwpPYK;WGM-j$#&ak$FkR{ zlsd)VxPxPv`L?9^$Z`+9p;9=)4vcY~YpaU9u{*lS1{yqH9B1&IJvu!9K*7_vj+@IJ zA8nPGW_#@3vL9LmMgotV=GQ+v@&w->=k`sDchsu#{A`m|b7BMzlF$zqP+H!{4xyOr zdYiaWZgZPNOduK^8*Q9v1M&Qr#O?uu?k&+1!VA*# z_or?Xxe|IlY%5rey^yfcQaepnOZ7Y|kfLCxp*HWommHZ9ww&#N#a_GJVU=WmR>8c` zr&HGK&RcqjI?K+yu(GUq)T+50{pDUOwJqy$S*(b0p$8yP?6MLy} z__6Hh`bc@U^<=p%t(@5p#t!&cnL~Rtwe5`h5S?bXUMAc`mAPLGHJpK7(WV9`!d-F7=*>bYMS+v{ zU^H~{2&PLP5A)OAc;jDG(Es!bg<8WJefXn8$;(a{L;14~uH(^`7Lw!-SQR^^$=k}K z5L1uuPXN))Kd) ziBhqz*)B9E4yE$PFSx^)cwC)|ibW+)h-K)6drmk#w3%>f>3~Y-H!kRp;jhAO&}TEQ zh~;Ml7Po-i`F7bKT(kskz>V+5WG`qS!^7D*;r1zh;Vp#C=BPgjSC!CTO% zHy&<#7;Uo0(o*r!H}=y-%s9NRtHsiBCw>O$4xd;oV`)R(l$ynZC!~+XI);$!#vxYhdI6=>m!Ee8>s?rW5PEg3x1mk zUI*uX6z3n*{To@^iuOaB6t-Lz&N%^8AzY)~vTp0zx#;()=HoQvy$mMmutIOZd$!97yneI0>oRs(Wt-b?)&m|7 zZNw|plMmss&4HkyX@QF}ihz=W)x!QmO;47Niu@T0`i24DENg3(X7LwI^KADk*@Rlm zHIvxAo%TkZ))R+wsqz#ZP@_0psDL<>`V$p{7#12_7R=xHaB^;~UpWcdh_8OgZZ3jNL_rt<}eco*r#~tXdG*%5h7;1|JBBoUpytD;p@u*1WAH zH11`B@iCKhdg$KBkpy?Nom{Ft*jot%i*?yWPvw**m(o&eo=Rb!evLn_-d1R}K#?bm zW$X?=p?*%5H^M@5BYo6l@zn8}*YALR8F1Sc?(7+D!ZOn7xG1J8m+DUo4ofI^;H)aDWVuBU6WP0}72aW=~0J z=&srsCjaCgXeZ-mV=mFlFgttAg(9xqOY78vbo5wx%pOBHR@&B6n5WMprp(D^dG(3L z7N>S=0rchg2zX@$K_SUbg#?-7>^0`$8iVd1v(w*`zxLVR_56!CwV4ehTB!T_6#jRMFi@ z93`W7bDnT~?)HK4=`Cya8;`xn@Pst&rp?scIbCwEyg83SSvO%jOUv1_qPY13k`?#_ ztu{n5bZ$`N<*0Gcm@*@vnyf1h8SBg& zVC)ZPWOh4XTp=pBmI8?ZTUzCCl&*@#knslSVhngLmi(|hfs;3V#2#yXxamajo;cKw zXk{1PKWk7vr-~5xnC7;g$jQUA;mEWvN6Kk_KoI8=Pf<6A_cZ|pzOMjpbx&}0rsTYlq{I-$LEGVTCwe74qV)Ov3HD)=Yx`-vT}M5QLtD{5FEHtlxGaqC1?DJiqAC8*Zp9(sf3PRr{_Mu)}7Vr{y+QtQ}_ zNV*Q@o4M-&3nQhv=Y_#t;5m<&j!oPfpGUNZi7txA0{z(Byfs_<87Jh&SEsP8B!XFv zM;A*0SbDW@7Zz*lU9X2QtJ=x(vwK9i?(05(s(fGN`MkQa7wK{b*6vI9<12d7c{^EU zzWBvv!V7;-Q0KdVX{F;B;o&R?u=oX)Du&z}=S_CzB$|bTr6T&xExOIw=~#BUEbD-5 zz0GTu~!BLL^%;+sKJ4`@LX|koQ=Jr-2^YK^lOp7U^FI6FBM^lW_Ya@A~9p){~ zS5pNs;~IgyAr0{l!;v1g9+>D)PN#TE4o@EgFbSY9p0MwO30KNXi5gj_zsY%a`#Pq& zXc-(eLSL?Sw8<(OEX2g(aWlyC0fIL^KQ1ftk~L0Ud-Q(rCF!&NMwdyCCvIOdCQ>;k zEwk7IFxI|?v_4;$(Beex-MF88*wI)PWKoX5QSSH1D+bTNBr?1#FM7ILS)(!56K0zE zS=5n-_v91iOQUkqTeY&9z6n@~11desjSS;>tY*COM!`*GvnzwrE_K>+=4+?Z*H?7K z@nz#M!iExE#ouQExSM?NE>P-~KS7mL(u!S_xqiRsE_U+Kb1R>7EVlNU(&3vBVAkb8 zm`#2~R9C`AH9?m^T21+cmb)Kyxb;?Eq*Gczg8YT$>nT%K?~89iI4oibItL8$@6o?y zI$ab`2V>C+SvbB1jS6&a@oxIrIxH)%7XVRXHTN~Mr!6Ilum#!)%hF#98Vb{P^`nX?MLC6&X=a+YlF$wa4Cz^DGYcTCg>i~vn2_%-&hey+Xwm5CV^-5Eu{>A53WBqY~*&=MXgTUauHo zN@>{Xy~(#AV7%lVVqUc4&H|`*oQg^n;HWUzn;wcnL6yS(DfcFGuQTr5Fa-A4IP}rl zzL({t&ClQ*uZg#dW7xpMB~;|PiLRQuU=;Gd&=r`jYA7vC|03t-3pHxA(?~~(h_bhs zG{#Bwv^Dke7h>|p1TyWUB`;_uO3uqm9DjGiLVuhJJ)KKiBP@`d+97VUeke3My1cZ zj@`AL{VX6Dg=s;zyfY6k{2ISXiE}5P_uP4uEtOj>s$MH7LQ!5XzX#{nUJRogm2l=e^}W}QcK2f?eTsGEOgX! zcyd5RJC-)zG&WG0s%ZY)7&JG$y6UKSfBm_WN!59Y`5BF`+y1+`*CoV-gH3&tZps?q zAcvjgn9?x3#>%oj{TJL4u4f-AC{zdzr&>eCa4Dtj19Eo4I-`2ixUU|6!t&VJIV?*3 z>Y~g3|JZu(Z?+#l{JW~9YPHoGHBwcysMs^)U5cWls;JtV+I!PhQMG66RP9l*XKZ4N zz4r()V+BFn`Fzj0&$;h&zCYw2c%8hG=kt0#uIuW8Q%!K?sL84KwkVTb{Cz`^aC-nb z%r=v0`ZPfIIvtQt!a zrT_fsXmxoH8G!j0tY@<_Dc4|sAnX8Kz`5I`?R7yQ790~zxE-pJHbDB#GlOQA8GINM zBXcxeHza4RJ32o@*gJAq=}n2GapzxQ0s6BizYJr#b9T%$`Te$hwW*Duh=Y&hi?58W z(-{Qre-tvpYaIii;e6};ZF$S_m8bhJj@^$cBKnfDj$hG*n11?{@4WMLTZo1d9Ghm5 z+VoX#3c4$@4_WkIy7I-er)}=O-LD-T?AI9;E2a_|rh_gL*qL_fIBiPMLaU2bT^7aupi&Ma=f(^VyX1!Iy46=ZE!fTj#NE*sa*N@GoNC0v74M zx-AvpM7qF)0sBAcEDeO97w4@=!t%X{H||=AfEwBt#a=XbY3^H6y``kT$mo9O&71!> zahi|iuXg^U`)hCawF|`3+9>yR481w852FL!uK3Q zTa z{Q#4t(3;?`Bra8>pO2vNg!VU>`*VG2AT^fG` z3Hk}XO18=-d!fk9?G137yvBC$joNe-hG%g-+*3JxU^EfY@1jtrOa@DF=( zQc-TRRNUV7e(43c{B>$W?3PQ99GB3xAtJ%>C|3QHP@RN`>sDOb;=CArXxJVufw9

J{V|)T;&CD`ixA-XELwz zvyz~F#AT%J9Gp`O0=2R&bqj7D3qD3(q|}&+RcLY~NIGXONSbR@<5t3x9bA9OZC0*-JRy{di)>_wdfAWhlk0Mg9vD4Ui z@krtNTLVkIlc*R4kWr-#1<{%^RLvFkl! zx1MHpqHaG|q0|~MMXc<)Hh#Gmq;mhlM{N8!OK!wYWSp=neSkp*DKMOc}HSSq~X=eh4ZaACQoLhcAvIrcCBQt>AP0o*WvvL$61PWzk#$* zvk^6an!_j6roZ|SjDzyDS6yiW*Oy~xcqj={JpAgfR9Js4%5r~_je_^N0wAu2r`$&CMuYy z+Sni7V=CZeQ|}K}m~{I;cV%0-8jagjwcc%!4<9R1`q}3w4|3nJwSO&kVWA_Yzrq!& z9L)?c3|Vs@)iomhiirTZn@jm&TsHeyNo24Ra)~D8<|FG|u?k6V!q~~1d&Bb2!ywAM zPdDD|1eM1bjpD*O$QKHD{Z#BOlYp3?q<)3sA@_~{q?tQB{WYdOnsH>|@AglmG??PB znq_0$W4v(D!#2@~*T|=^bU0d_PwZma6#1Mhzv0nG!%V-4$PYfn=|@lG#x^>2K#kT1 zA)pDOKgxiY&)q_H`R80QZsO@R1q|TkHfxbb|7mrk$_eT>QF=d7$s19oC6#s5R~6aG zscDkonbOQgD3Z2lO(mxXA?pSp1IkhhvEB)`6Us(9uA04|t65ukJqZKZTViu&4fPLU zLhD=}?NNyM%R#`W5AcuOVurT4lz`x(puSi`pHYP|&K6z>;z_&Fb-lfMQa~KMVkb6R z$Ue*bK~a=_l}c#MNx~RHS>IlH$>eH9a20K}oxCZyZ8k>6v0q;=8F>ESpgHSgPjv~Z zZvSzz&~YYC{XB*Y$bvZBkjfU3!{RScftdmLF&JKvYP#$@h2ebK7D>8$Z8J{DLuD}z z^Fx~OhkK*Ws@?TCnr;3DYr?$7It4WfEeNxuQ2Lp0?qc9bIT3u)!13?35#}dP@duZC z3OW$pvEF!F)ll)%=btiuLD|?6pbV&Lxx)t;eX0y}niBCdYitS@;Xg?EwqRZpK~AGl zQY!fs%|*rQ@l)&XEVslKC=(EAeUoD^vd85QE3Hs@H`hxyYL2s(b5~agcp{_g+6DHp z6p|CrDF7{kSL%j4^Phb zt>Fz<#D7KCAP1^2Z;FnlTbaH-z3^lHi%^Wtc&m72}SEP9>{(nBTrD z%r)C;VNIkLKGm3z4GK;GW?yv8dO)QL5bVYk9zOak==sKar+KE`UJ-C`w+3V>vtP-a(!|}o^cuwxY1Pq_Lq&3 zFwr>MeMq-p_n+5`)b$j5KRn%aN*CV)Z}4{bF5ptxrI~tEgDKfO_n3I*rJT=pn9|sO zioK1eNl(VXt!>fSY0=ZXQVTFck(vt# z7eix)rhX+)>};?IkJ@^V2f5$fWG=>j>F^ef$m0((?Xxus;j%&Rzu@l!|+8^ z?=45H+yO#c8Yg-;=SE;azlmjfdZWVLD;_nz5;D&?arx@i$ps>%RLVNy`Iz`I?_g)( zGq$E8N^|#mpPA~dMz4Rov@CltysfU-PG|BhW|O?%pS<))FhvhQS#0`x`lq~OD!LD8 z|8F}XBjENlNm<}8_AHvp_$A9YlD{Dy&C^vnTf@J2L>|)39`jqlQ3@Q#qL(C(7*!w5IxdN z(A?8kzF}y8@UP)VP3cSzGVbYJ`WkwrizD${{y_B-Zm4oc;QdhKCOSM+*faagG4 zPonw>XQBeF^_J$jvXprrI~})7j`#t!*HT-kAd5#|az74uNp}77OmIB7VqEQX?dk3* z4eacl{xN^*8O0GIyWL)^$zVDkOI~+KNogEM8=g^3SA4cRvol*jo%3ZzR;76CWrmXo zu+}Pbm$!&V*}|H8*UnmJ5j;Fu#Q{jd_CImOsfX8p^R#y`xA*NSGX}VOhk0`LV_r); zVO~#uscanUU97ycjO=a}0gJ=XXtZAxn&aD)q3=ROljAgAD6#o)&@l%dc)i25`G+}@ zgLJ4aB3`-TU7~5Bt{6Z>19Ex57U8!viF0uOQ46=Q^iyYIY$e4v6z5yie-NZy7c#W_ zFL}(BYhI(h_utzmO!p4svUReROBwFeM`>A$G_XA`ef}6$VbKqnu{5X=`&{QEli`0c zg^4smq|@)pj6OPhVwr%l*{F%vvsQox4>xB4*Hrw`hDfcT9%eI#d)IH1hx?Gq36ryeG+@J4h6&5Zmu@3MD{u3 z@BSoK+3B}gGycQgrF+`S;*NavB5tllg^3=A-+tu%wdS{$Wv3Cxk0Hrvz=1rD?auq7 z?mGi5Gt%|R+FAU<3@;nf3!a3qdop}U>&vcmxzER~TQOaR9oDVGbX$K{fT^Q?^xy8j z{ZHYk6c_tzmE+@M3Lq)hD@CjCHT{LhNg@d&au9t(k-oJ%G7g;rsXxgs`~SgMQFn@P3i*S|;D3I7! zv@f#KWnF%i?SqK>31; z8j&U`>W0wyQc&OX%A8nP&LyG96E2-2>%OITUkqyFlw_iJd{VbYB0<<}+7a&aRH*FF z?_gTyft?d}HlZ!R?45_vVvBv;yYiZ&Q)~7c%H5HqYjF(DChaV3b+5`-vm!#x*^&cJ zvH=&E(~@gO#vcr{s08?#DuyBuG^J8JRgZZZ(1XNAhx*G-FSGr=jJC@3<{BIvyzpsY z%T#lzd9LQgQ9Q0w7fTEXSasL=-xQvKx&f(QJeQ$whh9Pz|H|O189F(C1?)5LjPJh)a&xG32;0X{7 zp>p^3$jZzK5?)K8mPQJLDf^JZ7mPf@c@}TgQn6Wl>g^o$2sqj^>|n@Ul!D2sA3LU-Y^-tkz2qlBBbB!L0&Z)8cI$4KIioQ%McxBcriRou*IwlOr8h5 z1&MB)(cY^c3Ym9IWj{;f2$>JWbE6vA&oWcik@Jd0nE0)Cpeba$^v$?$2(%clW7Wn} znLzr}E`8L^O|rX3Zd6rjH>9}GKk;67v&s{s)|h!_Qs|HuU82lX(lSQNT3J`X`-;Pr znw!-(rnIe4TF>Q=i|&ihIq%`F9~S=;SN$+6pr|GcCo$;i5&V?GlLN9h@!hO>qenCW zp=LzwZPMDSNVy~a6%XSlnHGxf}!4N!?f0(4Rzg|sG@tL$KZJY zCt2eE{QZ$?0~5A7drfxy(taMLIKX?$Qs&WPLfZ+huxg2he8%b$hTs6(`~GD-QPToM z%Gc|5AB)zoF(O+mFK2+wCx7>BGa-SOwSj2_Jp5*M&wVmZqo&q67+SiMH5=>kjNeCY z?+LG(g;t0gNO6-T=go~dIMLsfX56@?c^Aq<4(P*tx$1(r;D4^N&*DW7bZ(BpH)Bf= z{q}5)Tsypwlxs@l>?#4h^3Lr(0V(1buO``^4*K7oj02=6_Mn3|htm$@r@=}K6E+F7 z_Vs~kfaRyC=@H)#NPuDJ@V8vIVDV-(hcr8kK-7l{Roxtu=(bFmgF9Y;TOkPfcFe*f z9E;Z?a4Y87u1s>c7reFcsnd5_`!;w9jv@0-67|14R)h;n3aI99aI@qCe2!3j5cJt^Dje?Hv0DVrst57_|9>(+D6CHKaK1 zy=Z07dNXR59bg&S-DKM$@dof8+n)L*__Cx4c(p5x5%mRMTm^y;aA8v=fo08p2f$`% z^Yxx%t{HA0^DSc?;o7Y(PLE#r0T&=uV2anZi4(DOnXmYy)XuXnq8$=v@9?4d*@sK( z_c4j?g(3xILBdse+pTY)z>A&SB017Ht=PYHo6dVSTuR=FJ#Tcy*U<}=jzusKQG@>%YHyE>rQ9qSK<5>5}?bIuvBmn@EmSkr% zCVvdW4SUzPCu=hsQU9t1OCZM}FE_`$5v-r9t!;uXN=K3IBF~7F#;5&4qQ0+w`6Q}? z8=wxmn1zmM8sYuE)F7T5FYhUiYSYiRLP~#q`H0lEH}0C>@Ib}j^(~mb0$ZOBzxfxg z?>5*n9xnO&?ADy8lcOk}l9|+C#P{di*2KHXWx4-jakw>D$HDiZh4{D@V$yo=$-i)@ z5@1`mR0YCp?3h?89+ZD8w~j?dG6n<$EC!1Gs6d>f(Qh(bUN>d&+B9`&2fy3=A|AP_ z!2Kec`7IUT{AnV|Z)Y-b^Y@*(LF6Y19UTyt$4MurUUZRe4t>SY%`YdNmr390^YI@` zJ8yFY0|?mA0HG|ZD*@BAw6tS#N$bO+#=f1@56;*QdHGo%_kmQ0eR!BClv`8{pbMMu ztDc@O?`86H(!`x~cFv$XVSd{$r}9hxYDgM{cWeLjpUo{bn(-X2k6wf~Ld~<=4_DCn|lmE8#emd`teFvzI z^Z~d0(XFgdK!jXMsQ10EF7i#P4?^mqw(ldIG^RVtCDmN+ZDg4nI{P7HY-&f1JtSJt zY$N=&QO*XG=1C%>`RY^t3;ZpO`kwnApc7ZFhUx(O@UH{_6S>IH10(#yOmY`0ddEfm zvg+54edS2gpGbXixB7)(x{6p`N7qpWVZJ$V9$xHln$OWjfF}}dAtRbuNp-4<`S_`8hN)Wg#TAjZ zSed`qM11k`*Hxg3O^Lccb!*vS&utAfdZCLvC2pl!IvW!m@AV*qrzf9^E5D>S)Jj&f zXW^f-gTs2K1NW+lNhz8T8BAISBfV>!5bD`hZ$r^oa`3|tgk6pxc=_FwU%zXN7{^R{ zhckGvsd_&$Z5wEi<;Z+oM7nV}T$nB3zB9YUAKAWDaAqqxqO$bE$XuCLo^o zm=jhuUT7Q9p~(zgCjJ=^-(#BZ)F=Z#Cp1tY0*IKf>LbKh6w?+Sx;plR1aC8?koicS zudC*2Z4^CUrIx80K+VKl`uQAoF)SpfMf<@5@WKIKFU!r8u9=|tGL!H_LRUBdf6Y)= znN)T-cx+!7dZNpF#})s?zSzp9@0(=lknj$?VIu(Z6USW((9yDZo#)NBCjCB?NBZ!K zOmrK3fBR_X!XLiE2T{{uc4^rQnTPJJ2SNCa)S^)`=@sl}h34c;xiTI+;_9=!C!K;f z4-X>90{zjhuOwenp5Q-Q2oO%yKfm7jp;2`L@$z|zPU+&wR0#>~i$#V#!d7zjcL<@5 zvkJY$e|UCC@={cqX=aAaJv{KPrHe$7q!pjZgxA0@S}Z)- zMW%i1*E>>Gp*q&J;YYb`9fz-(7<}p;X-BQ@xZG`dzRkBiReYh{M^iAxU!JO@{!lg` zmvTDMy<*1%j+z!=a6O0(tcib^{%SbFO~7nT93JL`lEN>sa0>q4;Ty(>#zr*J)OQZT zr)$SoXs2D5z13}9c1b7BvQoDu(~{1L_v`p|O7%q-WGiGk&aP!B$R^YO$oS<=l5%8S z@N~dLFE;q3mE)DB_N0&COqVO)Ys5DdTXD}C>a58UVaeBh6TAdoy;)TYShBz8=BtO; z^0QKG`}tGD6~DgI>PHpavpg7P68Ls5a8bl#ZuK z%eok1vNKC{W=LyhKW(ulJ3MGUE1lWB8lrjavq*b+4h_73p3vT4Y5yJkYCIR;Bl(>v zo8{N;f~uJ3w%eBGAqO5wUL+Z%tS^XtJ~mA2(2(E@y4KRyMbZ_4RiG@voN7AhN2??T z-qgTd7~$0DW`Z~p9QZD&F5xF@uQJ%LQC|KI^-K3VYg|%Z zx`QynMw$>m#41 zQ!^X6zlvFoKSfX8E2c^e&Gc|qr$>*<%NouSKIscFxupo?Ggtbl{k4&J=3x>UShULF zt+J-0=ZqkIl>w`;SFLAu(d!AD5A<5G!q9>V$_-`!m%Zf-0YzNNoMqXbEeNMoBeo`3 zVkiW*Ae*nW)D*Q{J06IK69ysuNUN2Mv(eMqr9%(@ok;Pb%UhaAf~L3* z%AGm0C&Od!y(VsTdzc6SY-V2i;0JV?6SVDSl=#VeKf!+=U8a9&`qVuj1lJl+7*Opw zx(NG~E6A`=V?>#SS}upWKD|j}+H5J^NVRVR4W%pe7|Jd#1u;iGwj>trA@323KTC5T z$=`llTKYc&>pjpK!g!>8I);vDTI&B1J;IKBu$WRpGBGXWv7GfC4*!g;vuPEksQ88n z`Oa`9;WaF{q0H>|sbAA7j&G!xZ;HrE=hA^AM3Q4hNTdv#clsORn%7lz>xD{1+CQ>6 zU&JX|sLl$tK=eUx=a9uYyfy;s$YpwYF{bhX6p9;U5+l*_33q3oo>%exDfE$eDb0c+DLP9&mzLFr zXx{RdYyN$cHVx)Ahe?a>h7DXsdsMW3uZVVqKHkol%a%RciYX&s4rZYglm06Z;9mM6 z^Bg;B#JTY3=lXCaL9TAwx0Ft@+B7c@TJWPNa0ILjOfM}ys*h42NM$nt@wcx%r#;^} zwtr2OCH2-7MEA&s%nR-I9_jdTgj^)6F=osX!@-*U>5xCuKEI%ylTW62`FP;;kv5a< z=%w^I?GFE9wySR#OkzsVF3uodLpRt$&s=7hbLUyg&%W&R(NL*)uBbH%%oK-@d7Exj zA$LN^XhOx8hljm*wBY-vJAAgn*T0)v(YI;QS0WG9Py0^?xuGj%^A zC3j-ollV5GIo?_(NlVE>_d+tr1p-PBf?>_e`NtLL@;4%2Fj!zRK0p^5ab3oSEpgEn zSIx#JY-}dd5B|E^bH&lA;>NxowA^HZuUr8qYL^!;#EQnrnN!9eg}Sp?eqhUYO0E2S7+31d=F1le#`3J`pEVgTsZ zP`d;>MJbXfrx>kDzch~34|MMJvQ{5+y$7gsTIz>*%3ixIzFnyg9u#4EZ1$&Ln5O0H zGsKM3%I3=p%`A3%v*^!!m zV7RK%wT|1g33&yy8ivN(dMRGjXZ(qwu7}|^^#blcxn|hqC`{Al&PaZmUGhkM~A#NWw%mv>Z=$&^#pLic3nMrTt&*yv z>uRo_0q;+%cs2u@Oe-p~^GD*c`xlyB6eD!_k4B}cU$#^>EnX7CDYIXE=8gK+?p*jH zSi$3=*6V~%NuQpYX!VjzrRRG*%5@#9{B2=LRg$kqI(lO-;&ol2zS}#aZwKIWiuUa{ zj1vxRAe-DGyh*iB)l7m#qq7}gq0{B1JR5JGKxrP5{r`NtcCT;ypvR7fCRJ!{h`6u7 zt2+YA=m)pe11%s?n}T*EWnIIBL|i6Uu6za@&5YAv82m3azRnwRRGRUv@AyTgjO>!< zh#4PkOd=;V&LyCE)$P+_aK%#HO)J3-+xiAv5@ni>yUt>YadGOP_6_A(+{%@NAiP4w z%zS;*Tl)+J$)Jp|)#GOL6L47k7{Xg@$l?4yN^nHMa;+Wt@BeB$s5)EmIfv4xTd#!4 z0(x)}XS2dC1dE>Ey?M7IR+ehx7Lc%?s zJJL(JqdI++C*Q4Ih&tOeK#-xsbkV@&<5eX}cITkSE1%+DUqQMTuiNcQ%@%DRotK2a zui%2~gxbJMaq)EmQG0NeetkAaD(~Pn_Y2g^r1a9@i3%XN=K~viro47pxD>HS@629c ziel@;UGnfl!HLjgloT~aWv%Jf0VGCs>BYJ~#s8MfJ*8=@P(uq9rc(*TWt}V{^*q7- zM`&qEyo3xh3ecaif*C^&K0i1C+#CWd|J+2)kj%U2HNpe-iZS6h z!^44e)>d->(0F+;y&L~N&@>%%VNTdE##8RxJ*YhL9V~HDn|LD%YU&$sW>y^!5!4EP zVB2&w{(JEkPbx^KEHv#BbeE|c$5uGo68sSqa+R#}O!Uj#nLDW|QVpGY{7iA^KQ6>& zz5cPo7sDXOO3w#E>&-SH^DUP!Sk98{4ad&s+IWC_eP>p(WskiCWX@(jqHs<-27=t5 z#)F!Cxdu~7t_!=_RJjME>|y`pFKO7`jftW13-RhPstY0p+k?qjD`Kr!w;vJQKYD|v1r@0$`_LUqlL%C z+2zY4g`d*{ZZ4M^WA6YPb*qS1n)wHQ#BYu?X?T2JTWD{|I@Ixm;a}z49gWvNhS%z1 z+sDGNU+Q|HZFj{@w@~I`Yg{m3rhsBT6^LR4a+M`t8X>Nd(IX#ql0{{4`rq5;^pgZb zm4o|QisE4JM}Vgb8uTfndhX9yCEGtp9V<*j0*>g!KP`ulq8OZ9Cuta~v!xB)|FW>j z#C)fNtd2sYJ~$%MtwVi;dxhD}Ih-{|%q^~0zO$3LTf1lpH_9SJSSDUWelR*MuT+k4 znN@Qj>}C3ZHELXRPieJ4BJKUBo)#7BcF*EY6ZWz5Ch=w4MLzfRN`uMIHKIo;$MPBq z`QsE`<`c~?Mm4U=`E_z9S^8;ac0yurxxe&2|HK+&`_JNvO1dMN`3dG4zvGx3I}X`3 zS$=&}SP)iihas=c6`uf}M#!Ln^{<0JWc5};U6 zgMOiLRF5eLbeIYeb#dPA{~h$iMxt8u^Tef7o~uxP$5t4#_6?J0HVQ9>QmcPV=D`H) zT}O8N=C6@VT9&O{OD;)RQvHism)E@}0XFBEMQmtlrL@?|R(MPdJ{DT(3Q0t2BXz+C zaZ{Y=H-o*LH1EbiDlp?nUT<`HTz3p|uPj;;Qv|kXd7HEilsnXrt|Q2Xq;^O5xd->O zZ8}5rHcub&H8nq}i7K3kC3lE8G4xu=$NlF%+!B!igE2GiDARA)#8?1|>krUUc8R~fx4D*a}U@IdFp~c{T`>b_Int^?LwqSB|sfTBHt+&IJ zkoE?))#_LQv$RcL^EFYL{ul|Dl=xwi(5Sy7)lNU*=Y{GU)ny;G&Ceeb)eqS#|Hkw^lZrLiWARnbV-}=cPRi7FF8nDQ`Sgh?NN#)YX*YJIL$JRu< zCLH|-1lGw<`s|-W=bmSP9Ti{N{Jw(?0Iunux&rdAdXLj5`Ik->9nx|Zxzwtc34Xl? z8?BPyaZ9G2+rn4>hv)3v9LjK&-{*$;wTq+c$@5tMj0oRP>)p^&oe2>`u~Q-%io$(|)G2D^PnL$(ezj53F*qlY2MZ zS&Y=8M@Co^Jn^Gh#4qoo?{nt-=08%8oSH8Utk7? zx<^*>5o@{}=RKP?m&(;d>=K{Hn&ug>^x@QTU(1ylv&f0^Z6|MXYR{sQ=^*^atKW8a z-g$L}p$lNL;^SnrzY{X}2H6P>a=>A;XpG>Ks zj{8&KvR2+0AOF-h>}63=;J1xibin@r*=3Qowoq1&C0d%vwDhP;c{H;=8oIeNAY3~r zq*#g-zx~ceXZEgR%Z3$an_Jo;nb}S#Rt|jA;Wc`G8T zi&{o&gcy``iI~Lk|2=8JXl{4-w)3^@NuG?2@aGUG&sNI^tyt{tOl82m$9@75=`fh5 z^f#AX$J07M-y#~viBOgG+|RJHvk$S~<*bAmM-L@dhAM{7?T0=Bt@oXM-pz`0ik_e) zZ8YMuZdsLs#h-VNmJKFVZALYYepX|-V4hiC=x282)>Z)& zOkko-jjfTqx_CPxqT&5$LUcfnRtAM?Nnahq_62#yVG-DQ-Q^cs$QGl%CS=i8pan}q zmTimahqHYX47ENYp{{fN#^Bz`7Vg=0;)s7F2Eca5h__@1v~AvVGRZWi`Ky+risG5q zG}rd;Fb=1#mwxaUwQ+_!$c64gzXYCV19W{eg?-aAaL%aMlR(1JO)~o#EH@h`IP@k! zw+&6Q^IHtQD8}4-HMB~8a~gCKcyn-;L7RR4&{kFbC}1A~s}vh-Ngl!sU#Z+7A9Vik z;P|M+a@TkD&H-zAp_!quUCF^n?dQrPuL7LS>v%u&e#@VNt+Mqkj%8H+MU#b~q?fct zcseugX!p6a;8DVu&IOU4ah|C}YJf99G>+!mGU8ISyTN=xp0$i;Lb69+?5PS!1NHe? zx(@_a6@L;nFt6jj>XS6UH3UC#<11@l>A%f{!(U?`>ecQw{Xj9}7T1#OErH3f%!LdO zfK*J>v~Np(%X#og-3+lvnY6$12~dqV>@r!R%Fh?cF%AB99+T={1FsWxu33eqy_uga zjVD*cOc1d`1=TwH%Zq;WnciFgW^SoUv%V#1f48irT7;uoi0Grqm3bk%-8!#Pb%Zz#dR;|*Ml$bd--W0hjF3CnEu)eCpd2f= zLh?)@_3t+yap{QiDWP@ChwTq}hRoZ-{ixb>&n5O~%y`JI3pMlSV^X`Khra1_x0lJ~ zXO0wjF~tbS+Khx`hRKPO3I+URpqX);g?w^aYo(Tm7eYAkHK+G=KQD|w5hyjy+6W<2 zl8ND+@iWK;S`N$mm|0VR`&@khCYYb0+dAlpwRT#9m${j_9P_g@X7poa9zb_9udrqo(k2PsO&+!*MP9fY`2Dz(pLqGi2Yr(B~ZA8Nf!Nzr`{0G8;cb zpVmV&4HZ5iXS?ks(Vjewu4`NGlJ10*@QLe!uph)>FL_MThdjr)I_{3e2fQ;<08>e} z?)^E;XKi}|54g{|CzsJm3irr@4O{K(L72BUXKjGhK*sp|fV)U9lU%|pzimI|d_T>G zBSuOu^N$d>&`f3$dgF-ituf6JApGOwanZ(^y01p70`yx@zl(lqNqbUO|IM;QNpqlM zQy{CLb+Krz4v z)>3P`Eo|wU|B?mLKWbLb%Mj?C(s!Pxs>JEaikl;TS(&ukmJAy)S)FJ(nb47B8vo!Y zhIBX+5`wvD?B;dB_Q{0efvf6=fcUZpM;7Nw0RJts$U`Kf8P)g3Swo`%+wbNqhqrFw zA|xXQS~D&}acgKw7H_ZfRyKMXMtDak;0VtS4aiFqP529A5z&vH4Qb!ClT}NQBANyj z0?+>#t!=f8QrI!k>94i5M*3cbJYO=t7i`NF-Q1qMrf}mvMs0`~-$dTh9al5k_~!ox zl3vf*$ewC{UH_bgqJROh(N{r<8+&Qo$#Q#tYje-eQ(|1n^)@@V1KQc?Ly2~9&tKSz z4FG)ytrj6rKXMlkuzE*!!1>b7aFx;z50fr_9wIsad9*ZrF~VVSlX{0)TvDdhfz3YM zG(9@@o;{g*v#y2fyb@O$Ow){y72zt{Dhn)Rn_B&Bp;w(Bl^SrhXEWs1o!l%D}+ST?})mUk0&R_=@%p z>{XlYd5x9-rD1#@1omdkX$?9x>5j?>5N;gX&Vgs>9%KrU_S9cUihnbI-*3?&t1j7+ zDO-a(pAL8wE=htC@Lb@NyQl8WDkIXW>U#TRbEs*JO*X<4%^0xg2=zbh4gEB4-4WE> zYI%#T#Ccpk4NyYo@hv6Mi zULHYjSz+78w0}k@fXv96tH21q*Mf#vv+H=x8)G(e)_gwq34egw76o|rM+J6FChB~+ zxhk|ajOqSp=*k{#q2e6^RtsjaXEmRA_HwZ+axy>gvZE>N>Irj)ID-QXzVb|W2U;Ri zVp}yuxW3@td!MkBk2SV@P-j>6H&7UQ?1X%(@C6C>ZpkzBmK{2?X9~^IDA@i&+g9&Z z1)tSDCaDWznwsw)IoOH@37G!u#&`R4UDKOUWey9FxAsTUP(Q;z_y2T!k2XpFKnN&P zz_6zfXdJ)u!~X4YF`LycgX~Kf3BPDfof;5u?q9Uf{n^OWaD5$eBI&YT;K+w+H#!rGtPn1%@pR{KwVbzF|89o^Y)H5ItWl0H2x-l5bYfj_4#8zlvTwVb0{nCRhd>pT7SIw9i?=&hyYFHcATBBz@j3_V3$3v@j0chzU zKj^!3>1twNwh&P-@{w+`=5{fv7^{4ppoBv5r6x9KPnnbsEq1wf4QmkE9Ge8GnofV!-GBVt+u6An|Fq@58e>`T znUH74Sq()s8wQ2&wc6Xn`K-yc}1b4r0H?!$g1jenpNGui~?LJ2!A3h*tm|dY_vr zYBlwTCNk{+kU$y@Yt`S12^@3A3xt?x#rqNitBs6L^TF-V+sJ zR_p2MPHWH7kIVblwzvIcXATX23P5v|nNc|#4Jkir{6~%Jo0FKS>^2-2m_Cj;G=sN( zW(4R>!RN~&RVUy1iiun$i`QGNSCta`tlTz^IBjo=OQjWeqSHYZXD-}(^|4K_g>%G% z9TvWnOYYTR+M1A0mdw7<3Y|Zr3&Jrqc4wArN=E@CW1>=EGbu2jTE;^q1J~$7!!@0H zt?g{%oX-m4sbtz!>|TPN$D=8dDVw4Z<$^?#Du(^4!!bXYbSkF0*q$Pt4Hq+2 zUsZN2PL-d9XNg)&qc(J}&4h2?^el#z=~P)?Dj|wDXPRX_|I$mHH)@ZG9Z1gdKd|9i z`?VKl+~aw5u5sLLXY}v-+Ek;23t=X;c8eWtZ|4pGnQyh5#8WmI|BH<-B6~Lz}N$ zxO@+HNjd1`bpooBYR;~wRK#(wwi*#R{zd2iZx#Spo*b(K%WWwGhePGr_9%=b_fqNl=#*ecwaN z64d?WwttiBc&T!+yOT4Yx5IFxO1J{$4zshvycva+C9gH=T5hbYvTrNT^~KB5gzih=lbpy&?PkitZXdtuin#Zwj6k1wlFKH{Hve4RdguwNPD`DWVfgLbIBad^jhS8R8)RLg@l z&}R6bT$p%cspkM8+TY8q-4NKKBV+0>F*k6?mh5kO(wekR=et>~nKHxNZJlA%`3F_8 zU+8b(^u5yP>}sv6z%H#4K{S`T<}C12QPl@x5e9sDteMgF7Mb$Zwf2qJUOwjeUzdNF zh5^+cQrG8Z7?ZSc*?K#%gpjjLnHsK?*Ao#<@?WaX#>9z!a;kcxTDtndm^mC`gc|j` zr+1m{3k!!sL_Ksjw=b?qcFV zOu63*^-bnQq*aL?Oz7AX%1C?B=n~qQu$y@!EaB@U`b%Oy%Y2s+Mpvc7C_eQ1;T6#E zYS><0H(&CQnRzij1d3iX!HCvBEwp}&44ap~B95v@Db?JSJn1$Y?;Yf3 z%Q$3&W3eHzzazw3)x?!M3xVf@D3o*vb|o(r*kSGKpw5`H6{O$HpUt|`M*T|MD4nlw zFoZ?Ci}0T%SZ8P|D`7>E3Pt&!+xk}E?9R{A;BGdU_=Oe1pv&xq$i1bC2E+w=&f`8i z(wULg=qUcBjeGtcxI8p{eC{2b=GP7dG`M8F6Wq9dLL$S7SZbn;o~iezY#4ycmz^Q4_F;q z-xzrmZ2J}$K~W75(?$yVvG!B%noJx9`f>FJl2roe`j)=V4e_GR_LriRfr3ZbiB$F< zw2zhs6HKuu^YgjQ-mLj^6@o5TB)0%W?u0v5l5I{!-5iy2hQhHV;(@dxv>FH#O#e*nCn?Zai}`YB?ulSxPBl_BUiykE5Rvz4u8V zqUDN6eBXuB4-iS5t zXPND?W6fCoL?$~tVr-U%lNe9fDE1Pc^oqGxl0EH(ZK%zX=%L=^tT|Z11Yi5YD~5x> z)=6u(fR`b?bgLQXA1O|Rq#;9WeQv9JG!Tkg=NraV?u|%UacV^yt5(F28%JpS3Cc^&bCxiv2V{Qyn3Q`?{D7fM~V8q{~Vd!JLdq{4Dls$?RSAFq}L zgOg8%%MfadAn(*}Vf7QirDvOiI$Vn4aP$vjcN>vaRh{haNrd}Eu9!K4YpI^NGL$iy zdv`I|u`U2yb~E5QD&ISP@V3GVtEef7>ZCy83Hh&tzqsJhHc&9%NU{{K5R+5-^R3b! ztqnzdx{MjNCLKF&aSm(!&A(bR*jKwVLmg?e?QC^G14DL;vmJta&5!eL5~*2pw^KqN zc1^EzagkawXwW;Iycy~xbQKDVNzt8VFBSe9UZFpaz5$&dbPxVqZK1of$=iIZz2nG= z|4_m-Gi`5rpguk0`grk*31vE9D(s!R))U8~Rq*IF_^Xq%V-AdgiLwJ-s%tdfkou8X zelYXuRZm+O8s`5qV86|EY7sT7Ho0XCo(~W(TWVw3rW3Ms4gj^c&M>uIZ(N|9ErB=x zt`|!xo*S|4vjER$l3Aj&dL1g<*UVr(1_OK%pd+R$*w)0fTM(5D^UZdlcrAWd>F`0Z zym^L_pJ9+I9ewAE$2q)zsq%x4RYk2&QFt{hCzDUXM);$5R=&Of1f zQF>!#0H#$DN>yj*^*FKTo6_0o^X*r%M8*6_<96F%7kw?WaNlG0VL=G8T`PRJ~!;&+(_bLrA32 zFe=FZEe1j+B6%zx{3lENHg&b!sESj>zj3suw3+zMo>&j z&U!q;FA&`f^K-YhDKlVizK{+%kdP1Xm&||99;i~NMTc~4ztALE%abW_=}jTBAuzxF znIFVr+%YFsN%%EqNTkjCLyV)er(@A`Z0N!K%%QPcQ$KDSVZWk+FL+pLy#A5@^6$$r zIwYNnPxmuaS?i6bao$$PTr;qXEz)kPp7!nL7nv)Hl6$v zle~>?kNn)>NF0h3hH=LUF1joslv|5_OUsCaMv=z$D5nV<)41zY9q;kaq=mgmxAVbi zA&VwBAl98=Lc^zroQdt$l9qCR=(LMqYL^+2CJw! zF0HI>DAV%3&G458!cHHnXNM)BX(ZWQY}9W~FMXZ7HrLw~+L zFjkgisxL_vE^hh(f}e1Ge|6XHER@?yd)bGgD=J4iC{$6;O~ic7PTMsRpJ9;dcC=6w zWa2AHOzIMcD?Z(=KJAdcVmOGt=UcSSGAlh+eq*#(J>f0x`YD4yGrC=?OL~eUg}cr} zfv)f3-w?2ExMWyZl1Zwa9>$bUrgfzE{kftvTEi0kU2&i515>uFm4jh(uv)K(>)+SB zJxR z_b_yjrXJD99iTEvIC!;nzjK06(rpap~y?O8fPkBmB$6R?vj}`be!F>}9+=2CbQ^lDMlY70JJX z7wW^Q^5?U;FXKY^jTriBS`IkQrLfoN!bEFn5kY9rc3!gcqR?Hy8u~XKSbdxy(DaS!Bm`-!ptiXdUH0VrCt7K)^q$;`4sgg z;}3`~b*?_>n9V9%{?FQ@JONZg>GW!SrCGD&z2p7vjcxj4Q~JRvD^R61eU^X;b9h*L zKkXiXZR@r7$rq`BR_@!e^0WvIPLH$k{M(d`NW)3d1JzY2 z3{=^}3eDcDy}zT13$N`wrjKTlF+Y)neSgh9Umf{F2uo<;ofAV8ZECjzrA^~W_*9hH zG~5=g9`0W6e5vH4rq<>>`j|f=@#o8Tvoq-X?HI5UUhh_;x%Yrp164w({1+BvA}))-UUhaZjKtJa zb;dH2kRLp^H=Q%)F20p`2MG;O7P{#M@ND;dSRi$H+)JOMme!P&W*4p zAR+}+>e(^Q0MmKJwL`ongmeA?KjKLTdA~*6LB=CHT*uKb>~@I)cn=!vam5F+0g8hk zk%vXj^zUi9!)p2PRv2f5nMoB8h|WUIw+2e9Nc9~MIA>YI66+$zpe%w2#}APWm~W0C$+$_2 zw@$i!U{VSq)z#O2j9KTdkeShlVAKYY?l97>wz`d9^ z&wHCnB^P@n%R0`M&tw#8Kw>G~8$_Nje@>7B3pLWZEI{Y~(K^eM^9W)TUgoberlU_h zeZ*lZlCs&fx=a5xyN4a{rUL%9FH-v}y!wHJcITCj#l#;8?ye^YQ~dPlM-S}R=^w$Z znIjBwcmc{dF~e=Mrt7cs#~xGaqgc`mdf8V2s{1@{NJTbF`7CRYWawDJJu*%t8Z7HY z(r#7f_u6lyuK&-_(=M*-aalespsC5OQ0tJUJl0+Z2-ur&62QsRLeZ)@);$#V8&S>( z{6naB$!$R0Ra(C5XMgXt=?t%BF0;jp@E*02zuKa?DRmNjc0|4?5+G$Paw`zxR$$#Q zCb|4+0#r%w+Iq3)u7uoR%6`4+$b3k|+J%?l^rujGiCq`}rk3aF17}-8#!*6sgvo)g zYpBsqk8UX~>?v*}K0Oy(hLt!tnj9<&SQo5@@`XGITDiR;yuqfwR6SSP?_@(X4vHxy z6!mdGkf^xZ0(%HuDDE0u_PgF#K%N{0q*)|0tMe%q=ht=pbqZKXd1a$LV<96NY97JW z8(lRv_1-LqqU~s!S7E~cC!lPM2os~A%szfOH@$38Ad>w3GL6?{)S-j$BjL|)bPO8k zGr>BCN7fR}tA6F9k+n(=n(~(Rze`A3whzC(j28BA3cY^~&incKNs#=8V5oRr1Cv03 zfdLbuTSbKQ;j_-#4b}Y?DE<~%p;xpkmnWMvZ(!$hq&O2+UmOU!FeXFkPXN*YLlf4c zx}I1g|1y-ZVJ!JV|69jfZ;G6daF*&nMgH)xDhgvn;OXf4kRTmy&aqoT`r_$JtA65~ zLSQY!aaTYdy?*cF?Fa0j%mP9zofcX8CeCw^Tm@5WO3`gWqs&HN8@=dFli zFG=MnqP$QrzGWJ~Oag^go;ZrI@GEv4{1g8Pb@F&OWH4)&*biP26H*dy_Y;ywOdAp= zY_@7Z-GtBHLx=@KCy~H14drW{^KI`NQA2}1gE`$cw|B{!a=mT$wyU$_JooS99o0>b zWJLm;aAoAvdZY*iMLwS=GEI531W{sLnJCkt6@Jf_8k218BS-9tmR|2?h@un>rC-Uo zJkQh&0dy=uJ`~Y=`sZi>PA6nT8ip-vdFyM&qjzWU-STAocltAs|g#WPGrW_J-7<`< z!Wt(?YAVS#T3*Jwh3^Qhf=f%a^B66lq-)>sawY_DXJ2O?g68z<>SbJ$liOvxH}Yw( zwONyhSkyQF99Zf;AYzHvj@3K*pmlPtO3C!-+T9m=UpirE@BV>)R)xMv} z?`BeQ>t>EiPaEly{WLOj0>T*~UFsEu@^-F%ez zEMV&{a*H=qbS{*kbol%06zRf$0SdePGc3m7X2Q%}93N22uIkB=Qy-*!%ClttL#A-{ z6#Pij%@Q#6m7NL;p5O8IE*!gE{(HRR>p!{cfiBwUt+b!{l_vQT(QwN5M#xJ7yQ2Kt zl`89Us+aPFIlRayAL6v2;?+=-x5l=5wihmK1YN-&eCd9OrA(HJk~4 z_IEbeGqpcZ@SrEb$15HUd|mHIE#^$5aPFdjY3kBS1v9q-N5BZ?!7ks?aeQ9N-2qb@ zTmkb`Dep^HpO6$`O847PzV?J%@^K(q7fT0}W)iBx|0G=AlDJ-%geR5q0_8UQ)s>tm zpPY&I=7OCe{J;_FGtTTI$k6WjMJ(7f89f-{_d)7e!Q5VAXWa;Fctvf0W4wJ*Mp87a zHGh~$Y*U_-i|G%tv6IiF5RD2fd|2i6j?@g~w_!UITGyK^S!p67EGEMrF&szK{@p$Z zver=Am2z#6gEaL|V~O%*RO|k#zqSbZxdxkpH&pri+DTo@tAf>9LMiplakG$wsG0 z&Pq#Q6HK){w(9@%9R2@EJQM|f#S=nHWZfiN(B0_Oe@84)WZ}{PJJH>x{itr?O47`; zPS9Q8Sv;yZcar(eo1|ceuiY^SXLquX@553{Q?>u)kF#rqnc0<&cnU3ZH325fXA06S za@Iomff7vqjp0zdo`d7c^XsF~5Jkm(-n5l$3btOWazPWqg!E%hW4bK-t~A?LqH_ZVuz0?A9}h7z>>M&nsWeAmiCg!)!J4@ees2jrv-! zoaOnC+Y&nDl#Ckm<>ci_URC$n%y*&q)d#rAmwGk_kbg=y_`;^{s6`FW4s~#^%2FtB z^^R!<`zzfKmVK+O??HlRy9pR*i`D%=7x&}YTu5*XhI~|{!?#UNPEK=0JQTgcQH8L+ zwT$lg%JzQlU)kXuKStI3#ex`5$K?j3n8FtKU^P83&v_KkInY~Zq9@_j{I%H{LLd zmJ8zL4*H#fxb1Huy8{doOclBN6`_`O#4e)P;3k-aP~Q8o#bW67n8uMwzTicgKK7@0 z$ouOCj%r;ug}k7NxbD$yYV5;mzH%yMpv}#F%IEEL6NqXOfCUXU>*4uakeQk3@6HPQ z)>|W>(~p3QS2Z&?Ba<5m-4%qmFen-P00FS`VnS`lbk)X%;AHJXU%7EHnaUcug+# z_gepFRYq|VUJv3V6n{XCs|UNSfX%t}pbC{E!dQw}Bc++F=8Z8ez43%i7lV?@m+n_n z>3Y>z3eWgxNY@`38m^F8G)OxCW&i+V2&Y-nkC>R>boV zmEC=vSNQHHj>~6hZ@l<-eAMJcx}w`xl}J%qJfss>7=xh0rDMf?k6pR^3jL#4DsW(j z)Z5H2ADo<=Y^Xuy`JF{d#DBjJZdo6S9GHLoqo{K40-xCSxP;D<4)iz0VOO!qdAnZwxhnPe30T0$d&p%E%Mi+^$^bVZ*~mY zUbB{oL-;2}^t1%mts)oA>^O+@)jnPO7Ydf(SQR6yHE9)51RcPzjzx~qoYq)f2#Z5D zJRk$`9sEHeVNBL_PZpOjYi`S0FaZ8>s@peEY2x55;bwZeM7c!33i!e(TZcb6+o=j$ zfcdtLhUwzw(8e_5tz`DZ;}~)qO1wQydaNd%CB+y^g|9k!@S}U&d5!f{a*>uSUM`6f zOejK%_xq-|yIkz>C;WX!mp*Zxql5_MQDVS~(Z171F@bBt^WOCAg55{^EG*Ep$}=^? zgT*#F?BXDOv%HQVJp=tzSLqr7WkBzcz{jp~!J9dYY%mP`=uCz(fu%PRpPC~u6yVqE z^Fie&*iOY$MIk_ID&WKJj%2y*vx*^mY7sRq`3orW;Kxa}@!&Hm0R2LNU?RmTJ~bYx zlpq)%?r@C5r;$I`{P|M&6!R=*b~rB+G|CF5ogQ(>NxLC;rU^TI6pP@mezrRs;~&lT zCkb_!Vk4GGC*0JhghM%~GE$?VVa>iItK#AgIPOsn>iZr;j<}$Gbn1-5!=Z8mf~=(w`uODLK35EAp`{W3wCAA% zCWN*n1DkJu(jKmGqn5u#TOB@k+%YU@^c%%pq?epQ_5{3LlunjHI=YL91GENp{vLC9 z-YT$k$1vXYk~fH%GMZIfnpxJT!Oc0WNxbnxf2xsv#ikscill^zm0 zxk?+$aL@X;b;Mlhn|X&xd&>ddVz+xT?`6K~qbmtcBJ8YlrEz_abqa^N|p z@M2I|9LW7*EihIA*vIvdN&)nb2PS`iDZIKABHD|>o(X@sjLO|99Sr)X)nTD@9L%EC zl9Mc#8H^Luu4L58g`dhX$Z>uyo=U-$-N_-?77cnz0^e zBzxKEsd;Xwz-9{JcHqWOZD0UEN6DdXl(jsc)dJPG;$Ll!G?tPVj1O!9vuB6(PK}(L z`oPg6HHjH^J+#z|qDpH?x#-!(`Ynl&q({j{NA%$@iax9sP_!3`Pd`VbZ)xUr3>PEa zgnP`Z3K*c8G36}pHcfr0<$LO9EKlERXlS^^I=N=ajm75`l?rk#RBw?i_7a}jRq8?i z35mzj%EH8_xO1a5ZcK7ZhkQzM8ne z{2_$>bo+;#k==M==Mj2H{YD;J9}BKFu&0x1`IYaMAuF}&IG!NqKzqhir>8eN;mvVU z5WCgrwv@K7LpHJOnQmmWV=r5U0s@o{yU@hV9KmK(Du1gUY-p$S1n}?O} zWp+%2l})pp76p6X_we+Ty$IMYy+DDg6EEUv1_yJZa+mc_7;m5CYg{oy)=VH1(dfi{ zr7>auJefx(y~jpGdoO5{xq_^<5M0M2d0Tuzai#af)AYkkq` z5MajLOCnF#-k7a5LAR{#H$eg6kUU~LuS3qRU982;103yJ1RT2jLLtBZw|Fw9a>-8O zdzWICLTh4iWS_Zx8pjvbCrE$Avx<<9@S5FzY`V(96tUtNe(Icg;1c57p#}5yvSI++ z?UUHG=iYzFxoIs_*mCN(Y?|Vd5WCXwH&}5grmHP$Iz|@79Lbk%sRxHYU>d0DukHZO zP$-J-O785h&W7TCfWYDn;l&`iZ}!O1E0VP)*3nYtsKC?v!R8+DdYcuC=bl8?;`_I* zQ4w;zE#AU++Hs#=4M6}~a)&C+E?(FNxz`#Eob>loGSJRrgRaguy4+M6OI}c2(8=?L z&K&O@*1b+(UTr?Q%(K9(l#Iqv$tWaM4-4)w={{%TI(#hOk6H1DNcRi2INOoLcQC$j z5mU5RY~-mMAFJgF_%s~VvGDK1H^Z1VCz>UtYT~8cbEqtIt$x~Y%2V3NW8TA1DEofn zwUgs`_7^`Xah^)usM#c(GFYF&Km#kYJrNbK{)bN5ll$e#zXx7I^Q35&hvs3M= zwG@iVZO)|$vwdZ|XF6_6vCMkOl8}qG(h2D7h^=HWZ(v8h@f?QpA zL=$?Jv_E)TqURPvD8Rhuqv_0+k|!1tB$ZoSVR~+UvQy%Kvp?bz8SU};8YYW^o*4v6 z6)-Dj$Wchr6iQh(>LJO5!FBc6PeJ z(BEpD9Hxcd@;=|Zc0%bXeVW*$uP7Y#hi1um8!_o5R)+QfF$#!; z>y`$Q2e%eBhlXYN?+EzZDMffaeCNIHJq=$;4@J%bn=l_y@RxLOJKg;C#U`arc4K+i zUKH(KqRp5F^9ik&I&S)~pL6QzH}1<1iN;%5Ot1Z7(CuK(>eOOhMTJ8uyNBo*ElQeeB`h4iPNz6>U}!IY~pajm_ZsN;7If`lo^@>tHDr+jqHPTju>jnEBx)Lngh0pP-m*N z=X~G zYkkO=>pPVS(Qccua#E@ho&^XM7$(N-&bot^VbBU9uhsdU z86AvD%ltWa_Ql9Ux{lDZw)XqFwqnrT)qQ|UeZwmJRk%`kcrBF-l{Z6Aj%CEQ3O5|* zo-Puua;t**{2#DR=l=V1MDht1l!vQ8CcSdIBU~*?7q095#&5&-g4EhMh#=fq2?x#_ zTyV6+JxXg#=*@QY5YFz{)jY&~G->A9gSu@CR}SDza*Eq_)oR0xIzZQvfpK)NKM^FX zh2Pq^FRgAGRfeC(@0aO{R16@)8%^>M*!Qhj!fC^ zCGFs^_n^?r&Z+AtCmJQk{moW7x)W`8qeL8K5z}%y#i?p5E3enRZ$X%_F@TU}#{ynT z@hF+SYA&D>^J6(8oCd+G3^Q&Mm1wtA)Hu?%2HFuidtUQd{X=)pCpP{n#QswwR~Y99 zwHSXL^i@2ux;wK551$- z%-T;1R513u51z5|zdw%~L{2|H(FeQeVIL0E|$`*;`ZS2D7!cx#J;U$aM{ zB5Ljd(sPJt84JUe0f;nE^YN=%)@hcG;<;f#TV-1duN;Up_sr-u{&M+}ya&ofi4Q{A zL}O9Wc5pdkbz5N_Y@NHs)l$E;(qN&GNM2_AT_=v)3$lK}m=D@CQ=kkGAs(zlBA#;k%dN+DiH^l5%lzvAp$ z_$o{XYAZ|;MkziNXN3?%Evq9^XhLmbk+~)?&ZYYyBx?5;-b+g6v4JiP zNNijJg%)>4dsXHwLF=O9mF@zCm=cBl=320C4&CSPq^a@*YLE;F{@d^$&r`zAitpQE zRNMLQE;=UjM;SJ3L!TZ9)D|UqaX&6C5l<3bczA)^S8$gRfH$`reEBmpw%Sb_V5>}{ z^oFi2iDJ_+3dcfl=3h=@xfXO~(hFeKPN2Lz7--*(RsO=0+}5d-LvIQ}YC=9lU7@cR z+Al}&U<|BW?7eUCi$$yUo{4Si6KJ*(rx%2x*5!N29gEoMACPh;5D~nBc8B#Y*;L?c zizM<;Qavkrany`uAS1=;jd;U%Qj}*Y`vOMWu{DLCQ6Im)NTZyS%RI==w-Dz9f1bY?n&Wt1(gzU`hDf9iaBMBrj@X<~HkiHXJaFC!Z; zZ=UPh?gG4#8bG-`vqmC)_D~Ii()S8Mz@Z5Q&W@tBnTgfLmNAtN8pT_4Wx@ zltSs3e&Sb=u;c!wCjtM8qw2lD5jD*N9FGU~y_+6;fN6b-$Q#?nBjYSz0M0EAR%HbA zahZOQf1^4x@TFzJtnRT>-Dht)%I)V|5_F4y?>_U&QPIeWDfP#YrXcpf9f>-qWnZ62 z-Jrig41@$0k~?QTzuu3g;;Y;wk9kxyKKez3Y@#}!!h>nqo#+`H+xM&tzbHzEO69Bx zdL(7k3aK3Q7a+tOsw}uXsGj3Uf8-up^HJfmpNLeMG)0(YBYA@vS{+A*3Pk`?4WHZI zLs&>CLb>F>vpAFbQ;~olGV)wd8Mjj@0<5|1q`?)U@8~%pxOr*M>Ad>d=x___my}Xp zJE9lE97-v%I2a@`sK^BfheHwKkQrXYm7-$v)6Fc1g?wPfD$gMeKpY`0(h@)?mT1qxaOaaK-K&Yi6-(Ihda_IvOIC0eLQ0y=xLrz>&Av^4+#GbYayUGH9R6ZQ zm14CD9h6g5M&hNueC#j?!5d(SrxaosPjYHOKaDFMgYMRK(LF1=#^Sk0iIECUo*s2M;BA3WV zkEDq)ygXyPGi)z8i{R>`-RzGB6oD)0JHT<36#{}tqw1z2vuu688q!bFRv?%9NRM#W z=D8VrA_b^Yku-aAz~88#fA_I9usQf$t~6M;SBrM4M~ik7Xks#Fw{@_%O@?l0ZD5${ zWmQ22Ed%A*8J;JW?}kADe`?RiIH^r)c5c9_Ayo%&C>e)k_9@@-^Q+TkZ9uxX6mJzX8g4`Ey`f4L5Ij+$mz`v zpEA4{t5@4pYu{$AyPSTQYqi!&`tnxRYsor!X~rPnQ;w6&%4veAvJwfAwS)xRXZ?fy z<_88Rc|&*7;N@ul=;r-roP)gs4nsTZ;-F@dpY6z#i_RhO)-EeQqF}MflTC-0FR^&4 zNsfLb3pT!ZFr3E*S&>T2&a-k(l&MVUaO^0^rtX%yYBD+mxTW!!$WpnQ;@p;79Ps+tK#>ab~6)X`@cB!A-x{$wuYY zwU%h^1&yT&o83o^a;mQBA_|zjcs3{z#{_h5QAt?s_53OL-bD`c?_{@F=|t3NaIDBZ z0AHbi9B$#x?&D%k6Yy1PRBzVl$T9hamHT^TMi)DNpvGWsXWqA~8Jex(76sK5q% zorsS~Gu=ak8_hKQnZ=&`*o`53yH6uBGc+PzYr8>Gao<>CMJ(;DmNfw0$=1ibFAuY~ zo)#E@?WDYBoWDppSs2aw?oA(UECenlk)tf?j3Ei&db$UyE_pTm%Wv>h4m5eo$4CnU z1|IJzb^W9+1!X^uW(Z+DoQ8#{$8;_jfLA| zDrElDKS~&O7!ibLA^*ANJxP$7^A>|MjEb|Wm5$;3tBrH(^;s#gKYD zw56Ra7P{uxR0Olx96=7Yoqm?YZF|)*S#c$GF>Tz{50ECp>kAYwBS9_Mh=?KwI6jHGU|Dwen@3ZeXV>#g15}YuV%IcCx-_JVyGoTSS+$Ru z_)rRP2}c2d9u?umqr9hnoAV+-P9!$^tE5gNd^bRX85#m2Q2*q zj+ss2GAp6{pj+g^4<16qfEyp}gY&2tWaO6i`&ajwS3{l@pii>P56&b~LV!7E*;@3u+xMX&z}+ZhfF@8cqHNwXf9pY*wQTR7L< zRTw_^G;L?qykH~db$XN&$J?Y zVRJZhsQ7~+ARVmDZj=4T;e4mHFg2d(I{bASSfqdPbG;U5 znTRJf1M5pB z^udY>-DIaaIgWqeO1CX)QbuxpJ^HrXJ*v1r0w8J#3`BjRSW=(7sBJ`=Cl1XI=HA(=dw;XMP>WO{D1q*b_n&4@M0$m>2T??!@jX{Ta?MC^`EN-R(CQXA`^d} z5MPJ$99oRoJ@)&=f3$j7bW%nE4`;i1zv)?|@$BVvvv9$;1>kDAv)&<}oMzM&%VYn0 zMA`}@&u(F0(?l;}V|OKX6}Z81di55^L-x#7^Un;pQ8#ahXz3_!5jM`{;VT9;*48S= zsKk$)%B6m8RJ7!`=DQz3%==JT&CP-r@3)Dw)%iYpW^?s-gZcbTIDV4!{grpv0nomD zw&UgblH}n7>oN=(ia);^d1=vu>EXKEyEm}rXUYhoo+%&1wPPf2kF_Np&5cRl>ed2| zsKVc#;b7y*05)CX6JP7;9o;>Ta85uMI50h^oWWalQ?&FQIts6+!L-D%IRUl>T;q3-qj@ZC+vU2`a5;H2%dJbt8_UJs@qZWDfU zK380gd@)$MF#gByYS;PCl4X&-k` z78iMaUnFdG4@taRyk0=vSX>@>-kp1rG`V^K3GOyhF43t!x5ngc%Sz2`%XfKT?lF7bwrnwfMV^3EFC|MrJ=ARseJk`j?r}aR z`n2VKzZnYuJbCY&Z3`FQ`rOVw*YHW?{ZY zC*FV!mDK~B$AOPI+72h@%<3YRuZqd$9-}_;My$f9_n25X0tosx?d`G43Eg_9G&=cz zI#9MFSElUEoqPN#k!=H_--h>@q#({j*z%s>KX!YC zo7%SabI`PI~$KpARN>%ecFVhY^$*5=Uod zOe`7XxfBzf>rMn>GJf~JC9N8?Hi{}~Z;Cjx=NRhlfB0>FqRiTv-6!VRs=kF-i~DrV zH*EwpPgDX<=jfOQ(nz{1JPq4ZWfm3LZ6bH>$vo>T%8Odm0{jSGz zq^(G>RLI*s=W@&aq96v>t#3DpFMj`|ph0Lo65pY>{URQ6udx%|!|V$9b`0n{?=JK2 zOb|vpPv1D>+b=2ky@)ck7du(RgkK+>SI^8@n*5cy$_b*<*4K2*q< zhG2KD22s1uvSRVsIG4+WY&1*cj02y3@m;(5;D7z{jgD+j^|yQg^M0JSkgteRKnjpf zBm3K6cieuJ8iEMks+_&3P;^`|pks4b_}s!-5njQsBB;14~@G>;$Qq3vjzvV9pSLDn|;77$P{I9W)Q)jc3zKPcOb>#qbV>; z*r$4Xkl*v`KNFs)AwUO*&!HlZ89aco|2yHak6+@R5;kptaGQE3jTaC`3R%Z#NqJ2m z0>?j`U}D8NaJa_B=5~zYURj3lwxa6Tec`PwW9Z+PIQPmZ!|9vkDv{U26ud7jDE@5R zFABk8*?oAC`nswy@`VXtYX&xeh}g>(?JB^)8X2OJAMspbe{YY+BqQQCt$C%Bo{{j!fVZLmOGB`m&d-g%+p(doufxS72$OJkK;aFBcNH65M zCO2dV$6Ih{ijeuiD|}LgCq4+Gzc1BKZf*f@01JuHq!af(tTlD@-AM+qIzUxo)aGtS zixcwCZ)NqqKN*80g5Zq!jH0n`Y>Y~S(ZWC7T%s-z5&s#%;UAWD^gO3)YWpXC0f-|O zBPJLSMnYDCD%{q;h(B!3XDi6|{Z!>PZ}TZ~52UTCgNl9bf24vrJ$;md1IX~^S3kR{ z(>gdo24>Y}SxnuKE*g{@eE~+Cg6EprTO0S_ey5E0y4{MAg*a5&yU^{%j^tD1Bg3n> z$x|ZmQJsTm4Q(JT!?VrPq`h6g&u-097ODCOFNT;+4MPWS&V0;CIF@_$G*40XFQ0|Vd$*c*bn zu_ZG!eY#A&eSmNfrZ@6;g#i15El9mqXbL>^{67m{_%0e06e$_8H_Oq;rRw1I z569W(Ef1~ijgjkCeklKy4`1R=uz};TmkIA_DY%ofiazx(9mSC%ni+Bs9Spa%yG&yF zAzWc*w80JmCDM%^biUO1)8^MOEVKKUHl%rBbg4bSd%>egmgY9pc!TTYOU1PqClkp< zwo!x>MT`?p(G0jQ0N^`L1fIV@H$z?@HjD1+;H+^1#k;Ix-j~(Mc}){0FwUv4x%-Ic zEi7bVBT7#;n_I}NYV*OhDcrlewcyEWOBcNigx%IBjfOEtp@h3Moah-5ziZ5ZSYW`W zEbUo!`z&jMjLYNOA$0}7MM74*;3o}oHo18?b-_Uiopf|Q0Mb}de4v<_y=Fo8`s4NDu$nP!xqxS%H?bb zpbe%kq{JQl`?o9Y?w(^4eBl2)$*cyS-=dAkRG#2~F=<*V!Dq`|;n z_k`zpz$hDPlI+P^DycVRdTYyprd`L;JFE55NtZThF<1)&=~_w3h12*@FaxZMZbQCF z5SRq#cNjq`OTAovt?+@h2wj1)&ymWKvrz-}r?eZ#hMq2;PYZKsDckdx?rz#I77@m5 zejyGErG&)l#BOdg`F=U4V-MW0I4RO$b))4z4GJeS9u>#X>l~$Fw<^cWVvAC`7|9~` z=!&f-`H7X@Y*G2vx6c70Z}VD1FXIc9x5hM5+*EvX7qe}dyto4V0=ktxN3h6<+eP6*RfTyONJJ*NE1U?)cXiT`W4&(8eW#wv>w1PX*Qe;&V|eSR>k zG#;%mVHiE$b0gL*wyR?R-!|ULgvmkZ^y;A%v2F;eT;ohPDkx4pmv3)S3rBn8Zt~EO zoTLdv*lL6z(WMBF6xrLuTcpJx%jf%n(W`MYynb`(%yBBhb$Eg4i00H!de=~43Qog* z5f2`RESYt@>OxwEPUi;W_J>8pd`gmf%D$^MJZnG#DZ?qydu8ZhryTyGEkEjHl)nTDF9 zlK9Dbo#n}*^+dSU(Aw}UQJ<@`hf(NtMLps=D2-3pf?b^G8&i=Hh%w>`PNv)q8hgT1ZSFfA7)ek)OV|IR)c?t-|= zT~B)eI&UEa4(#u8rQUT1h!5KCAl`B_D8P z0-b4oI(&e)SQ}_6ng*WQjtkg%BL$I>3pznQSCJADeU#|&MS=MJcZO&Q(#6>HVfft~oHi@+qN##53U>?lXPEH=ZV$FL zzE-kU-ut@0K`&wll?~Ut5IuU&z~wc~YwBynuGF_B;Hn%C4#>xmvDy`aUUGR(?>|~O z7qFGvAHBhsr?L(akdsY&kg2e#jDMt76dO*Q8iT$d4S394^=31ml{#YlQkAnQq-bnH z_`j88Dkf^;p5#A3({>i3xD{W)R0VN+pB z#PdQvSA6i1Im_OJ_mx`tdWM`hwj>3PT974?16*E$1&J+;TuDw|7UPo3qy;VWVBQfL z@o3PiyzMM>-(^w1HIgZQR{(Il<*MsU6;0RV9LZ2vIK|f#~Ct31TY+f{ATd_g>%o z!7OWa13s9WT~7~_+|VWL&AT0DkV z-=z|KaWnsG-!s}$aZOb-jF#J-GDDJ8_Nmyjmy9iEMhw}sh&V{F%1=h*zoRU@5?T3J z%_7`g#WoL*2vmb_qW{?N?%p~_Sy0BolKk}y`Ba#5eM!b0{Da6Ql#r!7Zmv)la?7e=H7! zus#txHv2%jVREk|D)TO1G1r#Z{zOU``;(q>jD`MWIX=VAD<8;o_=< z{Ow5Cn$N){7%WVrVN3QnNuTsX(qUcpjz)ia_=CSF0L5osfp;VYg{-*7FQ=m)b<-7g zL2-KX6nORax`-BA0s}JCpK}r5b`ZYJGnp^3jksYXo_~dSpSpJ;?PON{VLkcfD>EpZ zfo)#(f$$}X#3iHTyTF`*cM9AJ9FNF2B zz2fo8moIoL%_BfYKDs9^D8X3Ce=hR5`J|gkYjIy%_0bUE0b}?P_F}(S@F$9HQ*TL~ zCTGedkc6|{u#Jei`yU{;)M)5|91y?xbZ@|la4ZWzQB7Pv{se1Nt6>N}OxVExkZREq z>=g0djD{OO8Aq?)X#p*GJ3@n>y984FoN3f=&KHHyDQdv+zpV$jSq`NHAJxp5@JTN( z#>JLC;LJBG2v{EBu->0`vN8e#c-JAhmpI3a4|1LM6hg4*ZJUGIG<=Uued_wZC`Cgd z%g>gNKQk}cs81@l->5gEJ{TV7o=nImRrp7zX#3R`I-y+(!&Y9Pz5&SvzLe3d56?Q$ z`U{EnnEf0%Bu9<94djq|+T!Syn?VwSMe;_!|J=Cc6n+vvSz#H>(`^TY;iFO@08^m5 zWNiPFLX*|%KS$0yjFZYwdntAnEW$VT!gOTJ)yxzq5l-xYFK>;3mrdQ|e z^zU(o9MHW{_*$5XuRk~z>|APAkMp8GI>$QI?O9+0yZ~7sZUw`WVat~4?dXm^8Avc} zO@3gl3Iq)FEGtPEQzElXPhdDMQE5=#5--PV4nT_>&9+W=CawyT_ZGt= zf4n<}eT6g7wr|RiVD;|1j4B|4FO=~!txPoN1fN~fkK>H*txQj54^@#*UpFBe1RLs( z4!HSl*&WxCE3N63{bD%2RN@T)k=ogdBr=tu7huT!8MaaI=`iKw<YB{%5j&Q6aDZ5<>JU)W!)j7Om8fU z(#RQ2hl%3@wgBu`U4VbsU@@CbXEqLAbzT8O`9E9_4SPP)+=V)%7=6Bu54g6#^8AQh zdM^7XcAy%|(cwGQU5Gs~8y+70D!^e)o|)uvnz&f3Ee zoqg;F#rdx%SYrjwSY(SJVY2O-uM9^@*URo~JFnY+yf{q^!;!5N0T*g28v7>4oDBS{ zv-bwwc7~t94?7qo9l!eJ)DFD|BSD(v^xwnF>;=V3ZhQqXhv~sl^H*a@kF8ourcX|# z9OHjhkdJ2a1@4Ugi@v)sck!lUNjL!d$t`mhWqD!)GnB0?pvr?}a_=PG)s1gXz@O?A z%0FiHJb9VIp`F06yJccFP^fYdEg(Ql=FN}Bs=3Z)v0)YoS8Q?m){qtsLWK>%U*?Bq z=w)G&^)L)lLPm2hAf31L{ozZ+F4K=ZsU<~6Xk}SU2uf|vDq{xmQBfK=ypGO)E>n9N zW0|pn>FI(m>3Ltk%sO*GA;- zN_O~Q|8_{6Dm<306>li-jXj4Evl1KHt%C&zncHYj>Wq%p zHwK%GffDw8EZKiCKMG8+dv*7fnO!Q8Uuj-U5DvnFT3I*$Kl&`}J{zZQN=U-pqPFqi zO2)0VF65w36^U!kozCW~>@l*Z!qr|6nibM;Z>nS+Keanl-pxzApaw3@S(icN8*G*G zq7YGF*#2NmmhGQmyKi6zPUR*DF8_ZTbqoYVaB`w8Y}|7f27|>mCT^Vr-37ekYF4`j z5jPvvQA05CG!399H{);mB~`k`2;_9a228^lX)slsy39E5C*^0m?Km4Pi`48s zfJK*9I%(2)X?7%Wd%C;nlvI;{^{=2es^JJ4FK&GgkBv89a11F$*#hTjrH>%0nkjQl zGSx=3qsNohiwyd;IyNguv23lWerN-aHA}JlH$LP8cTD2E#DD0zxI{*tD$s(K{=oFv z7KsFI>@^=YWIKxX(a8o_SJK5%O86Poi^l*zwIm)aE%+Ea%H5VUBE0yRLP~SgOt(Y~ zO-v1;i=5rxH-T+Ah+{<|A9-~pWU2Li`|TBx0 ziHn#T7L5Yx59e_A0l`kN7S{z{qWH_rnCzh_kfnt+k)=MD2kEh120k$(5$obt8`#-gAHUZD5fWY1X=f0cTeNNBY3@^HV+wn$mYSqU zYAv!q!>8hXoaHVW0B&0oDyueve^kiv!X~38xdT2L#VHpDn%+CLUX+KWak84`D^#|JFt}9 z+$`-MzKaWgc6>ndv+|^epAxJ-Lb9Qhy`mUIxR+#8w8js2W>)6%*Q+H2R|F6!Mw2ix^jq>@{UpTrIGjmS@I0C6< z#j(}lAb>8{?AMO}0a^30ZNPPES*>QEHTtV;iHp{|g;v_~JQV3@`KWxW zJ)A#@3kZjF2t_`QWSL4&N29F9v^h91|HPMl3p%-?S(?ZVFgG);{Bn!)Y4gr3#N zsudlB)~j!)tdt+%t?27>+Ka{+y@1$!O~qS0zG%>mXJ}a$M_1WYA?rC!$hr%?&nHT7 zGwt5MSK zcXU>V*C%m7M&1c+T#I{iNh5vlVlLzyN z;?>^cH`ptmV2<8KUujZ8h>xn#K-0S9+JL!Qjv8wx!)NEg6Hy2S)63S$A)51#K==EB zR<4Y=4a+*1V&qJJ`Jvp61~?OvXV{r!@F07YD2mx}1*4J}yh!p% zMLkGO#e89I?)}YfsIdgYna{vV*b~zm?)idIw3;cnix59N;pnuO8`RFZo}uS3)QdI# z#3hzHyqSNm3bDK7FN_X?STrrbuWU9T=@qzZtfFq1CHqR-|11~XjwHi#FC%ZU(=&hYMX0O56taA3X!x3`=_3c^iQzC0JSQmT)Jq@U1iX(w=(RnYdV9#xD2Pn{)k)!c4Q`kk^XEd{i8(X>ma z7w6LHc;<#rwW5O0zJobiSS8X3Im7fs;QZge4CMrPK{3jNkl zv-F0KQ(S;r@kqReB?K;d>=HPmj8hdD5IaUO`_@oqfnNnc_!4sdZgk)(g@5la>f82| zjlUwdr_HR7XSGtOZuKit-MbiF0>(Dkf|*W^wh;;Cs^GgZf)PRrJo9_rT(NJ)Omsfj zfL-}#Big5F+wb<~oe}}jIEn7OA^2a(zT&>r<`QJ24&Jlm11JA=Wf4~L?)|5@yEqcd zJ-0qe`HZ=qaMbe+683O(f@$R*J9`5au^pC*kARPwDJunLXTInw#qhQf&9xaCe6XSh z*wPr1ZY&%4YI>L9lWs-OfoRtanUd{A_6LgKwF=Jgf|Mbgo$zqEj)e_?d_LU#%i#f8 zxlkFI^SMV^MOz4}KLDnOp--KUqn=tt1X z)g1$D;aSFS0png|181+e%nE&$=N=W@>eB88IktC%6LFM{2~zV!;96?Mao5c zn~^i%Y_F@mU^cQ_;TJ(_7gz{)`+DhLl$QK8FJSBN={}$F$!$J9W5jlUz8b+_kG?lf z6;^-UX^6A)oiiAM9U^T0is*+;S|v(TA2Z5*M$s^iRDB{GG=C||*<`;sx)Qo4UYR|w zPZ(|eQe~P^AKK3(UaB>HTGT8nJ13D806dIkmCPI*j!>n$5Y3zU8`k%RWmU56e%I18MK%%WioMa}C~-}6 z|8;H=w50mXARSm;@44%4SB!6!Qjo?GRTgwqliw2zvI;&#*1pD=+l`pb({(c+Kfcq_ zHNf+W^$%W#rZwCMl}~W;ti|tOx5Kwf)}LL1D*+zjE>iA?CO|g8Su9Dv%?Hn}&4YRw zHzPWqO-tYxAeX_{elk_%-aS~4oEy+05Oa+*S76e;J)7Szv)bNN3*(L{3#fH+sOs@n z;_sVCe9BT7Bc}8LA&T@Fth}qRZsw(w$!T)m{tnimZrmBl@N9c9pHFrq1x@ujyeznISgThg5c?=Zc#9&@<5ECQxRR8sC&c-*^DXoSOey5&T zFhGA9B<(mf;rXs%iR9(yQX!+e`%qpjqb*@?5m~p@w)R(OQ8zgN3rcuZo=BxGD!qt}qB0we^dM#86mb!h~Ll3(1+TGZxG0{6jV8}1D z>e!gr<3suKRzT34MwLxpuhZG9g`!lQ`K>wy86-V{pQJQOC{t#4Se@1c=g!rm5CTFl6)kI6O3w> z@Hj9K50CS!{#Y~>e$i+wEf12ST_KS`4XJ^g_}%@*8~&jx)`u>uQ)F|s#E`C3W7nRA z2g@xU`CqoznZz>Ogyg8X`}c;s?KW}Q5ch9T6l|_LJ0Qj_b}sb1;bztNKfAK&Hz+&y z_yxWHG$cOQavO!Yi@*_EW?SGZ62Yq`a&#<+SI6}=tVN6Pba53y7YjTV9irv#!CDy| z201WOp|PaAoE$?d2-mu!qb6xl^QB1_X<)#M&NGR z4D1ao_q(6C3sgWZ00-sR*ssFn*z2MfAp<6Mn?un%W4y#==kRf+QA{(t10`Ya9eja8 z6poaljlbyKNlpN+Zdu0Cj))5vY1wy2Kgw~uO}M+A_QHTQqf{j4Qlk6w#_kB~UbSZV zQoTDoBQ3I-P@SfI$!IfMFyTS7WX#mm@|I;Ti>V=o$VKr^ab&v@8V*{ipE;29tgy^) zS@5VpEO_MUS6O~KyGu(73!J;PK1W%$5xdf{qbIK;h7uehb3v}AfC^oU0`*8oEthG0 zs%C#hO)CbN21CNq&)=_F8=;FpH=YQ}1@&_Iv+)qXuOiVAPjcw8cD`bM4IL-ChY@v@;;P?^Uli_dWPok zBnA8Lo(X4mkrv_AXK85r=?Sb2Z4-jT;RLITDHmNeYQ{r5|9mnvK$h#6z2Qm%*dy5b z+NLGB6odzWA|0yWoWq_RZxlw=V4}s9cCudb;-EPf97h|beB2fyR>WMh`2O06WpnH3 zVqdyCvxod#i!R=?mL^8n;G_jttW(4A3P=Ho3v~B^54a610sn!5TJ)PeKTjZRRjX_v zV&dvCW!z35@0Ui!t+5?czGrNPlw4=m7bS3y8)cN^#qVLq%$?ru;D7(9_s7o#59>Xo z8FMXz<-It?_7H<%+m+tgqa{XknY=kEgDX0j*CQGfcZQ`dzbN>fd1rmTjeXop={`4J zmc7TWrEFN$0zo%yPS5Qzz_&+($6>&Yx^}4e-5**x_10drloIVY17e$sIGO{@%oQR`+pfm^4Of%QA7CFjeKlH z$@)h_X%Xj?hp)?m`e}Z;|EEQ7&&|J-ezwGV5txBhFK zag}dD+)Ss9ox$wke?-CThbc2Hl4Z9;eenYWI&%Cgk`G{P3Gm~E@n0W~lY=_Hpj^A4 zT?<_>EDe@|F2dvF`dU-wN4HL=quHlU7qIogd3Ml4fd{ixBgcJFeC*N%pJDBj?s@&& zDpBjgC2+^>nk}((aH*@-L*Jd&bQ(lrUPfyT4q|F*Z0!Awc=`Q~K5%2rg!V%ZoQLj$ zH|^v@bhZTDBKcy{N)(SRZo{c4)6tE=Zp=tEu{HJxH4KPmG8HBJhEmv6xjNH|>e~(R z1>V=VJt@AO#fXL9B9Sj~onEbwY)s1ws*WfI2nyCy*ArfiYVe`0kS zO*52R^GVKn23)CDiEC>e;dy-e#GR(tjcqU-MZ5G4Fc*2EUSNT>K3Mtmkt= zd->Ka6S65BMRBf84Ls-j^rLvkLGXuXNtc4+^!-}Ut)k{@)aOK6V@F7e29vqd#!A*s zW8JbpPeTwS$5=c7ulS+R*1oouUtc45^4=vwWuJcY%mu^EzvV7_BHWJunr@zs{ptcX z)bfOKsqv!u2LQ2tb~MtPZ5UOzcn3-S*6*+!C7m3-n6ZwHofavIFKcNRVo$<$&n%hq zL+D}>b-~iH(!;mY(TkP)p)Fze6JKtm(z!N|7BdVN)GW~ag2Db%K8Y1$lH48A<)qyU z692f}0wo^%t1%}G^RgpX$BzMnOBI804|R;SWKraDs+C2X8EiiY;nU{X26t1nHDV>T z8+K(iURvnVyXY9e)n|;L_~2EM7>c2|X&FPm75tO!t;pTVCNkQX~IK63M8K$t;lXG0%j zZ3(oR@4}DHGjml9%5DF+UmE`M&JfMOwx`H_z8jsVSW#>UzbM1R3T{C}2sng3(J_Au zcp-aB;0x$|$UqS9tp$}JaZtS=5L0&LBrTiNbx)y}pk zmwW4@pKky@`EO@C0p+dVGbsWEDo(n{yWZjC{vmnoI@LKJ+f_S05=mVp%)J_~0TWBb ze7wOKd6!tES)<@gy5;{5m-=aDE=vxushVke_63=qjGfWgd^1YP4k*qy#3LWzHPyOb z)s$bMy)qN<%+^dQEbkEE`|{H^J}Dhn@#lb&8`N=1Nq|+}jaP$YoP_U=tG8JTQzjc< z(fy6@%(DW1fa-_-o z!oNxs`qT7&Z%Jr(un)&xg>HrC6E9y|7AZYHzN=e>q%XVlaMfyJFxAi60v);?75(Ju zMsv~bTh!_d%;Jd>B5`yHO%7PC+=f~ zVvc}OgI?ZmCBs?x_2L{+7Se5%Av{9X?Oat)<`VCc@yEvnaTCN*i87;`%%pUw%fjiV zeiv9+QMROq($+WT3YxooExMtt2#{&m#EpC{`%@pwl$JcQ~6dv4O&b_5fjTx@FR^SWBg;1RWd3u=`g{vbW`5H!CVW*MP1s5w-w3!r9)T)|&N>3pM4hlF9i>GA8Jp^|sq#bvLUfIAT$HXyp~7?y9I zyss~>k2g#7B`M?>mG%{nZ0hG9Q|8qa)`@d#;*nd&+Boz=7p;Su-;-I9{$a$gm*YHL z(BI2pG84$?72>Ll*LlrhBb1Muyw=Ev^>bS`cr5;NLTIF1Uo76O8VxqFvsZYa-Vy;ppx+Msvluaau z2Bovar{+HR_^id)2TdY|UG{8pbiD8bgSlFYVMEb`{(IbnqK&Vm7Y!F|OZQuRe_6gu zJA?#nkqpyNJkgwNs8mu`GSY2>@pH*KftZS*_cmpe| zgtz)y7?(S#=buuSlPd~aO!VePc?NFqh%n!3)x7rx*H(>E=~Wd}af%r#k9~|4`AzzK zB*-98(S^LU{^+QEq_oY@f>@+q$PUq_L_=I(0j%HV^ri;+s|Y7)0r*L8jCtY2FLGhtQ1!mQs0di<#ZvZ{jZicb1; z3Twvj$KD`<0mPeakVDsGQ%A`raULe2oYVwEcr*+#F5;1Ql0tp#w(uzC zr~4M1-G71=ejEetUc$@K#?x<^zaHqwD3rGB<`+2_cN^Z%5tnrnY<<8w_)I(HBAaWk z6ePv4fPIg!x)sd7)WyaIIGn9>tdPN{X`2RqRogrRZn{gV98S{6#e%{RxwHNBAQNIL zw7FY-Me$&m;|nGqu4wpZnq9-6;_*B81A$nc+ej1Jh2c2Ke8IDstXce6or{CBJguQ# zjhXWOlb(9FK7yRX+Nn+%DmY6Njo81%7hHy?vLlKR2*L+pUrDs@Mq)T(&A5l)bD`$-Q66N2L!`?_Jpg{7ohYNXYGn* zm6XkX*7;*Mzqg*+wzF3V5IXZ6a_SOO3?93mNn1u4yyx4_(8Z5Oud@*1Y%I?h^`m)Y zv3hVuoS?V)86yIBLo?ih9SPFb+o|Cn*UgNl96i-k7_K`~WKH^Yoqa_q)_9qtX}zve z$eSfE9hjX$HZ!KXi}msp8?~~+u5OwgO=Ms(X39xkYX)Ll>d~dJ@e&|FOS=0hXG8HN zFwpra&()!2V+$6#{!ixWGTRa~j2@Ud5>sw;uRiGW7`j_X%OtF8#5KwpYhrCXJ}o?h zo%DT?@m{OnnnZvyOFghk0?*UdWja?JmNGyurj|D7kOP;O)z2*=1Z1(R`MK|;Mhx%= zO0BvmAPbZ!ta66>-dIFDO*(6`R_R&%!Py!B!BnRzA>=WGT=I6B@W!!Q>xHq=O<%6Y zfuZAMaZ}^D+l@mF4V#`OnLK#rW?haxd|&iqvcF|6!NzjUk^jL^)zBuXq?p%vGfwz& zJwyfY7ur4tW&ZncTTRzR6ivz9q0-+su{3m|Ll651RCFOZ_P0|$N0k5yilX^5{!9h3 z&y8I%Bk0h(>K-SH6vcjHlGJ#J zGN}C5HD;el=*nQ5)KT^aliWmVU_?MBO8`#paJ|u?+*~x+vw5zmrC_d}-}~jkUlrjW zB1IrMvUv0-kJa9kt+lyQXy1rPS#$mGxu(y4!XW6GolbZf*+Id2a+kEY@N)O{|GjFw9rn`+^BWa`J3;PWfjf`>UpH-|0&t%!?HaOk(@~_rQZ_%H=T@AInh5yQ zbd25#ZXfv-r2Fl@1#SvI^D3*+2qJR2Zt$KyYWdxKD?{5m#72m_`ul2Tg%ZLtd(?hK zaqE11bbE+#X|OpyQfC4q=Nkfipo=Q2>s8)Tb~B|`*4KPRv<_MdQW$mOb~=#Bhg;ZH zRWbUL0KNd8xkz2opAV-*4tg!Jg2Zy534PKHh)i9L=h*M@N(}#03*~piyFraLVF6ke z7M7Jv%{xlQb8D>98VrM%S#NB$PSKrw@@1$HXfcNTBl&UBQk%eLbbzOarf)?l0*TK8 zPV=JvoSqG+-v(hu?w`w<)YJR2-aXv@m3CvFEjK}+c(*1(Vpe&H`A;RjnC3Tm>iiwB zhGWw)Z5yZEqt>M5_;TTi*oJN(`Vh+Naazp4(fWJ#dgX3uh1P%1qs#3p&sp(rwUzw< zuUvVI2k2f_{#5+>D-YlG^!3gKu9EV>gIQfcer!>RRaPg1{n*hX!{Q5DLf+u?hV<-! znmil?VO&Of0SD$Qmn|pVPgGVbQ<)v5QDOrs@p|PXb-N%;FV{N?fBG^3ctwF1&qOHL zllT&4dSBB?bNdG*V1lBIi2*`%Mkht%XY7MocWYv=+t7P9xcd`Y5@ILxBR`bYqCvON z*o#v`d?jM|b+-Sp|Dm{FiRUE(J@!$M-I)(S4Auf^*Ykckt)8ozg*= zGq{b#Kx@wkQ!}ki8H$0$Jj>m|TwZpF8x9!Xcs%zN&k&F9E_}Ufih*#xf zN2bMasq6!CPEqT@oh@#8jLKO^Cwh@PIGcg%V6wDRT6sIbS1Ph+YcOXbO+fg<3i7b- z}Rx1zE0uCH=-;m>45 zTFrjUli#8vjxo#~x9#6z7=>%EvAw4ITiBMk5l;H58vzl^mZ_@@;HyDy$yuMR!J@#c zjcl#^eKj*@0fG@y>kVS|E37MlkWfhIoHZxnRl~7D+8RV&Z2>D3&pd*j#!E-0vOYPX zVZZ9@wAW&#h?lkyQD^VIw@da{)zKbVjb6kOatjo$ov;I6C$+(h%{WZZi|iLmRFkq{ zBlA6Jtx+v6g0|m4tCJ3W*l$8LADEgjp zS$txl{XcEyN@S&#n(AR0r-f1g^JiFYoW+n?##cryKtJ}6cQ@tB8n&jdz)9i?U6Xy6 za7FV8-&^>}BtG*R?OVe7qS~%d+qJv+4-(NS^RkfJhfsLaygCH`8dUbt0!=MT1l`=*2W8fj^2`TkH>7sd`_!E)fwV8qx|psnvy>J8G!=z+HD+bWQ1 z%MD|EfTs7eA+ps!@(ZU1^P5`yfj4v8rqu zm_S9zcmU0jN!^5P6Ks7F258M{9+s7W<99%xjL|S?SUB`R*k*_~p@2uj5dY((BjU)B z%)JRBSB*D5**#O)xVM=$6Xp$UmRTJq#9w9d7x?zZSk}M{)pldnj*(#6Cu)kqxak~PNhcU=z#H5$MNvH52hN)F!PVJjI)~sxjU#t225XW`7 zBG5;WDnLhf^?9onDTwy?C7+Sg8EbY;R8voqB~I>q0|3_~X#GOSy4T<|MbLq}#?5$l zd?mRlIp-1TEv10wp)dEyiCKy$vnW@RC2S9Hx{i(b0b6UP_+`vzNzED468C2PiaN z9xUl_0oqU;ok@aPgTx9)kqq68Nhx4U9pFVmupozI6P5*3E%X3n*5#-5{Z1>{b`*w7 z)_KO^mhTqX#_0OouY|4lDU9))(BBT1iqTK0Me0*ey2}6#8;znHGnz_zQ%W4_Fn#|Lzhw1PQtoUCPj-Fv@=R_Bmyvnujf z^KPgag|D?wf3wgfSHY8Z_OcbRGHazX>Nj=r{$-7oxNX1ZM;jfRY;ajt=H4t}Nv*|_ zgms7K{Iid`e$#h*tH~5?-8nhO!#T~HZ+}fWOpBa{+Fm*0PmK~L3O+XwZj+mboOJ10 zCE+jFovb64~@oZ#U!_%3eu7wl`^ITg!i8Q)EHC7!lzvfyeh zrv+?9ol}Bfqt8_*Qc@Q+FbiCsZtae}iDgL8!O4%9-?^F~Y_XBfow+MT2M$q;MQp5L z!G}@8Ybm;pN=CYtc`?RTYjjItAT7A2;}x-PU+EEg$6U$NU#b?cJl&M@`0_iV4>VApycUlWCmcvA+Jl zh@MwfMOu3U-KE_^bE=9T#WVE=esW~GWX<^w+f$OY(BRmYa&o(>Ae_B*d?^-r-4%lR zefkJ-BB9`1^ulpzde{wsXl!U49|j<*{37#T(;woqdtW;mfzQXdD+pU{8yYGf2zX@R zY{WXdyW?OpW&o9w6Xa4re|NR0!zcO{2BuM`xpOMkRQdpQ4E+s1U9*?OWq<8#oU}J` zhtNGjONjDcc@Q(Z_;Eft?p7WDIp_Lxsj=}^{pLW#?XTN+rc1wrtLSB5FY zz7#Fqg9L9`1TG4p_C81Q8NCyYnK6b7Ul*yVANv^qODeU-hyUfQ9dG@|O`143kV|-|=q#*BDol6(BFZxq%t@wI$ zvuHjd{pV88!$5-uuT4)6^xp1PMwc*5cZSy~l`msOJu7nu7bwjp^migL2Xc++9Fn1t)k>#yEa<9?FQdb=E<_6B*Fzy^a6uZN2ceB0{NCokkzD}dO58?^#eVswuWE?LA;)fO z_?xk`oA1H37*6Bhi1A>gMVhmysnhZd7q5I_VawjR~@oe1(*Qg$vLdk(X9k3)q`sWLswo^0j>DFv7N0K)M@ z{rg)QwKV>3CHvnT8L}m}ejYnA{psUb2>zqpZSg$g)@l3EwS*`6|7ySdznexZL+HV? zHoAbU{r4VsJ$@!q!rf&tF**?-O8hVTPNeR*JVnkb&=*q>h3{ zWwWh(wRlygnMzDkzb>AT&m1lIIkc8-BWb){#jdbuwxJ-qn7~V@ya)u28l3`_wyjZa zxTIut`f))-vxQAg!B^?SuQM|wv5RaS81CT(?m@`MWMs*Xd8v9U3Bo3l>Y z%GxT(CS_{L9dC#>MqeIAm7SiUFy8c#HtN0h8B&W;zmqF0Pr!?=$s&gIzOC--Bzf;p z5oNE_p`AdyAtd|o(oaAGqla~f=!e9Y80OKIwB20nlRE#L{xw}@j$Ix?&G+@>?{>9F zus;77U+2Z(*KiCUkP^S=ozL^~@$mtMo!tL+YH375zMu1y zL_$Iy`x{|)c<=8EuO5HAxuqoBXi+0=O3}I&9v!*<3FD!A%$!c?10My6yDX~$5P?Gh zN3Es`Q-Oh!k45*9H&%AjE7916P|51tAARMpUzK$B1>ryUzU;Q$zdhU?sS~D?bAVh7 zn5JB~1;0M&g+Fyo8sa$bD>S3%&HjR(tClP>sNL7oH0U~-PTHT<7K{EO$N>yA7JB3Yi9@ey>GULUyfrEAe*Z@4_a-mcjYGj<=L7_B?i>CHOpJIDP9 zc|dPAhKc17DEBI#=Lb1~`_UeuyTjLeN*wpNN6U*uV%DlSytNMyGX%#}ChG3AIlPR? zjs#=hT{mQ9eyz;gH2qk`3|wW4C(cZ3%mFv($WktB_x-198yg!)G^v9+Os0M!H?jbT zn};n*@zadLW>(h8HA9t{w8Rp%7cEPR(|h%p-;3Lu&ay{m7T3Q^U$3(?0FUf+7D@?a zFFIQ9lcIYycIUg4{15`#AALVQpBjNU1izh8r7>4!X8(y(4~%+B-vP`GO|%jDz@8*? z`w4VZTz;Cp()c%i50`z|6)5*piL`_KtI;w*g^b?J0kGiTSCGo-;>8vJG$b!|`y(rs zGaD7qT<04%##oJ=B(45bGUD5RS$3K$!jSQ>;O%v2v6>uSbLiO9ZrnBi?vF3vMcoA? zLX+KRX=v-?Z<3#vY3ShZFXCZiFFy-RUIYy38XJ?*tWryZDXjCR&<=B5Bn-Aa+JCr5 z6DGesp)RV`x+pBi)P0#94I0R&*&Z1=d0thNrl|0MWhDEMr@CBc<5j4KY&gc-Rxy5Y z@XKS@k_JhVS2>{%j|ZiXO5(J^PJqrA{=BZ8(@-%0*~N%jWO=jwRI`G!6X&8BUhiEy zA@=i3lzI*H5s*<_m{eTk@ z*9yqp{2Y8+ZBK?&j%v{#x5VW_cXwV;UQ;mUqa_*S5i7(rnfNd1uZ4qS@1Qr%3P^R6 z-~fge-vu1^$kHtT=v zF*-&MM*Z&ZAJ1_-zvp=V9DnT?_kCU0dA(ofdFE-V>`N23;y!_fqKas2GvV?v>CKna zyTg8ad0ByS@kyjAk2u1FIm;s0OiQ@9$E^8+_+DG+5U^XL2#V@oGqZd?J!hqn6Q%1b zoc$&yC{3OuY{crnL&cIjQ`1;!c|3(E_35j0{Q-G{Np}CdT?H3`^^bh`BlXH>!W>S~ z4n=$CY~HOtWAi$=u+B^p$n9PnL%syb()a~NMo1>5r$OTx4f={;j$+@gJ~Vo2SSW%_ zltC^ge$}GpH}9N5AcgR-Kh2h3Xfybd!d~_haYs%>0WCpPh<~RE&eucpP6?aG4np9# zdS70C!4GdbcUE)v-Lj+0ssknpbfcKmwhjc_{IDm>L4*V*Y;kXF?=T zkavcp|CI%w(Cbugf*bbeV%LFz(-Hj(*L!nW8ceCzGOzOid`acoI&aF%tdTJSr9oLM zjX+^aDXGM*hc=^;*S<6&f99f2mF_ntLw5T8F3%E_Q7Pj2+%MG`qzN+xT8!mX_AHLA zoM}IIC!6XvJwaPdS?D8#*pQi*qQZi zoboQ?>Gu9UdjX4Kw@!c3#!|o2R}n}bDTnX7?cSg^^822DnvS0HyOC(}Nc(W(dT*`? zYX9^IinOHFyIQu}MbwP)i@VMncTsPVhGtwnT|HWjhWu!LsmH>ySvQ-qiQ+VLL_LCc zMd?PR<}9}~t;s0%^BF&cWVJdj4jvazhk>YM_n1>`=&1ve*y(MS4+dHK<~H zNxqdC+AGq@w5f8&ZR}zYf*c_*?UBpLDGgrCc5b=B+pV#??`353%L!Ke-V8lyIU5+v zdsdS>s7TtedYQd}#ApyKM&(_)l*@idQ$7cqD4+KxkE&(P`bs#dY2Fb*IXzCKxV+Vt zj{oXqXum7uZ1ZbvDFg-UV2`{_L9e~UEvJ(H`sipzt>Rbm@J3FGkqMGrM2nJn{T#L z$8eq&C!4w+gry8jBbxnMyaDwciaCPi43Ro4y$8GaQ>X3}+@~~1+FEEI8{0y>)+=vs zs;Z&X#g~@NR$rO)1)N+S`f|o5xdJ$9H}PLd#cx+*8o8UgR_j7@atGVk{<+-K(*|(e z*ETf;N1><)b>0_~;Qw@m_uATmb!j<0&rh_v=AuzUZRg^@nPMfcYP=abtcq1wRg(ZSQpfnb4NU-TXaG(my@8nX&x@zIkMpnUAxC} ze)1^sL9-n-Kw~39qDhf{kF$37*ZD=d^zjpPgAu)wdjjdcf(Z#{XJ-=9xE|7&`1fbn z*(uaNIA0bvKb0ZVaO`cg5V8G&%YoVPtlq3e-Hy$;$C;9gIP4;`siDT}@>FN=q;NG= zRFbWo^<8*GY|&Q3QeJ8fzmr5$Pfml+$|mZ@!DV?Mc0D&ia&jxhYw&Sugt-rg&^E>kJdyS7p3 z&&E4t9L%Vo+3v6JQ4UZKsFtRVcOHJYXYaYL<}R^0Z>!Y}m z+Q=6JL2Jz!AJGW}8u+3U^^5qjeyD>;jmd@dfjQ&`^Np0Ijyh8na{`#%dP9-2w)Th< z`hEjWT*+ocI0iE*I5@P&^pd3`$#!F9zv~sra0=a}T@-hUU9-JK4&G7PFV8M~;1SF* z?f*v(%2VD=CcQB_r+|k%(?D4|aZSvP3(l+^Fj-KGXKREevPBrzKmjW)50v;k9l9_R>`%4@bfO)R=_6+?-8bQoKgemos+dTD$ zIl9H2cOHK6HY^%ZLwsW7xt{Ynv@>AAAN7lnHrL33>9R`t0_pxPKG5Ox*!w!C$6or3 z(h?{Wxpg6E2jYUR^&c`&5y}>K-A3OY%D?%KQO@KJ-|H2R61(r!Xu&A5vcsYk0z2A+ zyU{L_63?WU);m!zs*p;i9!ZkLK&IOJ~kmz`7ygH&L`@8vrP_X}Z;b_7>FQ?&^n z9f*>(>-^(uB?w##hL80+D&G^rSnLe{inOApzK-g0+{*>~y7_oI-G=gm^nQ(8YH2i&Udidb^(hW}Nf!8pTG=-bP650(z=q`Nop z?h__U`_S%4YNy3U9WZAJ(wnuJ7g|Co618hm<*Bi}VbtKB)cV0(%bK7OD95?2K&~+z z_L83w7U^s*sQ1i~uATa$7hF1Wn185{h<80+JQyRsmAyG5T>wC zQyBAE_;>h55N^7o{@d>1*{}Qh1A}NX(__6rOr_ZXESClY0E!tyW=NaATZx zB+^fo90YwC|4?7c^RQa)=LT&tZ3wW1^uOH8l$rIq@3?DYv9+BT{38h>ST>S%WgAeY z-=^4Jme$+-6i9iyg?-kR)o00gYHJody34gniKXA|+0uFy(005$YuTTa$mzhXSj9M0 z!Suk+YAWvVjd0S@Wrx&RJ&*FP0aLvG13#0u{z6>({EgZNX-#P zC5TUVBLYbfAD)BnF>nX|SLFg?45H!Dd2)w9mlE6_lwD-%6c*K{8;H7RS)w8|ZPsMX z=z8DYXlF}D8pJ0m{xz`tm+&^|T(=+S$vFr+oJc)vR}vS%I51H0oupl|Z)i4GjJ)u- zX+6;3IE`y5ni}_`c;Y}ppY@J0i0B_)r>lDjG+@=};pTR~ZSeEccTDsNYDWp56dXB= zfnkE&j(xaKwaJ9MZNRs$n>i$`o_%jNm*{7vN)FkK=Gta)1^`sSKr>WhZvEt5Fd zBGT;haNAeO(zVuK>oz|G{^P)F*C*OIcfp8?`}f<(vWr4zC`~=LGzBRZz4&4KP=Fg^ z`#@^m>PQyjW5~t(hlTe<6Y)x<`P>&&XydGBv$UWtcTq(Q55A$}R~2w|3`D8Cv|jp; zM@Qcw*N_gzOqqB_22S6>=mvip*a13kgY7bRJcd`Ca5Prc)b$MwDi>}tS7uqMyM{Y$WsYc5Z;@4WLCq_el5$`|%g{kZLt zBdH(>G;#iUG`lp94q1XaCV+xmq?s{0WdRKA4p0&hJp9HUuKIx3Llk!F`m&C()x`JE z4ZnLVmZ*?mT;Zv~B`lw$+zAk(G#?4QU3Pn^0E$fll&lByYZxqFRc-^_@JO6yERnuFt`3XQC*&zbV+uzg+s z*iZ+a+q;<3cw0Xd^A#7o_K2EeiMR0xmLy0x+vIJTM#6~S(+Jm}5ks_n7^?s+F;99Xcu1}1`5(%QoMS>lx zOFuJi&ZWEwy)uM&q7ydt5+utxD&W;n8~W*mdxXj@wjR4s&HPOkH9&J-7#kY z!3hb#@Nf$Ij!-T3&D4yhOD-+7AH97E2jB6_me&RHz2R{ln<9Uj&T{fM`!{psT*jR` zxLq7ceH%nTzcVuYdZjr#kQq$k`AIoZ#vwztJM+4X=ozA14yMJ)n}0M1E#HDBsoL!1 zSg59C&{UFQg?vz_+rT2@K%jc?Dqy~B>baktC%C2X>n?U&#L=UuuB(ZH3_ zThVS{^sdKh?5eiV+G89$p?7mM##fd;y)=9Z9g?D{lPpCJ-Nmf`_kzKT$$FFUodd1o z5#_r^5x>!N(IYrt?ZU!BVWpOUf_m%i!{hCHlwybNSy&#kwtcU$&_j1M=QR{bpAy(qONvNIn5sODA&J#GYw;*7EAJ1i0&Zi> zxMyf?j{3{0YCIqWp~4~}zMHN=TKPltBd2-D46pLp5hI_xJfV0qejS4h6}GJuzh9S$ z&twDh^t#FqU!-}A=K*N^9x(-jvVnr5ciJFE4sJd6`*3tMmZsVSZ+Ot~j7#onX|VRrklZNHS*&n(An~>U8naJvgt4A_A_Gu?H8?)P!MSb7s zIg}?$7b`bLfrb+t&=0ESib`L53YPl3XP`EyV+l60x|pN+nJp-r>xKEM`e@s3muVh> za%x-z*GYc*acj#8OM5l>T-s?7wc=Tq?2J_7vx_jl_-preE^^SF%A8K|?x3ZwQg`^7 zSIO$ahcEGCtk2C}%Gk&x4jhr1FmG%(I{N*(@>w>sCA zc*$608;Ih_2aF$~K%`JKMbfiTZ@S0^=#h`f`di3lahTC?ATRh5;Gt~#ZpcGeJdZRn z(tf$ksm<4rR7wc$HO6>~Bi85aB?7e*mM$s`dB_R%IspP1AypaLt^;wr;r-xX_Z-_M zcEyfI0gZDf$IhOcMP8 z>G-Bi;B9h8R(wA?wEaIHAu$AZk5!kMlNHrc^@LQw`Y?6OzX9>g$L?Jx-eIo-C^g?2 zrZ(s>7tFV&#?7`q5jMHJi#fibsjG$N51D*Yvo!A)R~NBg^xMl_ugg`Jpj)lA4@6lA zKJmy9sq0GYwfo4l`Y^?$k@>=}&h`udo}z#;RsMA-U;IS1RYU~ zi;gxWga6#-%j=_LSlOlLf_@baeo^>GQXuDd26xHAeQ-G#DLi18le-_xo_gjEsQ`2L z)F{hSa!5d8b6-nXUphj8<)-kg{qPx6G(?4P#Lx0{OkpUhbrHQo+~ zFfFX5#N z*9}Xs)Miq%QAy;}#rm{w77gy$B}v$sydR4Q-;)upsSH7-rCQV0`Rc(?4LJ5*lpKFC zkE{vs(2%4dHmY;q zkCgyTVSg`!z6p+*NVi(0(&!YQmTAnFbb$vPZ#XrL6bI~GSPezo8SHfKAS_kyh~sFu zI_XjX4v0k;-u;2=IU;lt4`KU6wT|7^;;j?P4plpUG6y9cQGaR5hQ1m!3}NH>SB6Ci zT-2#r{j9hutUErI)=BtKhHPl z#`VruMqi?q;M*H-`g;cR7jLtsv6}g9UK3K+@7-c=Ik<)mu%v57Q$&QA(jCGpZ{NUm ztJ2`=vK!swQp-u~&PH3=K8LTmH9PKRibS%~*j%U$Plev9_J!oH^%shx18cvMv!3YR z#MM=SRK=~UVe{}8?;%2&GAZ#p-gH9H8{X&S=M#P2hU?c;e!^X~RJ%e_wQePbhg}9} z@mdEh>%f-V^or^v?7w*Ws{-2FKh?}TcjQn0xb6|bffV^)#Z0#ww^x~|EjK$Y!h5Ue zY9JhDp}`}mI==qQe<%k;BFF>k_mFxNgDRK{@@S(GBkQ22C7dWAqwh$(UAJT3Ru+ zaWPbc=vvXjIF;T+esO7F^Sl5aNJc`JW=F-&grAN4dj zWpR(e%lnZ4V46P47-J}1;%rl^w_`WOgs7ZEPwHFmBd)Meykv zZyMC+svjN0Fjqt_^-OjiA1m{SV^u3jN@j^Mfa!zC<`s(DyTEyJa%P zJ_l>Ge>Ea=>Zp4!!qb6NTOC^`KdJ-#3PT$_Mmw=Cw7Vg)1jSUiXAW~ez9cY;eo?PC z(VEih9J-83I_tolUn+6pzK0J^sViUrT9vVu<`*{P2PJR25!Opn(d|v2hw(+DOV+0^ zND4c}@4~=|y6{e`CpY7dBBtr4woq17CGIc~)fV;E|0vXIl9SUTR$`e{PY~K?m}vm@ z-yZY&J_tA2{`vafoUFd%VG~P#=Gh(GKZ3KY1fk%&x>Ot;bOO=#!aMEdfd<#Em0SCc zgxnsMMv0A?32x^#8NKR~UR1-&`snG_ri;@DNqsU`+2hFPZtWf?st(wW%&U(rR0MUG zThn4oM!9;uVWNNFW@H+l?lBWne_LwO#(p&2jV>o))ebb7gk=Mk*P@^ZHcR*L|9B8i z^thr}jh!Wr1{=n25#TkQziv(W&6;BoETKyzZ{EpSkG3mWxL{7z_~=U;^Vi*Ni#80);KxpPv6`(?wQ@(`miS z3H>$rIa^>Pk&BT6lZadE*L#B|P$u{eo=~HRT(=%IxfShW65-v}ZzcQIKr~{+6Z4iV zsFAUXHn)J1L2NN^6Trb54yNQcoxPQT?;@Sm2SeD=!PYg&#t`9R<2Fus>@PWC37T2BKIH96>ldBS1b^m62M}?AWQAm!L1y;epqfhGpi!{KDJc)1r`=VUc2)a0q*VbL?ri{I>}bN{`rv==eG z7fbBvJ@^w;r}|rgokT|c70HYchTzeqK;Lz$Q?t-2(TvYUMLr?BF+ICY^!-2P*-xu7 ze=8Xlh7)lEZ<{!;?@M}_%|AT_Kdjp?(+ChhX=giO@zLcC0uJNT-Kx^;^C6SxOU{8P zR?hhvCvqEmw@SyodNX7s(?ZlkSD3kmX0ELM4^m7wb(HEyPBqF_$GIwHLWg?zn!&y- zN6bp0SRru;F{3UT$Z>uvpQTU7t@mRvUFiIYBKzkxm*LrKZ1UNM?^aCWzY=WlP^VC1 zhYeFf6~n0}t?maYeJhT?>YoB`kW{=Yyk|7}Mt!Y5N&0#r zj6(VS8fh1WrZb3Bc-sHA@vE!4|M?}esIP@h8h1%tsR@*?%o={l2-~-?Oj~-WCcIQM zm6JVvJ+332L25z#=4dsN z1!{GX zraCE2F}7gb{>My~dY7DjrIiC{VWFJcpecwV;a(8vra4w={(MDzsBQCs>BmOFpJ!i? za#_BsG%cZvbA5%nx<;XmpP-3G4m~ez40r30O!&!ATGpO$29IER^6^Y}-AZYNPA3nf zv5;5AMQI{t^RcOsJGfp&i%2gx zLIiT%{U_n;+Vbz{O-g?IzrP?(iw2-FLA^7=Izpv@XEB$Zh;%7gn%ymCv6*fz8f5La z;4Ot%F+`KKwXa%l(9iPK?y7{6fN?zpibDpB9GpED~Yy;ghH4d+Tr61xAY#xc7V$57>S6{n? z4QxB1Utf@@2Rl24qLUagjC#-ob=o?IZ8%?aAE zLskg3F0UHl8feVrjPts}UUq_g7k;)MN0J9%9s(lVq-QnQ(!U9Hmf@G8w)Odg?6 zPKNT?D`ba!C&0f>Soe`cQw7ZKN=j8Yyd|(D{Dhr#X}s9>fQV#z!?iwozl{q!=PaWKJ&pcEO zWo@=_-ffk^Ss+mM(p!q47QN#S*u&=aYjxar;mcI%P}7AE1Q_Ft5R@G=%#mS9LNgm& zALQ0&XWWOA;KJ2P4@ z?o3j_1Q3Tf(;(@^LH*C)xzb$+;9$a|KW?9Z>YSjd?topDzlWZqFZ(T5)NL1@fsi(! z*^BEMVrvhX`ELCmQNs!GgGG;P_v4Fh!a*mw*@O_(##qMkU__z$Pu>L=+C5)22z$5U z9a)q*I~YDKikkb@k?lc8Bk#WVTcI^m_>H?wHuanZ`Ag~UdE>#_KJ2Zm$X=IWdeI(K zebI*ij@^-6V10JaM16O@PF!i`2;?W5hBTQ4b>LHb>`oUV9q3VHy8l-$M1gLlS zj$)IpIL}oOA>7oZ^UpY3onN&G&rsH}pJM~V(etD)Gygv+t2lLnnvt)i!PjS5*3COO zw+!^f2O(f2h&S5slV86C@`5%l|Ds9ImpxZkPkzem)^Zd@QRnnW>-bwUMfX)6FAhPx zOOI>YE>PIoA?zpPubD+?`Q6ix7p^yF&%6SrFFCZ!hJ3eWtFMO8+a9MGK-9}Ia~ky$ z73`11*Ra^X{=!~)a(X6Mz5Q!{mJe!vrN@h;3tHF`_U}7~geHxQ2d=(qQ(GnD1~^d@ z5xV)n6eqdc8|^bk_lxq=2UgcY{_YMnimiG*8&v1Ek)}vKfqzI)#>lEO)i4B0GjGO-HDcGL^?R!L8?ppAE%=4aaLA8 za0qN$HV;lJ^qQ#A4=z{yws^m@NG%Sp%UOHMdrsnS?lzWpt&i3;v0IxexET+?s}Gv+ zfgiVcH;v3RIlv;(Kiq#+$`tdz*QyB`a&o#Rdy-;;|DOT#@K=BS)ND~>aX2HC!}s91 z*S@vCesvp1eBN?k87$OilUkusNH8T+dMQ9zUC4s`^)XN zFgaHwg*#h6LEm(ikt1$~q_OuX!Vj0AFvq7l*4Oz%Q?%!$)#zvCH#U^S&0hjOpu@cv z7KA{WDUF{v2oJ z%9i%s8IRv7V`#Gc5I;S239)J#_as=aSv+4)b)xsPGTOH~KXk=#z10X8Z1_>3c>i5Z zxx*gi(j5Ny;&)kP^Q|#v>W0YVep=-q_e|uuX3n z>{Xs-@&W%c5agBCJ?U=>B?ydWo;16D8B}aA*dqbfNBLy5FbMH9-`cxBqdyDYsB= zF2&&&ecS|+e2z#s+NfHgu}wZ)<6CWu08DcoO_eqLN3{U`KNNP6>gFp2TqkAb_Sf2~ z@8*yAwc5)Yrh-YofK$r`5=kPAKWAX`FtyZR>n$XkfC5F;j<)O>PMKF>R9f3bc5HyR7b_=J7nOPC)q?y5B#}{CxSI#DVo0fya-0* zzK1O#A|9`mxcyaBFlHWmI!2{PW=<3nT9H`aJ`y3GrQ0oNRfeDrGf2PVSr(!F;vcxK zx{NKk`Ug5dhqO=!B-#B7eW1rN%=|5a$szAV)JH;zDOIkB?kAX%#Z{BoI7@mpQEYR6 zUwChnugXkd;zp~vg6S2G5WeGaT8Og?J^M)!s!-=%UKxo2vpVN8))=kn$@p`d{# ziyo)QSJwx&>Z_a&ed6-1Zs8SAH1cz+N2cPaA7Gmi3K4QkhXxprSN|yXKl76F^9ku&vgqVJUY+D%fS0fy&{naYsIAm62rRpY}6Whc?t#y)Z( zRYnKSf?m&95jiu9TE*u@7P5L?`OP&FEz*2`i5_gxp1q7_+CO;&k2Q%J?<68qnu1wX z(T3T6#&5NcXV+|ElKN)E+V1E6I|;CP2H^DGt`cK#Mac`)SUf9y|ymQC28cN4J( zb)PQu#;4c=WCs2oCzA19X1Tez2IFeM3B;sINff9adA#&Fl}NFB=6Sf=4%pkcmM@GP zw)ju$_(o;n=*$Y{7uBZN-`=C&C~^&z8JKqV@hfhoA9?4gFpud+cFfbxTEIWC{zN4I z2XfT>(eY}X1=q7;SOy?q%GZV-r>Ztb)zNC(n z9e6d{W!koMMEe)%Z2;9&-;+kfDBQ)nS-G`a9-3R=4}4+IXi4KN@|T+#|NSq1aa1Iq zbmZ`gd^oRZSb@rf5+)3Kqxc~WY$51%`^G{9Se?2Kbj%qH;?+S-CBBmx6VO|OpA^)4 zP71mx0TwQ~VT^(NxTVdN4^168<)&VbOlTIG8WWY~`ZmfM)I%SFlnqd$s8*1Bo5}iY z#PCRBo#37c**WTc&G8rpEK7c;i7%FJdtUKk0C*|03ZgY(;>16GTG&+2E6|aN=Feajn$1B+25ht<|iZ} zdY67LOC_OU?(DE9lkd?1zq1VSChI%wSboxvf-^37yt&PLwW5%LS&oGcda)Y8n1((Y zht49KwG3by+BMsr_t$o13uCy)e5O*Jy6Imir7=V1Nvb*{NPBTs(0?AY$x)x#)cP;! z8UK&H1oq=}6?x~Q5XCG9`7Brg?hTLiJNXy zKDcgH-Xh<(SN%e_>5<6^>EG2W@eIkFXX|O22fnOIfQGkKzb*W)Fz62>X*WneU~47e zneQ;)pKI(-k>)G0n%8LwThYYjgzH4w{C{LfPfmBT8*98@W=1iJidkAGJza7$K}?4( zmX!v3zoL+PXJ1z+DT-*;uin0RR+%B_pl#jg&wUZPNd5K|`&g!{M3!)q?@?#(3g;LW zir=#2YJy0;P37wr1~>Yqkq?)PAFv@R(ihU)cBV=qBUo`hFvrnKbwHZd+AkWwI^EMm zB689elkZ92Vasi!sY{;%WnH8CHq3JP!~9%>&ocTInMcpIAf}G>UFF91gt&744|%;M za!p2%i_o~J(x?t!F&X?oWk2~proG{KS-=_ft<>OOV(%MYMDu=_6 zP4yt$X{dIyf(ZZ-u13yzR2P`4N&^*uQV2hMZxIad%=B z-Ux5>+bMyPlcha&-yYas5Q>>M>jB01v^7?2A+gyRxS-*I{D?Zs{zUqVHnk@~y}sX9 z4uQm*lFikF4~3fkJmwJ%__{`~PL88sijS2Q<+b*+23(}csgE~FWp`4+7{KWALCziE zN6N6I@Yh5?>L;@wiIh9Zy$R6cgkEa|qSk{5QwQ7SQ=1*sDZQ4zZ**jadUIUA^DOv} zwW+#kVT9y-XnhSeGhbOth`hFl6Y ztDZvZ?Vz(pUOh!ceTGM&=E-zrX-}TU$@VESGw~mc4$R6qG+%fFHL2$NxsxK@%|>iY zb3^=}-{n$QB1yf#T4VhX)0o#C3T=_|edl%Y1*2;jGnJsOe*&k<26gKX z1z%NPfqp`Ai#@IMStFe!hAn-U=I7YmMykf@YeI9=2?Hh^Ms)t9Vu4GBVWWQ&zBdg< z1aNEd(>euA^XEGK!xJswV-7q(VLy~WclxojeSdX{dFlI-wVpJ_gqTqVLRS*Z&C>We z0R;DSm7p$us>FD-n9`Gx@l>tR15ywb@rN}b(nGIo-KLqxwPIOhf$7GX^%I_-IAs57 zul6z(Cy%0m9UY3f`_`K2XFrw|lJbYZqomcBvUjWTIp0%U4C!kEhJsEtq)4_pm-U%utFg_ULBkzTT@neB zjV;bKcp=kzA6y5-+xp1>5kZ4MqEM0UQx!m^N;vqgy8{fg@AEG>J}%ro0Vi%~y*mL^ zmstkeA4srLb%;tIa2{xI@m*SE4Q&kYaKXO^s_O+so|YAR)ugb%u1^phfsuyBp;4+G z6PELf;e*`{u$ADStHB;mJDk!7T?BvX(W&~rO{>Y_fBT6l{hvMpC5=-oWt*D?tl`E$ zeMg@HMvcc!O6qMOUx=F|3#gRmn9>C6bhFlB6q@J{;oSX5AQ$bV+wzYmwv&Ou@MqIf z*t5qj3p-5Fn1elqz!tv^D}vo<35LG)aA#+5wCa(9*WE+e1xlm>& z70?gN(f4K3q=Ibo)F?vccQA>@4|%J?w9@uswMZQ+R5aBqY~YZGpilXR{f%)>eO5De z(`ED*<}&XoGUK$fzXz|Ea1@++Q2gl|>G@xWrRo1Gko5qY2wq1~*R!9^f6cfkMNdl_ zZn#vV{wFijao5RM>3-%kY==LKFP7u@xWne?FWNssN-m5~UiTt>P3FP zdw=7*hNzf&z%x8~l653?yx+L-{B*Ce-ng zntp3mZcIPnEXx0u`8JhEfSl!HWUIkXy=G;9p>rD3cXnn|HRn=+g2cl5F>ZtTD_RcI z3Ud<0$91FGYyx z?Z-|A303lhcyOeSFjHHHWsK)WnE$`EI>2n4Bsfgx*x#)TiMU3w4;^N{L zh81f)>Zhfx77p>$a|Ik|wL391=tl;>0`Y?5mC^MlX5c_I}AsV{$9RckV+r9WK0hZ%^8vo1dUwVDZZ*(Y6ont$|nRw_#- z(!{M|2w?oCr<&ioibTwPm+; zY3wFb7>;4SN3 zyWsCTem6i$>?Mu%MGNk_3B74;XBRxCr)G0=@Rhp1H_uHrF3qKF7G&6R*uA5Fd!c-S z)cWqhPb@!qM9k^Yub%05=5-tmq=VRoudCwT%QGkyv5j-z^+z<~@q+x7?W~k`Q)LWU zCxgY&R&3%s(AsSN5{<&l0@N43W$x#zo8I+Sicju>&&W{6YYZmqKLSml(qhde_r02r zq`YkX=x@vKrzsq&-(C$%dMzjAI<&4gj?765dUez`{oC$puRLH~U7-`UdtbN%B3ALi zzFvJ%fj!;$1>0w&)T8}vYd1AqLzLsrZ>I-DQYwd&dD(f+bX+uujRPqAmz|bA$HQ zkckGa-P%kN(rP@13(RaL$fhF*ve+Xm2-uarjAnK4D3ZS8el=`rrt~jZL!0vci;<=u z9AA-sKOS}y6hK+4_e_$oA(SMxGt#Epj7>@(@|=z;5bCesUN+^G+d3p$J{4*gdiQ5eeUr zl`I2E0wWgIRsZLXPTwhciCSMcf2ad;R+#gqE0hG{aGhjt%#|(!6Pne~{xrFjoI#W@ zHqPpTnK`{kUYg>^e2xqCO|u7E8(YzSuRanSHeZ&etCYQdu6Xf#?vl&*mFHrj56~jk zR}HGm`Nz%}D}C1x{jS(EY54+RL>)knfP^8}@!`qjGR0Iq*u56Qb|srPS)!hi`liKiU_o z^?!N%_fG~7R-p^HMxhpCn$g-Oo~ZCt<|=XL_xAX~0eP4fCl3|dN3f%PZaa=-a#%OL z*ZLGb8lsH;7+V$$gkLG8D>PoMgqPjTvOHXQMV$5TZ&75>Cwq4FZ^p5V;aA9Sb{ZZY zvkDKeJ_r=R9OwkO`(00Vw~Op~-VO0ORUuet`5`LTpm4};JKCsyv+n}(;tB?mf<&oe4f>_kPmYSB1kj2uI+^?Lqur_M@*4xI?gvvB&O> zaZ_*wKvGM`8-o7oh?;E`>Wc<3?s#_S>{$OdGwakr)N_vwA_gPVunO$;<3B1Zr*gH4 zyIu%HWzus_jUj{7IC13&^chxuVy4lQu0JFx+4=$)T+*FTPUye{*~vy)H!gi%{} zlBIoZDnJJ^&MOO>voMwu>7SpXeGF0{?g(o`eflL^*ZVB6mE9B$uVw+?2Prl(+hf+m zp9A(^KqwppIo04)rgeg1G)jlPUfHJOBHf0XpM~+hxD0!q`6j6M!Fw0XW-Ou@-Upj& zd8e>137D3UG#Y>D5!1Y(A#@*cM-#m)3cxz zf(yB0dr*Nh^R`DSWr>%aeiHR-R3$E;IdW^yoL%=jKwC+o8du?|o$YdtY@c;X{Xh-R5q4j`d7Bj%C^x zUE8KML#$L4930=tUoi8h30b)IbF79LTcDj0uEw5``|*E-Cv@>&7PF5NG@z;FUwzTQ z5_6W)I3W(fBO=WhWG}mi%%n=B@!7=xhWI{(`@hT7maRX6LkQ9+Of2YN-FX8281{{9 zftGP=p8CUN(O3Lwp~eEMQb*x(oTLZglmyc+ztRq_ZYLZ}Exw(DTFRt_$%-~t+3n5g z5N9b1gFnyvwq}?WjS(US=Emj_uvKW7j2)D$DmGO;?V0iJPW%6ux3elgGZVW4%aFeF6Jd z5?T4|DoJUaX*xdtt?=rQG7$vwYcO%pusz@89_b+<7m^a>7EyFfHfl- zHABIk8kTob z6;X}S8tde;DGVoP?02cz0S;J1C{?jPP2?JgWSbm%3b>tWesy?m?Bc~_Iz@Y!0;4R# zBkes?2Dqp{I0V!lC8l)**guC;3`$P&zfFE0<$8@zjU=WJk>Bc?VJey~R+~2<&G3K0 z5`khTu+h8hTDPZ9n-N>>uBLIGEC&s7OM_}8oo2YF#iE|A&VV7qb64;B?lQ(xem*d@ z4?bY>dZh+zL=sL`=DT4hbik}iPq;b9C%8u((#pTBQ}$oeNYmHz>q+4y1L1dfo}Stx zfIyb($B4#Y zYGEnHYVwRwVVbrcU>dgt1H_CtyZR4RXf}VD;rL{+*TW-^?Q^TKZ_l0*wl}e*wuo#l z4oVL_MLYJ7=D+5ujNY}Q2gY(Lq+D=yGQjc!LtPTClVBXV;iSkW`;V26_cAia!`FF3 zk$WMqYvGEMgq?C0@QPeoRRgFFnzsw8(;IL~d>r-xuwi1-Lr_AwKx|kh6?@A>Bfzkd zu5QZc0N^!HV{%+;@`+w3%`oH+(r7EZG)|Z`Z}`}tb?Vo=B5a=x@Z5eHB1Xze0ydUu z+4zuENg-ZW0EAlP1vKCQgSRvnyv%?d+!7(ThN=%{vg^|~@OlO5bTD}4 z*fy))!>YGP&h4Tzxf6%aNPhTLUu+qQ@WtVd2M;!bKPr<(8QVb|TVia`IzA%6sCVa`j+jderQ}Y4f^?+zgZHO)sL{2~uelVQU<)R=2)3LM-58 zkvu4#EB72@qBao|uAC%dBGAhx9JQ)^DE&5U0V--1mbB{K60z5zLMl+P;;mBA zQCr_k9b=rOzwa{mwY@Jxk}#%leh|gVYujPCmZlWqfAQoWU#`^4fd6~i2?Xr4sN7Ay zr8J_>Z*65gTCTkw4Pe`DkeBKW{$Jt_=LqWNeDi|wV(H@ayoQ~`{i?iY>AHJpL2Qn! z6{h%vQcWYFnC>UMMo&6ih=Wv>!%psWk~y?<+SV_Wqqx+WO#3sWS1*ZU`*UhsFc*AadBVTY_HmQJIo4! zy4?x5E`xKR2zQ@*)PkOks~phTiS~WBi%F{P$4ldng$((%MSN|94B4j&MqYpgNLp*U zg{aZ`{eRV3r-z{>sx8-hPUP%gM2uo&a1UR~mV8}i+u7PCqO(|dGyi|6I?IP9+_#Gp zN=S!vBc(`pcZY&X38O=LNW&=U9NnWsk(TZpT}qA~-QDcj_r>$#_YW{$Z1;U%*E#2N zP@??5G~7d#e;5P`+BYKKsl}4JL72CnZ;(k4TT4gH^F!@``4+f6om}J`wz{ zJMt^a%&gVRHv&pXINs4Rw4+XRcr#cb(KV^1CocoQ`#U;Nw+QL0@cAjbW+p>B+>$7EpTynVAKYPSHRzOCmG9 zj{56UY3FLOBP3fH=lcE)kGZ~yCtWEw2x+D|G}iqI)V z1Kw$@0hX`F=$cMSP>Lxq>v&`Z2dqqs~8>yIDtkF*53a9+n!L@Pd_yG2de7| z<4}LtaXbobthMMkyL)n*L3!L_UKQ3-E#c~Vlf*3QFGV&y@Qa!!mo?gKg$V7N;N>h1 zJY0p=vZ!UF{#YSxQ;7JMcK~FTC)@h>!T_B2wd`49hfAJ$MEyj&t+70k%{1K| zz%AsX?Fv)=44J!liv|rG7i99xcw>#rW2tB}D86{&E2H_?WBXf`o|-;I={9KkUn8sK z01@afIjitM9`DC0v174^K~JyGQn&pI=#VwMB&x@c_fo?l$LNbMvZcKDEm?gRev@?2 z34Z%tARw*hg>(A)RL!Q~X`Hq-bM^81=@fB{I=PyN(u`V%YG2>aA8WN}4OxD#u%SJ#G`t62q8&?+Ffi~ic)!S#W)m-A#qi=` zok+dkF^HbSO`jdR3qXjN0p>tp`JWiz%H^sYL97We?z&PZa=j}#g=MxXf zxIzyuMD65-<@I(}RqC0`27HfVZny212GD3SKVk?q|4mP22dQIxTXU%Av=+F7)?Xem ziL=Rf?U2aB@t%#iNXz&7@V)rf)dkW8=nU0zU*o()E+nH{KmrU_rshdLeC@3oWaMP0 z5S5u2k=6+yr@(Wp)0t|VwPaFzFh(WEynW;kk2G-{bTAYj6 zILg_PNC)MpdTK_py0&zC5z0?WM10FGrA)gz>Rgn*J(FCj4;i87!nDwSg{CAtwnph0 z_1d4h{q(5OD7vGKFp!frxPu9pRyG{T1Y7bI_y~_D-9eI7@2%Wva7R+#JF>nxU;9CE z$@0_myq%7xcqkcrc?aN^s>>|(o;qzCgQ3L_iB%_il0{^}W<(_=A2QJ+^V9*_(c}L7 zNq=?#UQ%tJ**qNmeSHaD+eDn%1G(FuJigB#RHYdwc@cGfqt2WZP4pA}nyef)dG#mr~NXR+b@y>1m#e7;o&FLFSmn$oq|9lq;GBr~7=W?cEVTM^dTf!{YmE-!Z1`0#QcOEorXdPD$ z#`mbB$zD=qG{9bo#KjiSa|2YrFYQwJtwnGH$z_ZX^F^}930JMmx+h9-lP_=!9C)r# z9PiA;?a_#cDvImbtBl6C4S~jIw(;C7n<|LJnRs-hbQKa0Nq>jbXZ}Eq z?&E%=Ee*5%m?rp$szy&j(j6c+^fmPIa$=JI}%B@q+-vWtRT53Q> zNIhaz;33RF=hcq7Y*2os@tzG)^A)|+fz5@^52=kyCTHm=3uGJD2y0T{%w`_b;KI3( zyek@r=-zSu)NaZkyZP9FtXU%9x$U{_HN$~;hCJO+$vExaOs@G&qG%T!-Z2x93uSE* zV!DY$6Ird%Mbbpa&R>sYy4U{?wzrNp`qmZJ0n1R`!6&%mMSY{dKG92OBC~-)ipGzuy z!HiCDynVJ*64)buB8f|6iTN|#g_8lvJ zoHFNyV|TjBSvaNm!rI*FQ*CYE44Awyv!yJ|kuvJ9v7lb$Y_p8S{OqO2dXD5$Av2f( z$PO$8`-$X60fMp0Hya{^H|vGSPg_|$oJU>+2JyaR3BImyxdJa_LF(nlGGr`MFmN%1 z4W*eth#ut4!*Dz^#)Cv3zIOz5fIio;6E%aG8_DhqYE}eBrea^+&?4*Iz8^LsRZcu8 zZ3l9WABJg)@f}SSQqRy)R+m_NorFb0eg~hVmNM6L5it!K^?p6 zE7k$pfv_|qhMQ=lVBxB*t#Eun?kpT0XKe3ty<=HDAHH1~o$FJ(H(W08E_HmQCSE8& zo62_N&u-#@D!@tOLHN>A3sm3i_N2k@yC1P_b5iNJ;_c?{X5`;HAsNmkyy`F}w{g75 zLgfiSK|*VqYrXlYadM&7mDu1d3qDiBmU@}vUqcUd&ey`o3-w4cQVg_nT|D|)S;)Eb z&)df{IesY=1g|0_JlWQ3(^{*lX+B$3%vWeDTQpVlvS~EKT9ChVECStIO=ec8dB>^2 zMM(H7RzT*G)Rr5mGuq&&d#Uj~WYMJzPMfzEI@-3rw6tRa?5=_9TX^uNmG zGb#HAhbHOJu;)M_sko|jOiM&NJ#q*=(QL`bMBs6BvDrMO1;lCR2E=oR$5&nae0&P0 z3COWZ2Hx_WPh_$u)O4ZF>fJ*5{=K)KaikqHeUOw`@%VaP?!N6=i^2BqDu4ZKEX~x{ zz;~y&A&eRAZ9o-=JDv9USzMoU-<-IsTx-X>YVywPKPq5-UjjOIDgM&c0t?>^EP_nj z9qOxc$E#sa&a;!VUp`wn$6xjKCQijWKhe8iwVzWP+HcN0RUxiAg^(DX=fT>z0xetI zRW$KJhk0}vL;E#b)+$55<=q_0GfMNajeih*b8G8MqTyf0CyuTn=iw(c7-q{VNDkkD zjC-t_i#W>4zC~)d*dDX{M&obyL^cVq`%%DsX~5e+rLA}91nRRXZ!6{K{$s?RNU=Sa z_#L5X)YF9t!yXO*h?aWn{FGw4<2mlxyt}sFX*~cGNa3%O74MxK;vy`-p7ep+u#PYi zufm-CQk_BD+cyqCTT*IXKB@9hLHjFB5P@lYUvTZ*FP_ixnAFmf-u0u)O=aKyx8~|@ zfd8#$z>Vh^;OQSA4bgO<89s_Z92cJ-8`v%W4oWmF0#hPqN3dXrO#gdi`hO+ z+N;A8_t=QEPC`ya|e)I1g;-ByIBU3oSROZ`iDOeQ6Q7eQ@WIkG9 zn^?7Vzy7EBb^r~Q?kXc>I1w_@TEC&qkcl z&ejd7_3$K-6c|8!Qj?lhf_uN;^SRv-L;6dANa}=bE1xW5cs~LV=Q*a{EU{`mgXm{A z8E#j5M|!yt@K7oZ8Sd>E7(Bi-PKBwPnS8x!zJ4Zy#RlruX-=9zjqckIJ zi$)!&&Rcs!j*2u%pbemJaSYA1n@}>o`0%;s+$w@cL}KLD)OHARTEs(1@S?f}4U0GU zmswwgX_wCM72i+-T$NFGVDXR=g=Nh+iN#7%gECF_Gil!+Ip!^UaxtFdNfmQKK3uHN zL;kiSgg|b2a*?n#kE$XE^7~INVx~gO;Y1bmQ);5S(fGTr$p2jMyuNt)^NNhfeU^*z zrZcf-b=8SE>b6#v*l!ej#R+QhhFG!Rh*K5%1g9U23ECgk?o2M*`qjxw=@%m^ZSSx2 zkj%Ny9&CHMQ&%uO@9>0XFJ5rT1DAj=(P1hwsE%ht^sR%F*wB1~h2{W~S=nK!v&D6YtJc^JI-Fl;H4Y$8+0h8C8Ui&eI(Ba@n6Bb7v<%BG$D>bDblmvF+r zQ3HOtk-eKs#p0(YlYa{rdX@F!2Qhypq#oK}QH_B$)?>(JwM?5wB5g0@W+-~tsKqF% zHWxrj1R1OOgZc*r0VXLHV8_cu=kcQWO}s&ES=dFpWPN*wL}boyPc&Z5|D&2)#L2E91_6IY~})gVxyzm#JAwbH0njHKeT)o9eTmqcDIvgU})XYGnFjK3t112rS4@GN(? zB`qniB(NrCh65~WS&cP*JN5!a`*4BwbeauP1W|-zXn=eICnnPvzl50#&^6oZ$R025 zB5eplZJcqaQqn{!incFdwTyBvT(gQ5Bp7!721zxzn)UpkIZ;gg(Iqo?F5!KyAWCFr z8RPzxN535yf-Xv%P;O{#0^Q5y=QBw1$D-kg~_5E4!>x(t#UNZL0Ip8WUk^%JwZ zvflE)e+2uVzv~L)H~n5Zd|aV;9*evDYxD%iOkPqFr8n|A`8I4Fa=6kiTz|v?OyDef zfI6>rFQ5i4Hn}iuE-L+yq$IH9zne|Dciqr@SL^rX15#IR(0&^zot6A`~FxFqGOMq5%<$#S$XqbtOOf|clAR~QZW`iJil>w~bxqStG0FJ~{c)y9Tw zTR2UnV?7%GuMt#WVG=Ww5%sO@Ug0cbLp40Mif+e;G$BiJKnVBYW@gI)-kX79h%~E+ zWPLSNrw%|TYSwl6xj+DyqQ0j=qQ%^%o*XaZ9b$0t6sc{Lr+5c@$N_J98rYX?F`=B! z)E$>3qC?nIPA(#Pi`s0{{73~yQs!8w#f5FR+1-}&y_m-_jm)vbca2;)-%i1gDwgv! z`N^hA4_>L_cPP$CTfmVNNNz*)(e zQmSpIFSJwHI3x&m+uQbo(RqqZ`CCh ziHi8e?{D2YFRYHO<)>qy3cybQ^Z_~z9p#qZ5{cS$I#%+bAAF6&6zvp}zKnbZxysDs z*)OWAAv-bZH!V8~hx?mg-bMWH3rfNedPy;kM@PEdzU2Ar1H^P$*>#06{wwHhP{Cd@4#XQ&1^@*&rPS!>rNytdYrpF7Mi2!2sqYM zIHswh`5sR9n2YikA%S7}Jhgmolz z6+HEOP#f?~$VnXLuAGdoplw*W(;jRA5NK% z&Ob?H5$!fJ(@M*bU=mu7fOg7{4MGmpI{RwU%H|7|;+(!z`TR7vl4I(Nr_QjzB3WRx zXgJv)LwmK_!FGLd>=4)by`{(sncQRoPS}^cu>6Wt7CTmukxt1haGgwpy_@Ay|MtQb zj+!L-i#|zY1+Qjo^jav10~6AQD$2)Qj@HbkO*1R;pdrM8H1*E>N>bj)#YK_edvj8G zh2FgS(AIdjJF>#yqm41@UrP@~DxSzG5DvY5yj0Krdxtd4PVJg7+zc4oWH~piz65IH zC-^>Te-YqjW4cL)HuF-tc?QA!k?YJ+|FsW7lo-3X&~_nY1|h-MkSR>_Fb3*hvyfxE zB#8=4i-74(An#Xhsn(_px(<}>j$A^joqH#18b~B)mD0FQ*gwG{#bPHK+AczkQW8Gk zl~s2oC-g$i_hv_+RAI4$s$J5(C&&7kgyg?!73W#2nXcc^HfpoA_HZsCfV1cfTC?`{ zr>u+cEKdMnp^b>)=8zcN`VzRQ(3WJnF!s-(WuZfy&+Avj=0cgzw&iw%UM;GJ3H&Yz zRgG;!Q3xw8&R?@3QJJkLY5L#0g3uhtZR_!Rud@OLGl-E4Y|-u7K}$=-jWJ8?K68eR z`^6_o380nQcQuFmauWW5JHlz{J3!TaL&r}GZ-_KGHw)Thx9(W?q~o{k>zTw74LZnu zMrX|1@m<)8)8*(B%RyV}&ILLhr=AJ7Wks$u;7NbIwCj}I8hxNARC`;Ja;^N_(%W)( zg7D@4s+zaD`GKN)lUFXUB#`6ey6B@B=s!&p*Ek&b)TB z*>ASrjjNJ6;MnuqF@8ukve|EtY9TiCJmv6U?qju^;Osl54<=>3^`7UD+rKua{XA?? zDpI$HzSuttS9FoGqfd5PDc)+cuKLWkt(_08lLHzeKL`d+jTqjNNIeqF;-&ZQP#I9b zf@f9{Ppgmx74!tZHZLzk~i^-2SXea`2C)kEoFXo?82<>04Oh#ko7<+P{FRf-uk6M%UWQ z@QLZ{TPy5{*k-y&J+0Lz zum;f(3#EVH>Po#N=DHgbqsMdyf44U54m$06?N_e*V=xoztnlk!%yrNB3W)K8*^My* zW`Xp0&SGwapwgfJ5G0-c@qo*JK`NX~xq)N77DRtIb5wonz#?9YaZw!s6PRPc2s{18C>8bD>W^=SN+D${el!4etKWE96Z}l%y#!{< z@m1;)zkI#k)z#H~EY(l5I*J6v2rA5G*p)tiel;dVnzr$%w^N9xEd#o)g9u4KEll(v zI{bYLmp5^`PS?1~pG`XaI2K}g*tAC3|qjCNI294=vL(4zo^RVoVea>~O z^u5`|^}|W1QnKR=PzH@htd=ghmAA7?5V-x6dI@&lUyf4;=z>T zhU4GGa9>fJ<`on132~!+Z{f7$Psl$jVD6vHn7NxSTaVWwyZQc;B4QSIbbc%~Brax- zgX%32wg~@Z5jZm|%ih|?rVzc9giQ)sy55I}j@DyY-Q9(L_kFuBx?713_mjS#&6?bf zc6JgFAE1xj%VYP*ri41&^Y&b`R z`xF9`20W+cr-3x*Ex*87vTU~{2n_<)olu-EXa7oqZEUVaG#S&L4@?; z7J~+HBh4uYq1`dp#@5TmvMbQNx%zx1Er$aLQ+}39M#sUDX~N;l!ipkPEo0uq#*h^QF^3d}CVSHVh7TgLTzAI&f$VSO33at0_i7};yk0uG8v(enkC z{oz6SfWISb9`my1`5GI!vM9F&95uI)SC2yk!w#ucqX3LBIPY~)&S_F!8m)CI$YmhQf4L7`t8;+ddm>TrokNhPTo3$IJw z_hyI+vuKb+wi@Vo`E2vlfwlPU5Rh)?8JeoZj^LV*vabK*U;b@3Lr zr{n49bp(vwE_^?n30Md=+KRtsUNlN!RDpd%3-0qb$YN(8Kz!l0tN&%oP?9VC&Jjk4 z#vti(83AKDd^{tV7Q1U*jZQcA^^0Zg&n6Qb z^xW*UmkU*8Qb8w-c5Q{nyegsk7~Ol>tTGJl7omZLe9qCP*Rw`jFVBK7dYeKyuMbuq zJ#q8)5cmvQ;#4P=-5PL86tR55o$#^PvKQCRZG}BVmXjC{Wf~m z4fZiD7^+E`h)TSe)%qU%;F+0hO8f%&R>5iUDlLVp-Z6ysh~JV2t3soRXRY*D0+D^L zU`d4aRs6G_Z9GFxk}x@!EpvEvK~7%W^zr0#f^vPcsXG1D0pi}>?Ruo{qx7CM!!r@6 zBSNP#iCs;6tdIB!YZKE1%@)AW;YKKhS$iSIxk!K5{fc<)1cVs0y;(Sw^HPXw-PSWl3%o7$Ox@Gsh8ol+J?|5@S>n$&2Dhctmqdfy39peB5M} z#%H0tF(eBZ@WNOyRfeTUSWa_2bbU91Nb4-!PKDE#(x|fGTuk?n7*Mi33IC)L6?ujLajyhq62+ zSPS94j)lDk8*RSdr}yW4LiN_j8lFFb{EK@^8nC%6jqzru*Km@R+rmmbXCX4B+=&!< z^-RbUPTZUl5BV7&Zy~i6kF+_^JjD!1%ED7M_K7U?aQ&Y|V>fNHfZ3`GmWRsEy0cnk zPFhOvXXQUE^kqXIWT&KoyWxOOMv56q{L=oMTO@K)Rv}HJ_7d8y^0!sx1g0}GT0O9t zao}Qjz{~^oRqkb97<~%M0F!t<n3w339|dQbzZL^q8D<9&!EMn)U+$2baM zU5Hm&-nf-GAtldt_`SN+v=HdDJ5R}JTEn0~vk33R4oN?`>Y&__&nL*NO7q-<(wjb-$#p=_u4BTZPCkp|GtCiaCrq->yqA{_G5Rr^p#-Y zp$oXq*Pr;)zsSg{Z2(-s1Dzeo$lSSBH>$2TmTtvi@;n>SH_O6k(ACkEtTP5kES$;g&6i%< zhSUxSwO;SFKVR!y^f%va|FsjU2bF+SsDJ*L;9}hqxb|IdCReab!5|;&V!SzQ=KmRA znsWS7(eLACOURh-;)knoap!Un`=gC@X1tUX@)5S?rh0+lgV`p%+y*-I-nw#|c4}*1 zsI&js%sk2!4f9!#y8pxdCx7ekpY=mXsI|3SzW2lY)b_+P{E7i6{VwsW}}yvP09+#IZdBJb@NCbYsTz&dNqTnO| zq+eLoD1Tli@xK2ReX$q;k~^6P*w$uM#lWUS^rCfF|F#awIvRdiFw;GIIVN45zkYBa zTtD^EF;@jq)ln7;olbl{&ieLH+@Dpgx_UNqF&6N&UK_Enll3dD-t6E$ZEPaoad~?% z;9AQTzEV20Li-A$YIP2;e8s=mG2!Q~epfz52(e$+1X$ha047W~a;nHBONDZ3*bE!( z#E(~|(pts~C?EO3Th8Aqyww32q9w7YtM=R0HlK6rdCICRawNRJJjD-8*>DT)6scy_ zj0<;i{5IpO8?Mtc@s>TI2zjmfnLAE|r2E?Rk+0Lm$Z)hX(fU$-%*-)P3aFB$p$K0= zaQdeRZGc%RU(Ptpv|0=^jrTa&{}bJFm%CD9649hszUMtoaqI9R zCXk2+O{oiuBB&HSC_COqOsBQs6Mh!LIIAf3YO8HYU*i4IIzv}BNtid|_IG`0Q-84N zo9#>>QO36h7i z?6>DM_q}7PtvPnNp{qlu>0>wlmlXNFmUu?O0y8b7`91<-QbB_GSaAid=SiDb#I@(F zcpb8}`h2u1^k43hq=}Hgi(kUxeS#p8Z{{tx?bG_he491kZUi8Ns1lewkNH#A+}x~> zV>!;_EnPv&6A}^oto&nZ>T$%On7t8*cwt!==>`Lgcns zgv0gX2=grOaJ!B7Y(h|cI?8|Dp>0l`-Yh}5!Z@Gvl?gE>P-h)yD@4?d$6}1C64zUE z9{zGG2qwm$QrDD+S$<2Rub@lQY>AzKFQ{w$IsCvl?*#yvO0tozmlAKZrJ5%F{%wA|4QS z&*$6-z7B~PH0fF2a|UhZ$$G+cgQANJ2g5wyxG!y3V4y7f=6?t0V1OWf^l1B=mv%FT z6^1(5C-$53=v%K9j_njNya}r|D-Qlb+snR7OnwyuY!@xVe&5|3RS*mz5*KOfeZxP> zMJQt>WMls344q{LN)uu_saZrkako#lF+0LmXWKx2TYR&Fi|3(OLmW-J*07)@tnV!0 zK~5qJQItcYArdy}rmw2ml`Z&b2R~K(W>hBVk=3tSFYPmBiLXzJnPHE9sJB+7m!aX$ zeRpR(QCFIXY`lyZ2v%ao1b)|9_&9YpnO{dSdO>qJIzBE$A!dgmoGw98#9Jm(+;4^9 z4%?B5!R=>A<4G8iL6c{e-<}v7>t9Rnrop+!m2rtT;~8OKz{|16mr_ArCsdy9>t4g} z4Gfj#ok)746w10HVEKKrFSMHg{T0<`3?9PfslL&`WLd^gU~vdOS{r*;QKVApX|pmH zu(pm37x!2N6U1?d`@&1qlqU(jxsO~26P*|9Tf#?5%LG;^RS`~sMhSthiVkvcR89Oj zBX>*4&M@!ieLTWYD5HjY@$x7l2nz?}kSglDAdIw)EZHU@OGORl;e+uF*|Fq z0$@E{;+jH<8M|{nxkX<0=3x=bj0uT?=O9dPt@OV42S~k&(BlSE=gSesHP{8yO(5> z`WS#7ogbH@8|1CO?cba@RE)h%mcz;oANl$BWuSF3O)J7&ujdwMZxyYtrUKaJU(L65 z6YlAqdJ%lt$HMsG9e0MrXKtdMowrQFn^6p4g+a`k6h8fW5V_oxGLorE8A8jmzLg`3 zO~o}uhDVtu<+d|UOGFQqPwow2BBT}3$5@N>zWI6{rd30TsDo|9k*6So(g0;&WonsR z;%8a2v+c%_Xw{B-nM?B!Yr_e*v&Nq=Tfq<0ke7dQen_|~%!nz;fZ5_ZhbgCkKRg6ox&IfmcO z2Mg}?PL`Ug&`(feevsaK$KKimxy;kvD8@_jCv+zh%^uPncunaM7ZO3BA0o-^uHnJ^ zg-Noh*e{_ndzV3rVnF?}cMWX8zE2WnqmdefN225A=Eg{n5%W=pS@_6G6HfP!J6JYO zQwx$>Vq{m&M!3HAVK!_8Vy4pdh2dQiQb7 zwG3`)KtWZ>(+gePbdvFd{f8AYFzxi)<8+jCIEwwi3S5&B$Zb2?Q+Z1N<12JC$ZkpM zYbxtwH2H&q)9}KL08v{E^mi9yfsV9xBvh%h-&&m2cJ;FaLG-7Q0hUEN=$@Wn2EsS? zqum7iNOHf9vKqB)vxRwXRDYxxG%V9@0TY}RsZR8MkGebJTqF_tzA?sCgdHM!=d=HJ zs4J+3;3@-Kc%RmalU2o1nNC{{{=CJF;8p452a_*;D3N{!e-;28BX5e8$IK`KMipL8pARWK1`UPD+D#$k zk{n6}*0t#sRudeN2)bQ_^GXZlliy#qcHzmDkHU;eGhwMW1;}5Th37u2!uAc~lY$e` zvmH*Eb8%&DO`+dERGKdNn)r@5F^wg~dyXa3IQosNQT~TN(6dS*kv5V=(z4d3_F}Kv zz0)M^;q<6m#w1sct%&++qIW{E<+(Mo*Rw|jw80)%##e5ADMAskIF0a|p999GssLrm zp4>A=e@qLeeKGfN7hpvfe~-A3@GtBe=fT@xS1iZblYl{TeGqw(i)EeJ4V(1c1w)HG z9G|c8JjqqW+lc=1IwpoUp;;8r4yt1T2F!j%7M#`!yuZL`?__b2Zk4Cb_Jo!=;s|Vz ze1Qh!osasiqW{Zc9?mKptBV@RwZRvN9@B7hlr3^0`YA2$YUq8bSm$Ew&vsJAFJ8F3 z6OEh9INB3DP$FFUZM~9cp!E{ur{8BNR9=pILZoU$)!muEx=A3F2brA{9bf%mv{55A#cM&_rPD+>6d@2cU7~8JoSn*^2FA;2onGzP9F%H0r;`7hx(v% z9J@^^mftW)`TOTL5ip!FBUw18*;`TJcoO;>q?>(3K_0U3yLEAtGr(qi`J?Ea40Q_V zUBTV;wz%_z9WKY%?gTxU2%I&~?i!lc_WN$52al*(I!)<#B8#OT6S*Ef>lH>8RqQ=5 zeX=Jpg{Q3?Z8;}Ee`97&zNum`TK#wAEZq!yyzd>!m!)I6sS`g|O%*tc&TdzCcP<@n zdL?(up>WKt0G};crvtYlXV|09{=g<>_^d=C93m28>5cOLECA#XJEw4b69Rqmabu5w zVQwrZ^SNkdp~MB0)EwIwPD%tATj?175~OTZRyZ;iFWri3&@h~)1B59IG1ZL6y7)JxlW6-zPQ2k6clRD z1D(9H>rGXvv1vRozR4PX5H12ggqA^U2}IqaxLZ|{?#eI5%1L%oQp>HiUL5P~bmX@T z?KoV$#4c`>O8z0TQE~fk5YN$1Hu^4Yr|R2gw??{6aV+(3m@NE@ks@s==ZEF{a3@Pm zpOI}$q4L#eF_~4jhX$dV!%00Bh0$VMPX+IlRbRBrt!8UN;c14pMQ)0y2=0M)g*)-!NBxqRf7#gaP2D&_ z0jTjm>{Xg=i>!~+n!nDzV=> zEo#kE%()Tf^^siPhVdymyzj?ju^+MYFnfAWj#{GB#jGjVy{uv{?T_q+GKdIsS_!1P zf}M1m1Xz>HUiapO#L?iMS&oJ9gA5)RHLBU#DSF13Yq})2ezY-7uwa`7UR7bghbQ0?=)tvfzS{E?pgmS=h@qf>(GD{dyS$r38pxTjUS```rdC|y~z!F zeZd|HTO(sBAdL>J@kPj7w49o;C$aYgr43NMxq>_vk6KZyEYg&x`OtxP<{y-{7_(jF|De z26Q?d@A;^@EfaL9ACwJOeBsrM?B!v)@h{%#NbEHb{P@OcuK}=MKc5!#)@Na z`B9P?RM=pGjh;_XB1s!YcYifu0-lvFj-;PJ7XzNFvGto6_+~N#@$iFyT8o#B%baCf z=eURLA<49Hw$8zC2JueIvY~|J!!i}?XDIRZQUj$iQW4jQ=W3nu^n_=|dQF?mkDrK} zQ8To{#}k7Ha`-V{hJ^jVT^sER(ujj zo0IR%$KJg3ljuW>mcGmp9FwOp5Fs;FL`nE7`4rmSGK!iFB3ct_t=NX zhlRZrBnvoAUG(Vt3hmRjLI-$9J~RFSaZJ}!U~mNIC=p|L9M=1&!YdZ^Y(mP;&qown zZH0Czw_n?RmvYP0Brdi1;D*FZ3OVr;?fmt^r6kpyBQ#j}6GkQjg%Khcb3aHoI&-5t z3%n%3#OKLx-z8~cTrBsq^<20QyryM^C)sp;ZBm*AKo1q%jdOb#Z5uCL5#&l#5|r}`Q1 z*7WHO04(n#4rUCSCeZotimtgotxlogxe{jh{ypQDF=yl5ifdz8o$r4IrmCglmQ<9CQSiiZFbXa{;N_1$VV+_q~Z&g?A;pX=LhAA$z;i9VXMY9V4E{iYco(y3xRhMxEKJ{XMyMe0QKmL*I)we zG8lhX7zzlmm398w-QdpOx#ln1^jHZ|JwPY7TvygfPi(4BeLfT#t8|t;9LsFX^LD*x-6~I9CL70!lu95fwJ(lX*oJ_$C z(d%iU&2XudiS?;U9y^6rz+wbnzRYs|;f(OKXKnN5%d0V2OFi0Pjr1qOz{R_6M0JP{ z&RM3|!(pI-c5%NksMe^{a&11oWb$3da;pKUd_B zC7&0Dpy&b$Jd7!u@sqp1Jwo_o0+hNf<(xrgCKhqio{5bKC{yrU$9G4RTd!i$2@6do zOC|$sjGq1N9xQ8PpLbJDL?$nD{0039xSQ%xKUV(0+3V#Q$di7Pn`}2dqCmnO5)-0w z@Mbf8CyI!e?UTY3&D9dIjnM zOL7aLjVy|UN(VMoHcUrq3$gQf*H;#T__=K0tQxA#Vbus&-%;3pcW^xruAjiG)_oH< zz??vPwfE*9>HtR*8^jKWQ66+K=By|#v((n^h{-O@d)(};Ol26|;5-myTg~D~7{8z) zI|C(nF^VKGKD5=d6>dSel|OWNu+xaf)~`dJiO{ickmMY2)mCxI-*TN2HA?fZ2_!Y9 z{R8NW<1x&P|Hsx_Mzz^BZNn{4N{d5_yHngPxECwX7ALqAcZX8k-6c@06o(+etq`2z z5}-hU;t<@wT-W``yVm#oOmeR5bT;j{TUmUOY8XCV@yxGXLD-ZvA= zi8=3+;H1QT`9OVYm2?~|1s_t*MiHusv7-Zy3gLO8{ysWor8(}y{3n2pN%tewP=*T< z_m{Y3=mekEjmMh)C<9aLHNDF4yy;x}A>Wvk`WB09{r8M|hvnI``SWEk;dxdpNIh40 z@jgP(oyQ7V$M?XE7I?C5g5?!5hCvp@hYZbzw|}YJ#uhVuB{5GbYZ<%EJ$!vkm|9Zl z2HWBJDL%fFYb)uh1-fDuOpsB3{GITaF_4VRR7xfd+%c+tA5&^PaCb!wC-I1YS9 zy3yJWJ)q76g`B5lCs~ga4%!n0O{SYMg6zA!P$mZ=A zdy~T^Ar>fO`v_n<%bI*?-D_zm)dF6Div)qS6K}f;HTozuK{p2t{@)tGI$ci>)EDC{ zbj?gJb|Yw9pROl@D3(}kp3Df8hG;c-E@<&B3=;asz*@AfcG}B9R8E{g9Yc9|U;zTq z0A4tNd3G%p)pfPZBv~(~GI=cb68=2>u2kpsv5EReN1%NFzLVff9O|@EJKKk~Ix(gG z=JU5H(^D4y`V0qdz;v$>12g@GXHfT*09!4qde4l=g|_00I}vQWDRGEdDf^jbiTguKOH#*Q|2+i@AU8nS^wh3(zEDgQ1vrv^2b%sqbYrwf|FGIO5bm{m#F|DV zG(d)ojdR5COCTM`QrGn^t~@esy3EXDmvf90goku#QN2!T!>c-G)$F zJ=&EVE>gxMUr^do1XU=)OQ{)xYW|KeQwQ8aV!?iBNq20vaEZBlDaVA4pOBo8C0L`A zPGXA>A%Hb*VNlY#XE$cs!UoF5a0Jc?e^X~?1 zxSs8vj_|01fF{}}92dV*ZDYSq-<+5(U51RY(K&Z9ZAda4t$eY&npB8{KS=dGq*xa_ z+~ZCSJnd!-i`l+t_3ga50d*mZY_D>5aJZOVD_FukPyB{+{m7^d@q3=Y7+Yl%#@bXy zA8*=z_?y$}WxgAqNjS`=Rmi!I&-%5U!$oOt{5T-(kPmoAA9uI3fD@IYaB`q`Zzl_L z8Qb#r7hOs=uQ~M)7{f*8>h>PAg5~sf^V01194KDjUFQAT-mnZ1P~7g>FiLxB8b9UA z0RIzj{RgJ|ui$IJ9xZb}%>4O!`jzPR_hE_+!_kZQ*OEI`-c)WfD^a=eipXQZaTRat z9kY5a76HRPhRvAO_ehZj?=A}O5RUyB@e092Ec%@)*DNs~q#bywTSj zXCTW?Y$4}YG=p_>eF}DS_I>I*@?)U7YMrvW(Sh^nQ5dO6KH|&HD0-I;+?)PRU9F5 zqtCR@0v+f#EQXs$7$8u)AdaDwOt{-|LwCGuz|MgvKP<(L_|O9^woPSNq{e{c!=D7O zw9@!ZV1AMiNw1|*w6I$OU@8^S&Aj4b#qm{S6N_zHLm1j?anI|hCn#bKVNfT5n}EVw zu*DKTQD=394)XQx#L}vh^uBLb#%0<}4rJkiN>MGCLy3o(7TnY*dN5>{oKtjZUkd#R zy~oP2JK&w*18hVAw>!0&Cb;3a>)%wubNV}D&9@0%BZ<)^8zt|p00a$K8P2@i6N>M))3|ZlN_eA2yWihV&ar*(}Y;S9C=sj8AGnJ;I<|qr01Rf=1=8uzm35q zZl6HefR8hzOKrvhEc=WzVsmaqhF=~M{nZNJz1yPcxMkbl)+rL-#NYDd+R}ThcTmTk zPc6*(S>_*=DL}i2;)#(U0{?+q$)&>AHHW2Q(4gX4pU1h+kUAvRd>q0 zHgqDyjKN!J^sGTb85E1sW>23?FTYpv<6g(dha-WYW=XaZ4*h+J`(c$a^0g;*w}X}DZ!BZJmVZk9 zC;V|l_U{XGO`#L~{b?9G5O5#knw~6{qxQbU+|MvgZ_RAFc<~9-j&Km26Jx^S>U6$U zPQI<)(3&sE!Inw-p_RBU=`sKFvgcX-YdN|jp8hmp z8RM_~{RJ80nJ8~#E~x`aQ1hlt(!+_e+*=mbGioCx(I^hq-YUO1oW)YIQH{DQen}$L zLa&yXPxFa~W{@gH(F~=!JyQDqncD_oTKv=+71q1%WO+|051iR{r&}XgIw@>j#KIB% z4)BtY=yYd$+xKQQn#%W^^Yg_Q-;vG<-6P(MC_XMtcPw}J#5t%C6tkuiB{;aJLzI%E zV2W(R(OArOGW)U)L(CuD%+%D_bgH)BDY4j9N^TJK^tCJ2Qd2ru_w_DWG6kG98=pH2 z!|#2l=zwrfrFQ4U)R26(F zdAeZXfokqZ&P3_c5NR<+q>k`CGc}thd(}uMg2%waFD*k`fSvyGk2Pr0SC|0^)ZFe| zK2Wcj6rp9wu+D1~%jcstD$DBvNoKE+NYhLb&GmavGveLPxky07kOnHn67+eCcyVv% zJ3E7ftEe*`4#{S^AXe;If!g;@?yTUsQMM&gMPu&k@7Teq%6>0B!I*eOHhsx0iay1$ zc+U~(ugD~>WFA8PViTEwxD*H|Po+jSJ+ST7K@x-v7E0==cFwEKI-pg^UCLOdVz`%!t{z6_C8@?Q+^ zyj(H_^T6GXZO(prt+oCA3TvQle)4TZ zv;k=f(zwY!e*3GEMHBRjoY!Ya(t$_{PXWImp5gSqVG|U_WT0eNUYI9(1#_c2W)a^A zLW_#qu^9z0#?`0NB62Sy!9QwMnwXr}I_4DrQv z=rCGw0RP^ZS>JCN>hG+5<+In&q7~3J73%wK1+cu}R$e{?=Y`(%I`r5d~S=mDqxbYz%l;wTih;TY@aTINbf5=OR z5w(twJVVcXh)`)0+SxV}==fZU#!7;|(u}L%2b>5Zv@^Tzj>HPfGh2Qi72s!UkOVy5 zl(Xoha_qg`-4yX44Dsvz&QS)N>`W9Cq|kKat{@HiK=q?SSI zzpuOx&La#bDN*M_AZVybgG_8*BHjJ+)9#F3Vr+6uan&JQoB;xxIfcvp8cJ^#+ZDHf=zReRUo4 zq3{-3?6o_wOPO&p6*RNm#?mM+kAwX52QsN=IysoAlR^O(xRIe&5<`EL&NH&?)J5;w zHt><`s<^Vv>sa2$f^bB63BQDx;9|d~qq^?utWccnyO_fps4PQikR>c^x#lg==D~a@ zvj?sWOG~1fjam{IrWj$RfH7yGPpP z;rTA#81+o)_g55$bBR$sQB8T55g`*wzvGJvv#>=#R8RbfuRP)b_#S!4KvzgijQp4d z>APrM`@3xD$4amH9pBaMzH>p+rz5c~c#1|*AR-KVV?6xbK-tU|g`Z{+l#2+bJGl@%Y)&=%T2!} z9l`0}2^m-I-^3QAl9{TBxs36m?kh;9Ed#Cny2y95U*1hlQpear(G&+n*bnWYH1g;S-cv;~D4n zzS(hWDqn!cA0g9Xr-8QcRpzRvpI*!IomMdSQ-Z^N>;UC;GdJU$(BH|(Vp|fVE8#HM zaCg(bPc_>Y(yL`oi_vzc8#_h!{FwUnQz_UENc{W0qNZ911p3zRuSr;Z%lqq)JUSv0 z;(N#pTe$;Af)#TJuc9d`&eCG6b5wRTR4~t1x2uGTJ^orY=G-;%yf;VEv6RfpZ^!Ok z?@rU{O16~Lvs4XmNzS)Bf3$E-`fHuK4d_0IkAx9(*#>S-ImCsj|MU+mty#2piXYn> z;YtpW^9c`zs>Sof6mTev>Cmk_ZYNl0uLXF224S{W>z&(;U9D!@1)i@iPoa#7g}LgO z%M^Z>ywFIVyqg|nE|QO&v#EMKk6{~QF5?s;o@E*XimV;1JQmz^M#7K%?Krugy_BNw zs@V;AJewwq0Hu4DU;-nu0=9l_b3g)=vnOzs=+BLlqO#2T{cqAbIh~Cyfj%COBNI8l z#4yKlYPy!%168v}) zP-c-L?(T^2RN{F%bQ%e12Yon(xC{M6RT;K+NzLKnGIWvjJWR>|Y_m(tGB($~PN0ScvwMgcT>5w!#4sv^f`$RC3G@G@4OhOoQ-QbD=GY>~TJ{ zoS3_HOW=d9Sl5hwPivh`49mE}ZBbdtlcoKXAO4i6Ou>WnLQkfF<)e8iE6D>pOV?*p znvFFP0@af{#jLq`A$ti?mx?Y7)U}X`NKPf~hx^_`k_N+hu_blWn*#}3yx&>AzcMJM zfZp_iP@6bks}*}4kdMv(GXfJuf!Ot(?9BFu22J-(TI zrVa5@%R*d6e_U}hpcKgn|Af$7$OO6l0Sv zBmCN92lI@poH5&OJC4^4Ah*`*f4I+{LVKNLwpw?IBYOB_P!^8N~d9GuSN*s>)>blK>4u{dDtcuxdGHf(K8+MN#tsKmOd9U(#vvU&2Ve&2{KlEcB|~w zC_cY1e-ll4Dx)x`aio{i^Oa|}rjH)$=&akRv}!!VzcZZ=!5u0~ktnvSSw<@9VaS|T zmj=@W^CVCv<-991lT+SEZp7bMlyujAw47~=9F(8tm6)9?j%fcYqV^=Z&r(0lY2TYq zT|7yxIi8tf$$))Y#x$OB{hj>=6@}@7OP#<7&Y{$!7(p=v@vDT zFK-Zt ze@<*U{_ER3=Ro5kDReis%~*BU<$W+~GoicOr@_hwwcm0!9;8i*uN9F5Q(DrVQ_u@y7`+Hd=%b*oEEIls;Qfjk;*$^*sR$z^#!;fi zdlqSp!@bdiCJ7xP&SjtJe{qLJ$P@MQG|Wb(Z~|<>ofTLsk8XidyY(WT-{mWKjMmm$MGm=-%Hy|Z*DbAeI1bzYXiPj z;^)Le97VCe@`)6db}a#<(ldk`7`H5_$!rZup)2fD$RM^}`NdDF`YB`9`v#Ys(mgOb zOzq7k2lf>PM(+QLv46wTmQhyLta$TP_~mvQx3J556pFKa7XQg0YfC z^r{Gm;jr(wm#)=G|m>PBbnN##RYy=#f!<5Iy>4HtKuuoq@T9PLu?^Ci*1o0_Bx4h!^lUu zWO^KxV)#e`5vrlJcC_`}0b?sd5KYw^UHNsT$|HdAkkS_29pf*a6zekHE8HKs8=(5Z zeubWzk6ehuYA_*hl5AEC-mftS)EIIM*k zYX^fxahiTAM#^pOItiNfAn2>9SY}7Q1Mp_PXIHFdwfr)sr=_9haA3G}CKMgG#3Xo} zN?h%(IiA$@&e_D{6B9-{Sx{mV1`vlv{u}wlv#jE%yos4t_nYye^1-y6zdgSY^FNU3 zrYWw=IA*=Utmw`}wh3CkSo%F!wC=gzEvev# zBlF?UNpN~ zyCsb!@moy`soP%+>eMQP=UhQI2~#A2?IOjkYLbSXawl4FNy2y1iA9Gxf#^}d6x6m^ z$B<3O*bR(R5rmfQOFxV!I3Y-;D0UK*s`0bDqfG5n)*)qYY9K~zwJj>HIcn!O>4mUZ z{aZaaGHu9J?v<)>z`a60>~5E}t0weEfs)xVDdHWyiRa%*)3~7@gO=-&UA(T^z$=NO-j%=O2M!moZzUa#EGU?=92dp2Ia0uNOnzbx zW9^ny6M|mEVgCiG1S}CwxcDiy;A>bQ9A!JU#J_rfSUZ_`E|_{cIaPy@hV%i?8}kD3 zA>bFWY*DgZ9aI!gG%0F0GmHa~a7oiikajqNWtWrOH1SpG`C4kiku zp+jes7XcYHKUM?9sTsEXrjoIvJjgFJJ>>6~xHT_7a_LGI<+|}O#;sWV3dFCwQo-zv z?X0g2P}l^P1Yd?YJFaNm`JNersecpiQ}PYi{WLI%`@wL_`+2byLrf#Sh;4mg@r?wo}BX831G5meLm3yl&W-$EH7<)CYfNVlB3@Su+Glr zXV9IrZ%NzUJ*8PE1`}GY%7t3FZC;wWt=9`~yxuRr`!Z+|9}T#V+rbfURQ`FNNj zv!3uFEyZR0_4{4fxwQRT!#xioD<{Dix`yM#*659~}W0CZj1`t&+~~O zoM|5^y?OGn3XXRj{a(TE2Ar8cw8)fPEL5vFSB@3=te zQ@`7P0cZVvTd7*>UDg`uy&>Os|Nkx*`(MX8wtJhd+whGW=BOJ}VGhJVwOWX`?9;Zi zFPFdX;C{PC)0NSkviXF+uBO&^E^k{0>M;FLSsvUGzHO6X1G_9fsNFbVZ%|b*T7Lkw zi{wbt?4u+b@5p^n`-^6o_gc<1J`5cbi1C=G4-&-nDj#LetilcPAu%Kyu?GZ!UJ_{{ zA6hU-wkfj8tjBBn-S7;wI@3c=L=yD|!$b~5bKNr>eLhp)01mZ3>!4pY8Xe|a!x_Wf zt5fH&xF#A^H^KrWHuHCUK#q3Ex^kbSt6kh?+Xz5>>Y%`cu$GmCiXWi!)cS{Zuz!8zn6hy0Owfy$1p%|A z(gf#*5~a$Bx}zzO8ljs_kfwHUW4qE{?zM6%JWPe}Ad-Mk6}LHb-%AUsR;26^F#Tqz z2-PYc&h}g>C76wLDgMMt#aIpPamZG|pIFpN3EeaMvm zIrV>ip7@}V7pk+%d)hpH~~;!~UxfSQ`NBON~ww#BggWo^93AtLtf*0*upG5xN8 z+jNklZ|wEj$a^D;>dOPsC@wPA7)Hll%ANd)_=vZT<@XdNb)@z$&_P3*q4?uPb!<9C zL40vh99Nm`6K6KB0ukJJk%(#ZRG-fE_0N{p1dDweNn5t5LHRqFpF30M9UIzbcdI~( zH=h9_GzD@L1O08}6G`%l7>Tu!*s#Qb(^v? zIh!Tc(a60QHzSURF;*X|W%06mSG9kX_vCoVxmNsVD*w;%cdJaDWa*$H!2BuMmQ|Lx zZM1u%?el`m&Yh+XYIZlvXFwQK+AsB*D7HKQPV;6u!D%gMc=5;Cmy_im+|)uMSxy5R z>)Wbb{u!+T{O86iQ`1dk=eK2rRD$EK4I?k)j)J~ItA;{kmlA(5#U1A zDJ!${a`zQ9$)BO|JZ~Tp`89FvX|0>54C93FEE|1|o}wN)ML z)i=8;ZGuWGH4nW*CGjr34F@C0w3yv4YmHca`m}_ zj+ZImoWC!MdMmv4O>g!Zz1WL1;77f+tnBd7&~?l-ArcV(Zj4GA{H zk)vz#zXKF~#V|FyXv{m}LwtT>fYb2Zvg>1L&Zj|FEPPB`l=_#bE|ct0XpBCj^@7Je zcr87ICAj4W^l1{6^uw|p+pq%$xB5O<@njl&#&L?$Q%@an^JSdX`eUwl{Lqe%u^P#) z;K-WqMov8f!1=iAICa)?UM0}pc-@CUGev`>1T=v)dGR_lwOoi#OLJ(V|tFFzP9+Je3fu9UTvUF7B| z)P!G=#kxv+@gDqOv$y>a4$1CUU;i8V!3J&boLB;vf0R9;coe>5+`PW3?_H>2}T)5DZcr`jL+|1wwVj= zpvMSER)2MBU#CiMe3Jm$)#1qOJA%av{S~jXhWQU8W394@UlV(9ZD}cOb$-#FR#a}( zGl(&OEhU0kP*6d`KRxuKNWpsPbHgQE^Wjo+LFR3O_j7v{)qHGCnmC0y#lG}Jx^$zxqTIEpMHaeMRtrVx%{v^1TNlAD z9@L0VRe`B-O}U{H_^Z>t^{E%;4O5?tI29oy-UxsHB}HXYD-bK$;h#kgYeXNm**3_xoE4Lz!%mMHb#1=!%(8};?i(hvnN2sr)E*5YvUrh4^B18D z4+)Jc9~`0EsHhJ2nfz(BLH*_q#$T&N)yqA8u#APT%G4PWO)VNJr`x%Y7%Y~qoD;9B z4a?S6A&v0<-(~0-et}6w7IR&!ZMoAxm){ApuezX6ZeDwfGQaA5`jt|naKk&b#Slky z%hG$mW;$e@<;iw2pKlbf+0;3p=ioj5yO|C0VMpGD=b#xI93d4`M@JO&nxtwg5mHvN zk*{`cqXg>e#LIqcKvBG3QqmGsF-%=UU}m-1a3NW3z~Ic^>S?fuL>Xqj< z&@0pb?fm|)k`itdT;k{3Wi$;4`R02w;~O(j=f^fja-U+&fK~$0~fb z2RC$7K*>#&fLz*xTrsa|5x+}&|6d$y?6Uc*LT4Qfk{Z8*KylVSVzy`lEiqRY7>#*4 zpJ*5(G1NL$RNs#xN${g&$DJ58NeH|GY56A-TY36YUE-ov)+%n67hFoXrgZtcr_UYNlyV1Kj6J zz|q5H{~Kr8&PZ}#$_%l!NULQHr%sf@yxP*$#G)-6;#&{<`Sd~OMtFlXAlQD&wR;PZ zKdk?fFpmmtqIu%-EuF)al-2MUpX2*C7SnZdJ#zsZ<7OSj z!Ows}D-|^{N3$6LUMB*60S@OUBV2-9ou~S4Ct*7=G_TAh#N|Au1{^5SOlxiu3+PmQ zpe-CP8iOr_O>00}xYL4^JCJM6uBw%8wN`_>MY*F!^Q%e!scS1#im>S4$C>miUU_l# z$F0jh?$Sh~(()ZXUTb9M20pqYTIOr9G&G5#;`R>c&eh-W!dDyKsgnk_l6sDVXKy4% z9W#N;;sCTgUVi1yYF4XHi&>*Vy9^O&$YX`oz~koU1I|@Ldp9%DA3R-ElH-1k2V7jI zO^3U6qIN+EGzQK=Ten`lh?#ew+srpU9hY{o*Q0Ick^?u;LDn)Lk#r);C%?tf`=5QJ z7~sI#pZluXah6L}?>}q_yNvTCq>mWJPha~Qz1SFu!uy*s8^_8;_~MHq>Q+bf%f5oB z*56HTEtETfnX$7=4xW&|_>hDkqNSDuDsego*fGCTR|gKA5J}_Mz^#k^>SHBQ-vtCz zDUm!LjF2u!@i79P>#qr{j=Uhk+V>P2nRRWpYxIeXGO~O_NZ#(Te=uqN{(Tc_rTz zI3my1l3h%`&3eCX+MRNAN4xbYf7)XQ{MP2Ezl<*|j;%IR8k&`i}}Y~hV;V_dnzsz^7e z&QevBkl-z0wOoL(m0znsY^ID;X{)E{42u}qCGgAT196ysiA2}9jvarU$uFWy^WISoBtmho;>c^nYJwrWs&sl`R=soWN2jR!AhG~gw+wTu{Uk1`oUrY z^XiWCL>&SMicls`xgq$)Xf^J=xjGMhO$D5(aFmZk!#1*BjfQI_gCqJq(XYShkf2Sg29~MI`z^MytuvrN zMpx^k;oI`N=+Q3;HWLi_F3(Mi3^D11tT$SW9_}^qn-ZWD{*N|pe;`F;oqaEJR$k)r zi-CLCqG>D(1w5`h&JmS?wR8KC%=s_RxQBg@^$gfB2*m=!DJnS5WaVNTd6WFuu6tBG8ytHEBpl*~Y&}|b^ipsUFPPCiKiv*hPs|VknR!+B5>9O67y{SIf`z=m6 zDn?e`&uB(PU!P{y410QCJt#g>%c@brzFstNVw#pJI_gqK^#GXW`&yDrGn4K^rgKa{ z2q5waz}I_<9^13LV!ZhxfN_rSvKQBj``AVqBQ;aF@rB^;{w+_`UEO+uOYPzo9g^tn zq+n4f{J_b!1@B&88*b zelL)AWOhJdLzBU`{!QYl=tEIa@K< zQ|Xs&ub4#HVMYg5FcTzX>i5eg}z~B0@$VMPzbRk zcN8b*MrdhCdun1yDBWa(ls^?2%3`U5)mPF1{be=uVICR->N`8W6k#nvTcBO5;_HBK zRH^OOh%!0fMjt&G-(o#a)c6T$l4I&KlyAW6X=pp{!p>pYQKzb5Z>F-g$LxvRKz{S2~VxF6=cGe zzS7+C4yqhLs`_T7>Y}^ELQq{bgifik8J+c_-=3m?*C!J3;bxAp!a?4Z|CuuM+D|fIC*A$&2^o8yltROFoc386I#h&%v2;CX*cB*_h31IG%jZA-K)*_N*L+Gv z>J;zG%$UEGO+0jx)*L`h?pGqkdrl{8RydLlZgiIfe~}$u<)pi6njG%naF!vz(j2=o z>*f@2-ht$E!4{MZmW0vJM_|J9xL1&oUwyxp3&kZc`r8YaU}MOM75OrrWIo0Lw<;L| zO3_9gsm%R~=RN%w>t)SM0tqr~1J4g-tA}P!2G!oMP!Z{{a0V|1Jgu5Fc<&fDu$Tz} z=^;Tu_=?E7vQm$TN>@_muwMh!6!VkuPHQ}bIZaK+FnqGsGJf@i(l}!lwWuPFZ>VM$ z1)Nzol|tvN5;|9GwDRMjJ|stc@SXy*fa3T+e>Slpd`W(?CSbh(NL3db1$T3cyH9sw zgo<}=Ge{uQzId5gV3jEZheQ{9#d4@uR5V$U4PxuKM@Y~a(pQ}(p=xS)-74@FLngVl~P z_91)9fLpgwJ2Ia11{u$*T%2TF1fTfx;iD`zxi9BN0VFx%hgJ8azBHiWh(w$?uF0UO zOV^AlYvHKIV8A}@L$Gu|%>>5lnAE=w|HW4IKQRlkA=#b*rTwaiHHUtuLMu=r#2Y(G z44dI#cT!ONE!+VEW;@9(zV{W{QCrI`wXMzpv)K)wj5UlWAU4JrQ8B$oOp$OUCN^QZ z+_})XI%N!~ft`ws$hE<#6H4S)@id*Xo)PCbH1he)#k z5j76@_=H*Lx7(>SFWPYPx?zuAde^7;L#jmQ`30Ihu;wgvKm@M^GE~$t`lD1}XZRB- z%8;Xxoa24-^RNTaXDA0$3?ffT=$^#8?Ao}K{2W>qGyWqQdm5C4{g@4Orr+OV zyVK=OOeQ-lwRb~FpVQasVHhIww!xzTpFpRg?k6AIP9|y9j_kexL5taQM+XAbH|gR> zVgB~dSZyk0#kf%_6bhCjaM&DdGptd{Yxbog-g+H;xw~z>yz4hixt?X9>(P$`5yhV0 z{;1WHO;uEAEM^RvPv=z{`wASaNdLH;5UX@&()c*_@RytB=jP8A)hjd|U~^A%S#thc z%#Y(I$&MmBf?Huufm7d;hZ#Xt?LpM8&p^rc#c5cBRc>(bqZw}-AbO_*Wa`-w(fR)X z2>-)b)=2mDQW;}zbc8{qE&W$EBZD2-tt{LT>@K?<{wHol+_nxbh3P?1XGiK2#}1?8 z!oDKeNOF_S9r5VIN_SiMWnufK?7z7H?lw1-;eiVJO;_R#LT_B9R#!Xx68O_3=?Shn zyV3oTG!7fvqVpkqD^V#E3FyJ235tZbhz~}b>E>oRWthR4ejjtX^yH2tvd?)I*RYmw z>W;*I)cnm!kNII_U$w88xT+ilLzNoLENqNIDcNV&#cQ&pab5$quK^~}E>i_9OqoRJ z+U#yfW5<_y>wO=eeHS+6EgR>uI893V6P-^{;w7gbZ1@-YXHErv^r|8(Vk;7a)pF;p z^<)%O(IS-O_ts>w{J%^92PI)SW@Ec}N-M5=POh7f&43S>_m2Lg7Ef2V<+mZ^-os5X zhSdKHVF(d1)^T6aY8v(Dd#VT$Ks#8pI3ljQRsw9WeI_wMI(Q_0*|(iot8xb@!vZnO znvl$;+!%r?#-KIX2ko~DJg2Rsb@~CS>31Z^E`Gon$Hd4(Vmki_h>m7WyS!plU?*2- z)$9mz5uSQ$vO$V+wP8l<+Q4Od=LvYL!;=u_zhV|v8C>Kic`_;V|4t4M)TuH3vGHk&^=UADqHX{6eE?-DXZ%X6hyZs9D(-*y+6UR>9H? z@oblk_Wu=Q(P`*P%_;O$)?R95gP($N{wMfBYq+j!vBTP{!EJEdYGPGfYYyqJa<%ni zS8Zkn`U*;R;+XQH)nln|`<@v=V(ldcaMn*1Xm+TmagzK`JG}uV*kGF9FW0-lvy_kY4fH~^=-)TyH@AgZT~6?in`{pnw)=^ zq4qTc&+gl0?YR|!oma}hg|de=-Bn7qTYU3B>NAOJG#X zbd?+NR_9YhB;7v`wX~u^bkvkG~s`reQ$$sSs%Xooh=u+cz#LL4GbDp)6 zI$rG75~u9SD*78u1ekb0d3e@NEt;_MHak8*5as`;qhJr(L^Sg`0UDPCgEbRMLPI?i<+>T{l z*iL?`#N@7?49aT8Y4T`Ld*4`U!6eQru`BUyHEmQ2-(jWOa!U|b zoDZm5Y3*Wiby{in82*?1Fo6T*X5(=|VG^GYDg9Xt`+f#kkv+K}-D3s%7!~pn4780B z;~F?maB=gQE3i8%D|e4xKLRWRq}W`KQc z%ovo55VGGsw8Vq8J9^P$eXfytQVufIrgmq%JJ0W##aXiM=)@607U_+!BZ4RXf6{$U zf5L*l@K+#K-?&oKeT_M7Vd-Zldc@I#mFaYV+Sr}&jvAV=psdqp0QZYxzF7MynFqmI>jwSZf}xlj_2uN^*-Ld%V{ zKjdFMF>3|Sh=O_KTO8`Xn0klBL(R!mg_44|kcM})@-4*Day~~#@iS#rJJpvg-2c<= z%J75t8hMi^0_GiyT-Gn#7ufc=+v0GR5&!B`gAgT0172-#bnA;Xy!g8kZb=|y{_S-0 zH^vNnd+8IGikZ};mCQtai{X@L@WaaZ0k-)xen$b@#t^G*%@dp-e_G0sH~m|3UdE0Ft^khb=-%z2R`@f_K4lvEx4 ze|yS=bRRT~*BOt`a?OyEXg;&tWf;GZgVpY!#s~1d2{zTm^=Roy&q)F@xz1;$Hkx}V zH53p?oTo^fCpAu+xBYa7`8bG7(Y9)6BC*M`LwmZb>GVzYntzu3!z1-a zlTeTv6@%gheXS+F?tbsof8L!VCrLf?pGS~pLx9$rVzhY_Azv@{8)*wv+k>7(_xy(BOsJ@8EBv!@!&u!0< z@HGl%AQ6)%^^bJB`!V2c7QTO1%N`U!5gnyC4aIVS!=&QADg@uI0`YS&r=DzZR8 z8zN%joPfJaLFBAuY?4;H#lE?@Hpegb=peO)Xf_FCH}`Im+$B@@EBSesvkPyu`iTiX ziR4J}=-9aB$$=`sklp=y=MHhqEQJT!&-Uu7Gtmi;tiOw_qcg2}r;3J*t^##SypC${ zRbIW_(0}V4@+r{5vEPAm7sN`Df~XAb{-g5eJ?Bf1_@1X})hU%~*zvz+`p<8;!M+4+ zjI*9dkl{FgKdM@D-bk{OIM6fdWUQ^YSi>@k`yYXt zFreJL=rU+_6u?AM6d@xKhR+(CsJDKjs2<&mR~q}@6`|@QXk~{#>Tr0y_CbSy*F_Ok ze3i^%d(J<(@lq37cQn7HK*!vmF#Iyjco2%;6mF7VplM-Ne*T<7e83oTO6z$Tr)giG z8lY*V!_Udq>h3a-$%^xHumXjGoJ9k@u6$nxQUFMK)%IWjzVvRRGY@Vc#qtABmyPyT zvl-Ja&MVCwNcN8rBfr)aLXj<1kg=NY^h+o@Iv;Ae^H98G#Ydhe5lQEcj}CH~8uUqi z{EjraR^W<8!0y({7fg+bO?{b0>i^!03MLhFCUjSy$MniZ{$1A>?ekqWokW*X6O_6bUqOye}>a_M(hj$mz%9noVhGS+=zteASZfN-FC#?mv6O z9t9;KyB6|Fxn2pmras!-`^dnV!(RQ(!&sqz_Rc?2Ny2~^&vR#8G~9}=%%zxK2u!qH z?GEmoVqo?8w%1&=EyMzrTjd;yM4;uf+e9o*fGJc0=D4Y^WfvLLD*x`le{Hb|2L*ZE z<+Z!N|JUaBla_wR3+W9vYM!u{(Df7{PWTx?CMSb+_dI+y0}O^H(~EbT@BD!v;OC; zP0$9WKlr7X$#xIFss@y$90e@h0Z37zZY-*W#*w$i@)C4BtJEP|ZY5T?gb>idz1zXR-;qak7iAayE$un(Fp~3k`p2Lao}tY^r^lI- z{9Ih>7TM@_^2PmRy}da67FvDh)hH`^tO=Xo_{?S{S?ij{Uxd(vhm9ynfi+~orgBF! zP;o|EaXK$WUPuOmU9|}GJ9oXVn zmoCz~BE3qNt{|NRDFFciQL0Fj-bH$^A%sv=s`MU80@5LnAT^Qz{=S}f@B5wm?m6!r z?)fj}``z7{&&=%X?CfltS_O~HX8)4MCNOiBJbc35YsBer51E$a5{#2J=DlE^AP%zV zz3|Yq2W>J^SLHDE*um-ZXa9d~={Mu~ga`2bSMG#7UGtiKS~$`Fn|NuR8~rrWG285i z_+=t*1F-S1z%-;Cq+aEnE_Oomuub}S@Sp=emKj$r zeh8+IZ>_y~+B>aB9EL1fl5G7fql~2>pZMX_-nKJ#n0o62lcFd++Qdpfm$-= zPK&R!Hb-Bt7h*@pcVI+2z>EH4~IPnxd?gqr?cT>kp-InLlWhxPz383QOF$2zmjnHec!`4V3;AdodB1&;ql_gUAGO~# zdpMDqntGA%&>5ASk`lfyzsSFF;CyzwQD;ab);A}n_SWASp3=8)#Z&St^YcgJVT)Y4 z`27?H76{Hh97QR~9KIj^@l80;2_?ns%|PYA`v!7z;2n}2Q@Z-1&t_iXC=_R4+RpCo z?mZSUuC@2|2)vVOSvbn+LEj%4?cbb$zr;u&H1O#SiTdzhM7~8A3jcbLyvQ9ueRcZ5 z8ASnn9aq@LvJl+qa~g|`uUf^FzQx{tLm;Q%>v|ys*6ew zy|!S&was?)F)L7xF(AQ!#V+8_h3zl=^C|Jc6EYSUX@)8glamk;;S zpvRwK38`$Jn?`BQ)NrGSWb|+AoIjZM{u&2OuLR0ZnO$__dA?PF4fq$Hnw|-iUkoYXdtab}qJ;H8a-71@e!em> zqTt$nH72oCxTy%rue;#7yGywIoPu-}X<=O=p+xN~$0dWYi6AP7WY;9cy!WW3#H%hK zE*_4Wz@uqy3}cmETe0AkJ~|lxOK1P<7io`kj~`87xS`YyE#C#sDnd{*7?O$(YXkX-pgSp*Qoa`wcfhm+Il zC|}BxdadpzYEmE9)(5!?N4QR{9$cV?BS?uee>?;I`!Im8(M7;L17EEiiYU>(b|jkp0+m0#u$!nL=zUpPAn9M&^I%!P7Lp*Gn{`p@{@ zJwqRLYVY|#CiwCeowoDy~2^r@r_jYW$K;e7wGKKv4bzyk8X+g71USb6;A z{(}3YO^u2YF86gwr_IKL245`xT2V3ToxAO!XHBH5Bkp-!bMTEnQsMw+?MQ`@>c7`D zCrzNNDuV5EDl}`N)T1r()1z|)NTc>*$6V`e5Jf~xII8tgRmT4W_J0-K|BwH_KK6u~ z3fMcL{tW%KH~hCo{TsBO?*6i|A#bsNzo}o&Y+4u)?nomoSNyB?E%0Yn)6QYlLKn`{ z^`86HoIBwU0`;|c9H8$#k^WC_e5XAd@>7le_(^NX>9ZZ(CFzQ4xh(A(YHaPd^TS^b zY>vv{$Ax|QH+xZ1jci#K`}$sadVBhN4t(F&-I-6FCcS^^4CSj+=WhT0W1!?2Quumd z49Oq={a1ed5PFW%4Nu8${_qdl|0@juuNAVsia|tO^!~T;{l)hIvV2rdF?&(W4U7CM zfq&S*pzj%Ace_-i+Ig~i>gByZV$OXz%KKetijq$^{)JzE*xaeW^VEgZZmjbbceUu` z|D6k!3IK-$sO)?H2!TIt@!x(W1#o(Ej_WTF_j`DM*GwQg%c~gQ@cY&m{#^=w_t^jX z@jk#41~!#H;@AI>t3PXom+Ev7O8p$2(BDA%^(lGW?FSeCtc(Af$xp&bdDXg_G~VpW zsSBI@9aQQ`NPA7Ynl8`n%T#V?oc@nFTOW6BnSBxPoHNY0o&R$^!e2a1>qbFNF0$gj zW4lxBO?3vRl6Kzg|K`ZQ%j`d;=u;yZFFjLMql7ey(k(3vq12fn|A}F(v25_q-}wu& zkb2HU9w(uID!%z^h-L8qX_rm{ZlE`Hmpm>zdrAd0WJ+4k8u?Q94{hCFY%^I75YZhw zCE)tg|Cq#xU%ICA!~XJL?DG%Ee{u{Mje)G2e1F0I{8=x&Z$DLU(Z`8}hJ;ReR%_(# za}L)fa0UGZeEzJ{z`ApZ=)27LyYjfYm^*)?r9n-=pdnP_n*Xe)Kcne)pm^K|Og>3W z;hDei?C)Ot{l^b!0GGYuIQ$;`$5>OYz5)!=mD@V!|6}!kbzh9^_9Bi$lkoaK$7b*} zAh!(nKVJMBa|HM*FzIQg?wj@`rl9=G#lU!qzSKG`WqZ2`33B$aPGe` z(KM(6@+AhGQ~nzqy+R5Ad+Mse-x2dTK)%4kxdMNKqYS^inbT~=znA?JO8hDZAYbTQ zRm=Z`qo}lVV174~9-7I_Wy)8dWRy=a*t}%MH+p<`2EbLQGj{8)@^ z6u_Avrm&_V{<3N9DzEP9E+?cBtXmB+WcLrIaPxr@MW3~pjJsHy6mNy8Cn#1HyIE=c z$e*Cc4mTWK^DO|z<>?10DXpdVGsMHTlN>c53uHu_Nnl8I>WGHuobGVJIg&pb(#?PH zPE9>6k9@Ksac?>~(NTI{*M_RJIQ<57GuOTnq82RIKEu_+Qeke1=H|gFWsMy%96%gu zqXj$t%+bxgriB-Y<9xH1H_%Bt&mK|M_mGZCTlqHVUv!gPJ4JZl&{dg)K_{JoGwZ5ZtUq!}RIagp|F0Z7pSNQogJE;u#B zQ|3RBri?#B@L5Z0i@tU7R8v|RwRwU?S?0pNK43UJky zeSqQ^U}rR~O}Le;bUV3zd+V0(|1=(dG>AV?Soeg4y?GNzkKjN7XxCMFgwj3KO&Hhn zps2JrxKV$%rvg)zoliIMYR37>cbQ9WX;tzC=VWarOWyQ?VS4e3lY9s) z;i(38wYHlOskgC{_afiq+LYxUJ!@p5Ak?zvPOx5#ebzobXQZ-n%R!+3Ej>BGt6Bcj z>hDy~NlHCNOuJ)@W`mekow47}6--mt`$hWiou%`Z!@ACJ!DiKqPjiYxl)mjl31eb= zah6vVGvU*l?BbA)LMDm55+=$L|LyZX0{i#H)4z@044g)O5kqIjI)3l{TqJZRV+;d3 z3Sz34LTcvVU`=hwlGEzXG4H6Gi`=!2c7Lu&qG!?HEssPuC5%cl?h(PdDg6=;F5W&B z2&SxG4@PaxX9~QtklYE5a%gscY<~o9yiDupTkZ*1^vA6hxH$OZ-h|l-YYeB771G5w zT(A$CodgMQApH*N_?4dBJN=TZ{TD5Se1EH45$k}6FIWokU(DF0F929- zHKyJCVRpQX34hi4YG8kq|FrgxK!iJ57z1G8wUb;t@Brncwd2jyS5Qu9w(c~u0tcbI zoK^1v+e)m@2QIs+drN#*v`CUB*c=0#aFhOmrbs9FX&il0aWVi*GK+4& zr=m>t6w`aU{&E|f9s1>iC?tD<|6{Spc)`9V3mdX@HM%_{JL}kLsLaf!HHJjVq~txd z_0?FyS*<;@Zq)nOZQ@fo!F;#i}HhcY#-cd7~)3h^=bqUC-10yk+or$20SIq_ZmECsP>wsVx?U9BNIX9pY0p0bpd zO^QhpehI=b+aSFsXw=-@;3~X_?7Orlxt?Dy#AkH_WP#E}U&!D|Fo+jIAk9Ks02l2G zY5af@y2c<;pjRGmiXM^2J>QPKAS7 z&H=rHTm7vH+Qt08g(ifBN9){~Dv!czK}+oXybL~L9`Rg}P7>eiqx7-e>#GGo3GAu7 zjJow&3n|k}JnIE~H-uIfhr%0jPwZhm7o)n892vbLlet{ZF`wD4C z$Ie;yp+=B<%YM>f6r_S@ksG#u2n6YRvtBWlOxn@7`OZ}HPBB(`48DxXdh;gnN~&Cy z=h^WIMujoH=#Yr*Qb%KB7eORGG6wNdVpUQB6}#DAJ@@E*ZEu#qH+y<)G2Z5Dlz|&RdT=hmVb26_ zV}Y1dYVw41SMyebm)e?mCh6QnBU(XIt??WjoWqe@Tf!1DOZc(|av2NOqSgAc9p2e~ zbb>BUz8(b99Gj5Jvv+!H_TXD4`)w*1V{1BrdaOQ?LMMf+j(xt4o(vuu8d4pDpvs<} z#zYhB54WTT92P~&5hn7M^{Mzz07 zIlUCGOVv#BWBrOPM^k-H;gFL?J_2Q#A35TMb5m}{DbYIGtpTg5T0gAI6N)sCS8kc}e5q@j_E+(p=%UZMp^fZx$qg^!>Zua+JwLK2l8~7_urqJinON4~ zPqSm_zS){Kne`F4`Wcv452(H?cQJD3@h{h{^i@DMf7aNemt8!rTv@b7z3q~*KUS3G z=q|0#76R{%-3YIz$F7!xc%xKTv$gpg>iRgB^XB}t!_@%Zx_ES4vgW^c*6?7AS-Rk9utkh%P z(+(^1`j*)vJUd@Pw)Jsm)Txazl3EFU5QQw812bp>6JShoC2BpUJVJ*Vmv?Pb>s~6( zXQroX=p}Aq9ucWQJ53_-XY|7}`l^>9Jak@(NyWBH_O%s@ZkJAzddPobl$7y3e%1la zJG(Eu{PHXOVJ|2a-dC~ut~xuL7uWv75h3aU!SZNqE;vNXAzoFuOz292qRjc{V?_;ADN@}Z z{Zo{(V#SiURS#T?%gfmp6YYH+ZKo8bJxxv-hr`)tGT}vf@-8LDq?~@d{OvVz#{vb0 z(4lo_9j|_avl(8KR6B^CkUpT4@2h~~Z$y8GjWOf!g6^!lGR6CiS0-9l@#D8y+<8PY z>SohwszP^d>25N5W^4@(KB9ZyQDc?y9YMZla{E>_qkGBQC)~Dm{mia#_sFjFseAh=?tH{Ux}jwX}0AoP>nL zthY=??t_(P&(GD5(k9KNy%_^gK)L#c6n{b}{XPiRsBIpxE%FBs=ecK9S6HzvCx)5Y zy-c7Mc-SA|U7=qM*TWhfceQcA{R5=KZT};O!l4%#rDO#sOIc5Q)qcfj@A0SDk1yEp<>6K&-H5gha^V7LZyJsi6E?A?j~4uI z$3Q#XH;>t$K3RUqFs;F-E$>VQml=a<`5sG;S<*Ll=jA0Po66&usom6j%S7LzK<%2b z4O0oBvqnNa7h&*bJ76^s9F^{C)|n*Xwwxgz=Lz}9Ye5=2z6CZiG-~nnp1KJ^h(PgGe^?7Vygu_BUYmLg970_^7;!9;a5jYG|Z| zO$VVC-gfaZ!@goaYMF)d3n-vmncSKr5}q4=bHn*xcZJ2$EVB@&^R-F^*8EgHRITx3 z@zmb$J$O`G%)8HBXPctLhC5wTaTwN7rhK=2@0Ea%+#;~m6B6Uw#p0G0V+vwh9FJ|s zz_Zfhe)_lqHe+^ePlkPQJolk6A7{A=%I&)wL6ZI36)LX-mgGx@oDj9NA?}yhW*djPW15)sO%-%=p;&;-o}!Y1;O21S+KJ?$;r<7$B-+m=IYdB3A~>1xm5N`< z$v0bg-%^MrASVn(&g`YIXjtJts_~gAR8g85pBrPU1?;t{5?Pe4$gsHfJm|cs zF`Z`+DrsR}yh~LT|b6+4k$!0*{ z`KyJFJD4EU#YJwuOa&DBnp>zOK!5#1Xs!xMjDypxi;sN1c8#K`m(5G3vw)WhX1*AP z;*-hT274)FsJIvfaCNi$^eOMK+%qYk*s-u2F36?pCWalKHe8r$7qa8`$NyTmcK-iS^f{1pn3 zc{)_@qLCcq>&D%e%wk|?Y>D*W3{VsFs-uDIm>hifN4OnAe@z_6!> zHeh(M*DP!d+?rtQZH~?Hu^cy_TA#2mC?SQHDAaG#o^e|~+ReIi^d;E!L{sax8Rkc0 z%V-B?e62&GN0fGIjPFYnt6PL;R6SZ(*!v|oPRzS`IOL3ETS>;9tdCl`N0r5-aN^VJ zxP3tY(}6D=leIi)=I(-C+3(1gO6Ofy);r>yTVpoE-6pr4mZtXxU$specgaZJdq&kj zk_s`Hk^Tj$g=mRMY;8}f7FBqbr1@Y=PG3I}K zCxrAwVys9-DhNAM&~cN2n*}EPs@q5y=Li(+M5>w?u(mf1hjp1>gjdYg$8zw+I4Am> zjABDjT2*j%-N=3JK}cAWNTO{Dcv&1E*InOq*ck|`z9lk>Bzo;O-Ckq_&ZS&+i|%D# zth=u7vAJBOqKcCdSWJAF6k}QtZq~Tz&|U`Tv`?xPwxzpGaFUWV--}9Ff`@9EyvXF)^krI7wz$PujIeDRs%3 zsiMa6EbGM(N$z1_{^p{BZ?YZvwtLPD?Y&2N+Mv?{!b%gC-Hlm6(q*ET*IwtCB_5d^ zc=7?uuurcP=fMh#q7S@A4T@QZUm`PF@QpFPwzeFRQBhZ8eaHRR+6_}}o&<^=JT-q} zorp}O@mrHS(31Z_=CWBrY<*9a}Kv9Y|LoVlW;Y)yZY zo10Pz%VDUz7~kP*X=goc%gjq6SweeigHVq2CXJJO`qt$+V@NZRMA?>Lk!3xh3q0L< zAk3&R>$*R4jF+b+860FSQ1eto4lxMbc2>2bB|?#c%rKdqH&@*#DDtXenWK9dAUy=M zg-#G^578lkY`Tk~`S9{F($-XJcOdt&kO>>Ba#_UxL@lb>2w2N|C*p}t=K zBzhvi8dN`MX&a3#?}f$?^8v_Iki0A~g+&8q7*Cqn)H5sa*ZbJ1k;ID)tV?=@IV@O? z_2B)nRC#((_HRa=M)K4Yn)TfL!_bDcTAq>;xu1;_DQfJ<{-ScJcU>TP+-WIg-mCcS zp{GDHhUIG%%dLi6Ye5oYKUnOXoqKuoEiaA{hrxR`I*AF$A>~{fV>7>4Lgzng0T>Y- zjf_Cu+m6$#dXQ!A9*7maQiO^%qQkEx_h{D0%+v6W|=^n{>Y@*h#FX zkW(`qIHyZ!85$a559>lxx>>fwa?r`+Y&bBNMdB8xH6d|!qKnyl*FAJ#XbpGcU0-5q zQ^nldFXLGf)3{Omy$>Z!tBC8Vpfc^51F^F@fUJP2i zqShj1iAjD6#wi8+kb^SGB~PcNy1FMRNDAaH7)coM3paj)aiLqt?nE%g-Ctc?7D5#N znmBsXV91^^)57A|d5g7o8EOR^o3(jj9h}1=v~Jp|A=3&dQh21=`eV|chEC-;k~7#$ zJYaO5P|L4R%7eLVHJy0Q?pG4=H$5kdosiwIM%}I^z@V}wgrSC%%pp7XyP6c_am8mN z$cu!_bF@%S*^sisJ3O><-7FQ(E8dPP(|uh-y4e5nVBMy$0gb| z1^2&Z2*?|7lv}bLg=&q3`vCje@p4ZD&Y=f-dwYwSKvv&v($Z;7?>OFx#F{McwL*6I zkQ8s8UiJz)<@+t_N#~stnHwL4+rpp>PXSHT=;$-LmZ$RIkscdnd|}ay2EOjpnyEqT zHM|;?+lql}sc!wu^59gEtxnPIJO7#VE&GmP2{+X2xha`VzyW)}=LsY)fd|bLm`v<5 z;^N{QfTeUs+v;kelsm)}O$wM55ApPS6}W~{+I@qHR7((jT|zNfyy`hv$MQfjp+X=s z{>qgrJ{AtF7?>`oXCk!>MQE%()H3hn+18ru1P4$)VcPAKynFEk@_3q*H}lokg^g8x zYPRg>-Bj>2fXBz$HajxKi3=87@;Hr=s+kMm(O6qSEk%taL%p?M(Sv*DIRf8a(uNc8 zmjege$2AMhjm?h^mVD%!t^h-@oe`;p6WSK!0A^i#US6A;V*p^0&7e88n>in0b!zY z8;JgP=oG4gFz*=@_bGl`r!_33HDS^x3@i;ErA4SUeYD$J`4z&mEYSlNrDcd1;sgZn zT;(ewK=S9B;^o2QEvP3;5Qrnhru!175wH=&jikb|Ms5@ILAC4&Z8UrIM{BLtnJyc$ z>8Br_IB_cQ_Ny2v75x!^-5zwYU}==$)wWwPhDEy(lGo!6t-$6YKgD zYDenoc-Ak(KKjbCh`Z3IxSI{IHeMb#VHVr*eL?ot(CDasYI5@TEWenudhqmQEry09 zp%Ei4No6Ia;qTv{k9;o5T2+VS{H*kHDh8#%Y{y+;b!%l1B6C5Oh33vub`^;wG!h=~ zy@QP-oRs@GTOi|V#iZ#`a-TtS{6V=}4@;{=Ux>f!Crn?mzo2kTw)JI@wa23-BY$&? zQO+hY(l-CXG)+Wx17WA{5|l$8ckq7Q&5y6PKaa*OiJmuI^PQre{!H*C+F^l~!}+yH zda>cXb>v6_kI~`i3-;B-7rsE1FbC`u)B!j$>|bsMzsG0UEKN>ME>X5BHw>q8G+i!( zUz5j4UT}{tD`&INOzc?V69}CY*grg@W4T3&X>ew;fuo(Ta3jz|tLurb@mLelx6ulW_LS^I4i+YdYUoIjC?00Op%r2FcoV{5l^VW~hpj2}N#TAU~ESKld3Oe0pNB#REYV52=OX;P+K zOsAk}kv+E1LW?+0-GY4MPGi~l3zdWW2_|)2dZ9LG&kOR6=0?hXNk>UYofcr92{T-1 z=_qI*pk315RF1`NcQ?Bj!IGA0mRXLRk|M*+3)ZK}N0yq4_mSnLCSJ_=DpnvhJjjA} zy6vnlXAr7+4qK$F4+6wz;q5|OIQ3eZOS5Wc6oWc&ZJ@^ke2S3W@Y+bh2-FFA*v>k% z|As;~nbBu8dl$G+F(0UqkOyFu4`7Apk|MnALc3aK{lY?G{i)017%U~AoVUySoUcUv zbGM!Kvbon6?Sy?%;6)l`h&n^bQ}Z8GHgu-9GNQ{i3Iq#GJ8T7ZZjK4rt?bZ{77y_5KeM-T#?1{D-0ZdS9|$OKSFv>+-nvih6O;=)uzK-ZjFI z;>YnK)7NQ>3Jl+QkDg|*$#5hZgD$1nG-)l1_Jl~5rH$}f zALKh5@%_`%Y)wTrGnK^|I+|g#@~OP5=YV;8{WS4~^&t1V$Ae2t)~ya6FCH1B?im!& zc2{T#C){LmQFH{qK3o{c40tp z)noLX+DWa9kJ^Vk7-sZctv-<6nFn!dD(2^_b=J{)Srby0C&e?Z(A41J<1>|$Thc82 zR&q4SBUbL`wmM-6Sl)xy4dwD&Mq{C_{t^{(Hc}3(>VCP4o{WNi7BY3*2r~hmAE~@* zzQsk}20JD)yx)qmBY27Lobsx)YOv#SaymviVX_#dT%VaNjL3M;J+qw0u;n46S{|al zF+XsJ>L_0)jdyGvPlRNIs-M<)pp%5m7$n^|ea*KZ)=rfl#xnM>i;u-z-0H0AEg3<< zISLojk7IriR{hN$=C*v^oq5UbkA5Nv_QW4s3ygk5#AdVltUnQCQe_paJT=qkU300L zz1srnn2?3h_UpX{|&eUIxcteXut<9G!;kSXH;Xlb8kH&bne>8PQ*VX<$9 zx06F#l}uVrbVPYB#yomoliGD!j%o8gZ4gS|L0er6r@0YE=3L=Tfv_KsS%M zqhEr0oClLIxuOgbE^wBkCa-F+pd1b*d|u4LYUCgk+$F6yxyT3#k!H;Z9}{hEle0`!mRDq(Wx)UViJ8i z>}fqef+pS`+MDKI;^QGcUWII2a1epW5Njn?9K?2PI@j;amQ%S!sL)1D-1n02;+uY3 z%y$~j@|;Q1;|_!Li@O=n3Jw3N3e7r~+01?H^osljU@|!}N{lKzbvzZu5@U$4S$}m} zelr#3MZ^c9HY8b*H1$&E;4)JOaqb*>Qm*xe$bCqCP82qJJ9ZPCpy_HeS;VE6AM0=K^6ia zHp=`1Ncd{U9{t+X-n#7ZQcn`mq6^#D08RE2?yO4MVnHuP$439gJ zwDLGDl`$k$i}<`rstALt0AJ>IM`z~~3F z-J{eqgBbmQV&9qF_e?K;JoVXpLov)W`%>s=4&Q=*0(&aZA(W*L61h%}W(}OKcbmD_ zg0Je=PzQ^xheH;4C}LUH8JfPwB|nem}F{=Tjo3nY6HI zhGeNH9V4BmMV@VS<2al3EsQ`%5_YLYutOz>x1&JRMrv&2u?Hq>+rGj#G8^|OBW;|2 zP-(jQXL3%(o3h9&=gk>KGMs8l_8IC*4$ut)C64ZUKe_m{=@X7dbd{Pn2+(aT9WyM< zW9~cov$R5_JPz~7#)$s7Asi%N-mu8B5iNHt?zcOn>~KWb^XE?82Wkn9)Pbg7wyGtU z>0QyHH}mV7Q>o|ce6iGhU5IZxZ~#k&_6j=SCaD%l+DLumm=#?71X%+c zn{GM#pCylNAnX>P`41Z^<1(W696G;!=-#WYoT1mLac9H&ToBCKcwU?D&0iCYg1d|j zaZ$mX9UD`%H-Z~!Xc)}BW$B34Pf}mTulR|xSH!N{q(xf6T>E7&i2E(u1!SwVwfG-N zitLN(X!q`9M;IC}DmijYCo0RFIJ+l;b#Pc7G8HXs{A+eFuoO^lTON;5-Q;MRJ0Fo- zb{~4t%-2{_afDY5aVOG}X0<`XIM9KI|)}+Ex-C4S-On!7^+3y{ph(=wBdv-Kj7*smAx=n z^|X~x`}F97%jV7V1vG*JYhSY_wnR;g9$}gxj#-P4chclvDiU0fq;SRDOUlk4q}Ue? z*|2&AT6wI>QMqSHuH+u=yPuAh*?;{0J7GwfX-_}4wnvkJLRp&k%ZK&nvPlA#o~}Q< zX8n?6h3yKO*}u(HSu6X@_`m*^VwbrUDxDrUwIbVt%}0sH|Cds=8Z|B9%mNM{Sb8Ts{`qsDuq{5v`l?sj4|uG=X(;?Wt=oEgw$B?w zaAYcluWibT^Q0~3rAm>9V@4L9DjaonX*SEeZ@d#;V=%G3MTD>JX~5WIhtJ5{JD{T11y|I%KrHrTu`uxD`?|-she;S;+yH+_ zFIn@Wc56vE{!UnTIBI}sMw)Xjl#Y{Sq;>FMKkUXEm-6~10gkubaJxEf4dVbNXi;mb zHTwZsohGOKXX8AvcuMowHlySZK1*+}YhpiocuGu^D1IJ4)oZ`{-GhGK-hBO$ak_-E zPx~!W!&@G2v6JpA?`w01mQP!=qYFE-AMOcjoexhK*Yzv3B#xwqB6||t{o@sj?@4_D zzcX;aX~%vB@kM43qI}>9wFwt2LIC0cRe)SSbB zv5<`?9ozpN8i!1hB*=4)DJQ-LgW)pU8?G+4gA zbmEM_25sOZib%Ad?yK^atqxt4Qg>RxYCO9I zDYs}!EuVldd?p!br3v%Q2#2z39rp>p-b@Z7R%|w=9WQGKXt*3ckjL%K_*c0P)|>yd zTZmUNwtla*^J+~v+BDTBh&&@Jp$PTntBE?Q=Fu zrJVP2Ttl0ZAC_o2S@vZ1i?cb(S$+1=&Xfch!)I8drPF~tj5SHsu?SI&96`VMYg_Yo z;%Y4iAINsc`&M_ftTniM)ymSXAaiK6tnfGxrd;|e zfjL}CvV56^iProFUPVv zu7s+!HW%4X`(*CMOi$uS>5Hj7*`JRav0)J9-nK@P#?UK7K9UtU__p#Xe3iWhx4pP! zi}T^o)Z*UIM9+I^ZTcvNsY{8%%VHqPYo8B0BG;S8vtDi&Mbu=~+}VgW_P{6X2agmR z@HAOU#HdpH&DINoEgxtS8R-Sf%NE8ZpxyN+4QUNtI>U$7Eof42RaC%fIY2A~cEhOi zvBVA!KN1Rt?XW@Sj>Kk7pXRdsCB5$^Yq!{s6KEb$C0(D``^G%N$9O1-orFrs98ctw zo0>TVg7PR1*Abifuvu@;BW%`j`i##41>g?x1AEQNoO~I(;ZyW%^=CX3ec5< zu7kJF8Mal%AX{z^O*zTunmPCL?XtWjv9(=8Z@HUo`yA6|;|Fw1G&7#Et{>5{d_LoD zfVe|{^{qNh;r_61GEaWH84{$Bh&JpP?==6L^sEl z;rY12)g);S1!Np2vlgCYuhK6GEW2BTy z>_@D!S;ZBNQ&Pz`^)3rf-;78t6HwmOBf~vOD0zM8?D#m65NWY7Lors>N=^T4#!^Dy zC-moFrC__s(pwwvX8xRro<#a6g*aGi$nt`oWoiLST6DB}mrUlBRO_FII|FD}S>i^G z15i1od7L1AUvBd@%{=chRjky{jX*DKKs7p99mv!eOfL_02mt}WvMv$_$mb6ly6)0h#3ZF#OI$N? z&NqS)Ow|TrTpusjbV8PDzM)w)2=xdVz7rL#ox9UA)d<3+iGkE8z_Xd2vfC;?9(XX) z+{Z@shw}q@k{b@{5jP26O84~AzNI2{0^Cz>U}3kLu1Yvvmb_j>I6v|pd^fVd=_W|4 z+*zN~lfgH8(s#kiBoLxWTWI|W!(X%a)#rgCN0Q>tk=hK-xv?Yd-8A=&a*19W2{m|? z8+xuSH=2hfMQVl8sJO~`y;eQa(4>ti;82i3_(9XhoGc;p^)SD4&jl{RMeH&pS^B)t zM1-@I)%J2FS%mfHbM-#IB2pjv2DO!?3)T4+6ItJPnG9989B%h`01?_SEUNG|K;>&+wO}O%!~j!*&NIV0JCT zbSQtRHtLoPMj*6GFd`isb+pCFh*wz#7T#_^%B#I4CcfS{s_bjk#H+dfZkY`ssANxK z%0f+=S>g(lX3nPM8Mt2w8?ui=6{oJnl{LxLortk3xULQEuWm_IUT}!mA41e@!ELEv zAJf*^?>#NO7I@TiL^SW>+d2j+mP*sXSlm{Q-m|_@Z!7b+rdW!tH)gVPeTl)Mr#@a}A$;nHZ8&iIyJ?`;})+O&CuvuRaRFN8`w?JI$8uIv2&?*{z zn&K|PX{CR6J3_!jf2-3hu0Un+vua)L$mI;H#esz4@`y6cI8JQ7aI7UxF6K%~vtFY| z^N+(Fu=Z(J2$LV8`BADo?Ge-?0Nitt6G#Nl4KK|bjf*ioifCeS7V8vJ&t27XSrmRf zlgi=R;gK|-t%7ot6Mw>T6q2a7kdl6nUNj{8Gqb=zpnIN2FWrO5_c%R}hYAB%HX*R0 z#giFdk)M^2xK}23I9DD8iQ!eOj>7w)8MDgQ@ftKgW^)8z;wbq?c9ox%EdG%{f^hbw*gkf48#?$83 z_vk|gwQoYgy^RuQC71r7fW-fLBKj^TaG2XY+n9ULfHY3PtTDM+q;D=-j#2bz@(p8( z3Dw6uk+>N_;aB|EMEOim>RB%iV6(#sJo(dc_LKdCOXgyHC>O*04;3q#MqWeChq%0? z;Y%E5hAqGj68{y*fg>kT(&|=f=sKw;4?Z$H_ot&gE~hgpugnydTV`69YffWBq#Dst zle@#0(cx__52l}eI^uc!WT)9}Pp#o;puzP7q8G=a|9m*71`H;xsNgm^oL>HzKF=0q z<6q4{*Tg)+Z5uh`NZnR(kIEw=d^c7OSi@bShC02Q>7SBy8LGGRnG3ER3PNRijl7;z zGqeBMq3P~zeHcR0nNTNKn{V~u+b79T3J4kG!N!Q>4T7G58O0!GaxB(<4J4R zK$&8iL90LY9NmYe%1xl%!H1ZK_%G7dAdw-;FsHr>Q_9)Wn#S19XVwX)%;=N_rQCLt zBjDOu5*hqv^~u2~j&A`+V@7t-eI)h50rE6lbq)O5NUf<`G6TnV#?sRu8$N@{>XrPb z@e_p(g32L{FF$m)DtDEci+2u%Qx84NBQL*$%1V7YO)S9%l^fh#@ zk+jvvwOfO`sbfa9D-S5TxS6YQc>%tkc3lvfJ@rm2TFA|!iUUR)dgP)piJ<7(-8{Yc z+eZu|cNk?nr9nwEgC1`YUOhN|7*7=x${B%a=o}bT4=on(PRqr~g2|`)=DBzNb zH3QNa7Iy_IWpdkCktL~L_lrB(EX;Yv886XQHZFUC^`J()o@2PfZT}GNf!lz4 zEKM08{~u-F{g#CK|9u)BGf#OOX=Q4fqjHeBa#ALlxiWL2Qd%w?xhK*xQ!{gKF?TN9 zxWF>^!oBxa6jMM11Rg%$>w5lx^Rr(5fZX@}dcW5D#l^Jakea&r7MGf;la5?3gm2-< z-LKPKe|cgKB8VZp-qEd4mAuuzhF29+U4>aubjO(I){d57KkLaeF()!>ERdX{-z_^h zQP2I^W}djls=L^bHBtN6xCI^E2_?@5j6xWP0`SL(Fsh5dtn2S5V?u)uS!M|t;q1WF zC*Xq@Ny_>W`A8w7%zJTPGak~1!#3`}$Lor~6zK=CaD!N@VrJ#As|eUwb=_|sRyrP9 zp{V6GG^KSi^l`4!AsjTeQ16~WxA~qkbA88PWKDd-d_PV<|0t%29LzA?Q|2zyx<@tiNsTvU? zher!!HoNW3H7aO&LilbyY3lre1b?08d}uQJ+Vrbk=GvHFXY`A_=}}`1w>u`}raD~c zKUD2UUx}s2>Th5O#Z9FUI#EGFhKav#{f>mlKw!6c8Z+q*^&dVi^Z(G5KTWP4dQ?7;gQjN7c68V7`Y((Q1MENkmOo+y6=3dq6*fO^!OEQ$|dU@%pqQydOHv_w~AJGdZKMPJOd&`?v1b zSE4AF6GcN1+Ppb!`^vs9ca=!ku_#yj?Y*qC*aA6k|5l&H0gnXW+0O?coj>~q3KY!t zu~DYR>#H5*F3lXQ_UnZ`G4E|={_3dcsUxM)Ng)O9rrZF8hF`C-%TadF&JW@iOCdx@ zR!j7opM`Ey#gCGeO$p|IET;$6etPYjHa&9T4ah+2x?$u#D`&0buz3n4egktiwjSRV zs{K4yrI4*;xb1`0gtA7#tGvTBY z|CZRXYxE9x4TM}pDSm9F`%CEYg&o9aO~gKnZ0@)pVliBM>6%3wne`So-M8VMt}{wc z@NXbSrQ?2#7R8LZ%&Lr7WgA4juo2PxhRu_0dnnI^L4p$hRxK?8%+Fsj( zH{`8@>h@Hw+ z;*0a=8U;B`yXq4RStbz{C8JI!!J1T0yLaqJ*aseeuO z<-u1Wt{Dr!tVR?mIOI5@|D`Me%U69PwvrKbzZ`Muay{bI(2;;TXxLobEsMto2j$iz z?pU%+Khj!~~A0KutwOw{?dJ ztfhaw0haPfK z^mPCc4!y8G0T;o!ssy_>l-3^@zdaMB)oig@TqR5aEXM)4O$vl7`%Q5>upe z97^4wN zsP~v&NpIF{_Aks*53n9{VZ~49Kff=wYj%9+W7r&U((WZ7rQeU9IgQrZ)5gzN{02kY zJU9|Aanpz_)R4-SIbKJM9HHth7B}jzh0>`pimy&X1QatK8LI)eZzy?kWWnVZ5NY3N zLn;x~_@VYCERf(9d=S2AnBc7KL0i1Qc`>8Qw@s`75l}_U-{zO7Pp%j?9Tk5f_#?E+ z%-4gXQbIVT^Uj3fd%~z>3u8Sq@)sQaLak@91%2H7FH7O#snH)oGR9Iin#qw;Bdf8% z4H-qe3ujXZ1=T0{SL<4C07!mPq1bs<{WA#(Q9>Uc z?z7S}w2|9#TiwyX{lb@qE*O4yb+@;wfTCX4@g6b!v+nZ8bA5*0LfnmLE@ocxJn&y{ zl`L%e8+9)+{Fky=CW|Q5V>0%c_|vm03xPK@S?5hyEixwWtBej}nYeLa@1Ta07SJm%lwlshw$YGimL5Df+PDO{Cm3sO1O0Lk>c=d?IqOGei)E<@Xnq0h6sr*TVLW;@5%w}4;6i+FmX%@`ERX)v z<|C{+_l^3JF>j@xTA?b-*m@!rE%LYKf6CSTbvoeVeX~vF#Rm;M<)YNYLZjfDk&g=m z68=~geH{vaG!vOs(!{d#E^ujkfN~jgA3LIY>EB#%{u&MQB=-!N$K_{RzSDX8EV~|q zLqXESun!eJqgS`j{qo-ummrS+@RK3M*#%+XZVnCp*jMvc6WY7a|3h0fju>T}flxB* zQ_Vg5k_Ys-e~APgbC0f*r(95nqlzC>qFmRV(m&ou`$`@b3QHQQd> zHsz0`=~O^TaE1Bovsj)YY3IGSE`opz7)Z(Djd^5?#MPAhv}q@V-`-O3_7PtJRe^Jc zTF2&jp?s4QtI7KCmO+zOX1=tDY|FIH!GE$1^YrNN7d_r!O%6TU&b)pI#&jpt;plBs zp=#n^%qyHnrH@ytJa}y2EU8%X8F0Hvmt#Pr@_Ky~W6$0d9L-sHEAfrZRlV~3jl&5M zJJyp$ErHtT8hl2+t>&R4Z zzJrzbdnP0rASeNqG7=a58??$m!D%XsV{mc++-U{Ei=BB`qHrydKi?MN_u4F?I3yCb ze$E#|7*yW|z$wuY+e}(3lg45#Fog=`i+{}+&d->PhwW}k>~>IUKsggZD<5GU-n$Lq z<0+{j{Wr|}PW2A20Thx|Md&i`bcVcLr466%>DzOCYW=bYjj7qVcC^2Uz-R>yfrf(L zxhZ!^SJh^wT>E@>wX1)^#OKLMF(6d9R$$Q7Skq&&RP&EVft`NoX)btP3Yzqb*simu1gD-SCeL zefrD=QPkOxg41Pn$*NcV@v4*Er@*q7rQ75Am&U%6M$jM0Bjt^s27O`C+=q@C@fw@I zb)8kBF0)c%66p(LseK>#ulYB-P{h?(>95`qtC9*agVJnYKOO`99N7c1K0w3s*K-jC zp~cUUiMpdIi!kT|H?H7B7q%R;Y{jUkmhu;MXC%uouV8w+p^Qm_a=hZK4J zMP;Gx^M9v}wdgXi*TG%)vO|v2h$}wWA|>c#zSkziF!^mT^Xh>k!bIGQ^R)CpUTy5v*qxhK$x5SwuD1CP5+zMf z1r(ShDnC2rWNcM?z$qZxL@nssuCoYBu?sr@EwWoHOdt20odeQ++w(&7M~lxbHT&ah ztICZ>jGyGdNv;$2tcg{T{%7^&=8qiI3ULD`c@e&D7JV+e?iXn@M<-Emz9KBRCU$%5E_Qy+G?7Q*hK?UKF%>o5M~l%9m41Up_#|A z%v8JD3l;452Oobm{Q5*U%TnFEra~q&x7WAa^z?G{(Ti;jXD-&6hOGmA{;;m`VDX(| z0s5d(T|xS+tN(DzzBN4%IUO)%L%^O3rR|aB>?bU4^H}$p;Pfvt)d~)Bc9vOdVVq`O zzJH~PHq1~CsdZL85mb@MY*l>*GdZk-!49)PBVnCq2cw>&hcTB;KEqYT(2qM~B6a3; z`y<;Y6~MbvynH4!4jRW_pz{I0P@H+lv^?Phz-wZS;KbbEm4KO)0TYobfywWl7_W$< zOxP_#`-0T&<{9%Mvr>zK4;#gcaORl+#~sLNe8gTyAq%ZM1z+supY9UhB>x||Bl|v} zg77W67v(;iJ;ADJ z15XaR4D=fY&aPw_Oa|S85qqSz-?|7({3W7zs-b-Wn~@6sWv|3gm>3M7E997~39*<# zk1D=X7xo>11Ivp0Z$D>ullvYp zc2(yamPodFK|CqfA@2!AN186&U69AKC{IF|_?=b{ z>mexr$G}#$M*7yMv1|h#fO$l{x8%7HERB9?@3AO#!Pa4((PW#wFDE;u5!)hdYqTw^ z9@AYO9N)SiwpI_zzp)==d+a{RyuOwkkhZ$j9=Y{yn8JFHTDO5r0+9YIa9%mLy)!Da z%#=&_UYS2^A}MLlw;Z!}Gpr7&>>K9rew8(&6tIALAq!)!4xVcaA5tz>hPqi6cw6dz zy+!~8RZLAvSs?8W*lph915-&7di-xZy`@-63j0VfTD-riw<=RXD)?~i2k|#~5e(5C z&5PCC@v*6MEr@QANSu^cRecQ%dee)$ebPF0lLs|nzrTQ%?9Hw|H$IBL&$k{N9Nd`| zz9471kx3%OXftpc?)I*cZR>%g1ojZa?(&pAYueQfY1;XX`G5ryz(-hFzV7Temu8u- z?kZ>QDS@}R4sm%>mk2yw>&`N&eJPm3110Z*i89ExnkNc;vb*X%#Ip$%aZ@ZR>|UQV z?5{wm4PAw^5G?}~eKdi$Z65iXN%?Rgh_&4PgaWZ`hs|B9Fyfe548|D>)RY2b6OO)_ z$zF?Eyye8*HHRG%Q!Dt7#qMuX{{7!UipWqhwCWS5v&QJ~_?EhPF5t)XvYvdCnTISqZu7?#}iEk)1SfKGfc*%TEGqM5YHtiPIC-P>v**8iJnf{rwQV=b?47T4I zm9$@gis8EqEX$pj&R;AoUKD{J{?J{!c^H!A_#$%l*Dxd=}h& z&->Nx0}(=xX(6(f;8cV3i?6g9Mk_f+c3t{$Vj4Ziprp_YGj5$a{v16fp}koBIzd-F z<#FgU<=vAuoh^|mOuUf^88=yO4EH$KXoj5(dw1CW?~5)CHN)RF`K|&mtDEa(SATr24z>iVMrMymLVS)rSZm%e*4#YaAI@H5~wN?iR#c1b5;Fh%R0D| zb|C|xv652Li~5_Mk}{KlzOr4V$`-qJgIn=2#=~XnV~7w&BMYG}d>RF(9AzW~)Am|} z(Yi4tqgBSzhuPgZycpCgGd0yS8=ZlKUTa$TGdxZPo3g~8vaLv!V+Hc4$7{)CaCfB zlQv29{Je+V^H)$tt}uWonO&se_;_0iQGIi4B-`r6s1npBVzMIEa`Lg6#pIbYJUiCD zj=Z}14KnlDieW6f36XX6W<0hY&c!Ll?d_WN7lqT>quGHV`C{ObBDs`EgrHau4h5=IOrFTfo6NFWzSzW@Q@h@jI zgqQH7ugJ&oQCxMAEiiE8^nA8Dd@%fA#GO_B%TRj6k=v>q=l`WAd=9I*3A?6Zm@nqe ze6_Osn_q}#$Z1}>wG{P5^X}ap_gDH6VsDwj%NC=NVJTWF%eCz@QGdzptvE(A(3E$E zE}a|F^wswE2KFShqdlRo^|jGRW{J&=Hi%?iIVHHNVzYXj8QCS_xY*d5NcdO1= ztN!Nfd7aYYy%7IB@3X6!-$lEQr};SuCL!6f5eW6NNxDjx{MhmVFc^W5hM-3asD5(cwbO6WaD#Zay!d{6i32>i-Y{@*%;ED#%BWDG2%FC?e24ud z@ZEJu*Bf2MQXn_&{PL=96OF^}-`gE2cD~Bqd{Mzd+J}7`sfriFk7G%ZNDuL?iIjU# znbTy#h6Y))5h=Q6mw{0fC9`e`xLie8Tl?4w#*d}R>3xvJhkn%d7w1WNN{Fq$=Z4gGx)CG5whNgx^im^xCiFsKyjzY)Jsq6_Cznd$kQ&XdR89C zejDwtWZUq9yTp*IxOiCCV4r-*2}sA|ttL%A30IWID2G>Pn>q7fUdPyEeG3_DpL6Kz zk~x3uy>Dpm#)Wsa)@Mshx*pO#$O5>*ia2+ozf$%8?*b^KjGlBo>KdUyuo>5BlfHk3 z9z@@T{S2Q2%dqwn z0d`eCQKg0glp>YIewLWtibM~*6z#GPpL?m$pTa7ns5`7_Xuh?ktcS|=mgYfmHp-gW zB9Xw7sSjsFQSN1pU)7{mxEu{XAa|i(0K3qC+{0kYRlk&v5{~NH>}I!6>TO2(Catc0 zBDV#R?cKCJ=qK#n%h-nyUkxYimvr$5w7kah84FzT81-)Ga>?E>K}ebt<5O;6@4*qT z`KpxzDJ3O!l2{Y}kWQN9+|CT&wor5zu+LZxqO7N|YQ6v#M$;D3QtOrdzYw(mmHOGq zJF~lem*@uXsFxC!_?6#hWR4|BwwcpwoUk)}D>nS!W>X}hQZnLbd9kp;=|gvhntwO5u~e3==#kU^QDs&?_t6phJx|u}(TFkYZn)V4o)xMp#v;-u{y2@cYu2tlhr}0373P zQsYQ0K%5*D{dQr#Lu&k6ak_M8EY#kp1c9TkIm8#57Uf#8*bvb7dW!UAmOnt0$`Tew zCfT{tzJ;{zqYe4p%-h!7Y>wE|earRD|%^WZ?snY+=Hwy6H_EeUljrBZ_e&yuFnJ^iQX!taIibN}gwx z;lT85(1bYMLu(KC5F1t3PzOGdn+s9+RZol_G2ih$)jR-h^Jn{QwgisEBGV-gZjFNQ zu(pqL%KqE75^G_bX!6|G!}=*o>h?2w^bnUzrU+%lkf_7M77AALrN-3mX2>71gZ(+^)T%Yj{tP`&;Q?VKC&Y z36~EkwUoiaG^rRGUR^*!x|-lUn<%>W74Yv4RhrO?s|3%C)@|e=L<6zHsVtFe7StQX zDDNJM)uI-wzi$f51^V3b55l!ruM--p!**YKp@svUefJ=4m*D~0k`2O@qpdpf5o^PT z<d<5IFK$1iEOw;ELM4EStTWa0n0 zD~RT+P8e(P`@}m_`4T<8xGlE(I|BX$c=G06ma0!@7Ha45Pg$qt2~1hsQ*Z=T!ztGj z{dBG2h8Q_rsgzzME}p||WrlQNt~aI&qmT*yX{L#U`P%JM2xQDpq1v$YB>|2`+4y>l z1_e#G`6-P&85CL=TKdL0tiUO_Pc&dycU-HyR)s0Na#h)+VSw421Px;)r?}XNX5Vs? z_Je$lO<6^8D8sJsxf#Kgb#44U#@Lg7P)=lz9>aK`Kh9C~vn&CSyG(~qS9Kk(tk(bb z^_PyBy`!e@mqWwvsggQQ{Wlql$osBr-TL@*b^A?RYW}H*-RL*TzMdD_L_U*eT z#t9aCpI<2H9^+|bS)AB?_JxCo&Mvd#0T*|Hl_eD|3w2+|0iM|4d$)9lW)j`XVspRf zH!&tc?}H|^LXW?34i@3HTjwBiQSzaS zdv~>HL4SdnzcU|`9_W>2F&X2w0cqV?_+=?XnCmlIMu*B`7;XiXzjua47NG6jh&6j(hfm6V)8}sX zbdJ*>%NGOYo{1F!fIK+G1;tPw10%P|ivgW$WcJMcm6dqT<#Yva>BYJObQnI9;{?)l z4xmeBS}j?D1jw`v?cn~3I=p`XW9re`#D%>ujV5)>pn{lp-@U!6aRKp5eF6my1};3# zO1~towYncxENlc0@#oX_E?kV9pgJGcyOWM~+_z)S$%b10v4fGuS6H!vidP5fZYG+p z!`UQ$eVTHAnxcv3o2oZkFX^4H+#;u~&y7jZ+u!n^fq!`Y#B*gTx3bm^Qm=ocJcyOF zfO}4cnufY^MtwY9Wa0xltiZLzn$uA;vJ@(vya>>uuZ^xm))+T*mr-pV*Z+EwZNw~q z#UBeq)9bH`H1CkDeCwlwyQ5G`$g1BaW z6^`-PsoZkFCA5k6keIiAFN3zod`Vt*_3CV=BorA@!5Up|FD-y>dsSX(`C+#4RJPc3 zMsKU#En)bI*FI_{;XbCzPA)a`iRbLrUgqnBMx;&SF63Mj+|_fD0h4TopgKkuii3<< z-<|!<8?jo?=6tZ*25a>}E*F@GeXp5m5y(~}F*197AkaaKY+8RuSS_#yp0U z*X!cT*8l9yHY1P6k@4qtjSw6oGpysKrY5V_ZUNTf=4h>OP)cNYJ+h z=xRb<@#E!v@SU;PvN9GK`4meicwEQc4T2%u!DD$fR?B?OzG6LF)2H)I=l+L^W;dhd^*)Qxf8r0uH-02YMK>F+ zB}W%`FN@YeY7bmB;u~T!uz|Z14htedsfSlC4cNdESm`^`gEq8SVxN1`96$>WXNYt} zGef5fyj8z&rSH;N>9C+hU!bCt_)pvoJ7#S)B#cx*OH`Tn%{h(+t(p6J8D_d2i94S; z0j*VJw_H?0gsh+4%`eOoe>-7{!fWtHi4(qRiJ^>*1S^CMV0=oHRtBzB#_dF1@-N_$ zNyeoI6w!=+o{YRu%>D7vl@_Ce>hPMBYchJ)+$oFI?90YS%Adq6ao@*&e`hO&jmGSH zDa-=gy7zQiCvruvP@2$H-%)yYRiI~aCHqz32P_%{KrD>>i2t$8fm_YbcH)ilJQlmb zVK>}B`t(Wsc#$5VgO>_@kmOqqV#x9qv!X$HE7PqSrYFPgYsxQeC$|Q;WwY50KG|7j z0_=hh)>j%gvjL8qj&xbx$RQSz`^+@>`0=o#V|`^&a#8;$bC-ZMxW@dsC>%5uwz3=+ zg45wHuUm?4+==ThKer`iys+n)e+?Mc-&Nb<1`+Ojpb^$|YJ7)q1n8+QnH2LF_rU{O6Eta3pBbj zT>imolzyIr6F;v8n=7G+jxqQo-7V!uQO*?(1A61@`p%8}bG{Z0%^xX7c22{e8#6Hd z=#s&dpv}`?|5LNqU+;u;uD5(AUF?aefA%Objrs^{ zJBuHcGPc$^R5$R~!nYl^P=Yv*Oce1cvd*P5Ex$9n)bS1TdC7}mEB;R6DyA}HND3Qo z-hk5c7j`k^99X-fi%ZopBZ9#HfCdw&6EGm!)ixo%b<;CyIx^Yb(tnBN!C$}g1fFc= ztdsqAPpgZiLhYW5l$wsve3-`(}x=H%N#ycswbS55j|)9nYg4^%ZLG`n)|w<4|0fg%HtM`;)=L| zRIIznxL6LB^?S1{yL9%Sj_Z*785PE)?MFuqX{5g=HBiB22rYlkRb=sy^*?;KPC5i2 zhgDBDCIkhor#v+MOAk^>2#0S5GViFYW(VOIuxHTLUMdoEQgZy;6n-J4G*g}YsSl_36=(_)5rDY!B*3c>_lCft@d$}=Xq=kYN&{eJXOw_#-QsLdL|^eC z_dqWF&%mL-al5&pat$K6I6JEwd}bRAzUfr%p?4hB4I9E(UbKD={a*-z8B)>U%J~OY zzlEq)@im$<=DY^<${=amHy>2S@2PDi_RTOX^ztihn|_Up9XaaRXZronml; zu{?RjiR8aOUXB0`VZ~%-0vD9!!6s9r^&+i#pMA|+V&nmp0zkwi-bO>>Bx&)oeiEFC zCQBdK@}Tia_*i~_khXyOJeSu*E}nbUeMWXBr6-&qaIz^kraLf6KT;6jnI`YeA{=Aq zgApIhu_OvK4DsB^#|GHC?A;)Hk;-FeYDEaLBp3x`Uifbl-erN)XjmO1c+1h`){Q4^ zpf!9f7+4S{k2Dxe&2{PnB#QHX<;SDhqDA~wkAqq6Z$T{vJ zNP2^3&)+TmJQ5!-(f|=-g=dT}5xm#kT7aqWkBzcZ5tpOZf{OllabemE4Ccm&m;c5gPj;d43}p3!g0-`2$cCUO7fD>!*i3E2-G z`*X?RcE$gMJ+Gt4F4@0!Paz>I&0^O2?=6V?voi1N+r9P1u;PybSoQ_AF zIa*i^g2BOI{n(j@vHTz+kMjQujiFzzj$IO$q?S? z6n>I|qzg{}&4{oto%nn6$o$*PM9S=Yd38rlf)(uwThAQh(N|eU)L8~Hg=uDxAyW>b zFDQ_kx?wdWy$lSpVu}?1%XY0EkVH0EWAwqb986{}{W@_>>6aLU6NkGx1diDJ?|ZIY z?wkGc?qdMQ{7l946bY28yyh-rEAZ%W^MW35FLGb)`qul2s{&s#khq`zw;9@$<7yd=Qzi25kTCmNmyE*NIZ$w2Mm8&f(?i?Fh zJr?FTK=^v+(TIz}PY$VEi?a>z!@E)au+GrNkbTv?;aUmTXo4!|6_#>eL&-`?DR<`o0}$Vr*=|;+YUQIRxXUyqDgi>e-ei0li)XP6=DeY$78_o<0Et{iwj4%9Hi#5} zMtZdlyKhKhwbn>fL9X+Q0O&;h6itt$Ys%sVlkP3O+eztkCk2}Q&?JF@XZ}op_xU_i zealtuhsmb}d3eM|PD+Koo3_BDiG8;oA%mpz&P@gP>1rHVPIn2z$GQqYhMYYG`U279 zMP|%?9oDDN!UWB9%(Wagr z(dP9;Zw-7SYyuh#Dmp;!TT&N<6|MdUo`dLhJ{`=HeWtYzzBHMU!?_cmBjF7_QF-;8 zwR34KxbHDxiRFEi(c#p-iWtgBFgb@>t#IcASQ z(D1=ota8giry!<*W^6eXdXmI(B()c<*I)ZXxK2V8BGX~a^2qS=Y{6>6aQZ7}Xz}+v zbJ1r*_10Uf0Fe>9`nqD|!~x?Nt~hnkRlt9mGyQMBbF?UD4mW*rWF#q*hx(Zb5Xq`3 zboB^RI@&Ugfiv<47lmd*0kirv30CfwhU3q9zL0YIP4E6)?%rpnY3B~NIphW}F-np0(8^njbrI>m5u&V`GJ;uiL`ka7^)F)*qJGksdy4ntOCGc^N}Lnq3j3 z^`~x?A6+^*5>03r&z`fSBpseLi`6g)N;`k2^gwUO{7PdIFr$ltR{bI%+g;vb-Ln>} z#VS&6?sgact^uqg#ZV$<;O-#n>Xy5EN@$B{&A<~`VUL5nSO!|KG;>m`JFwB~(a>r#+ zQOO~=KE6244r?qn0%^6*V)(=@^|Kdi zoRB14&VE#fQ>UTlr5~9eshSfWJTacfu%|#@rntubJPH-qp9_)pCUf&5xkOag;(qnFQfi<-BlGY(%ce?ZcRCeF6AUhcOwKzye$#1K8J)vTAC4!o3R08D;i0oa-8Rwh`2cK1~FT)yXS;n&kcVbUDHV+j>2y<{zOQ@ z%7WFmY5VXudNf0DgG3cB4`I?UxgZm7!^-LaQVdtZhVx3K&r?ir5jvha9$mjh{#!dh z&ZFxZC~NP-O|n1*>}#c@jeqJ?@f_~Hq-e3})z>x9T|Bmd3UiG=`fLrS_l`Qej z^qAR8l@54HmIxiY12WZ&dWPA3PX$=W3B+2y1%u=TfE;ul={i_9)h&G$_jCORI7yHG}2<(+L6$A2u( z?eb{jz`?0IKek(J)H4)N*T+Td1lx;2_>*YeF6&CgX}T(Am136Z#bs%-9hj!I(j^l$xDkJoGh#Z_5NcJc6 zp$~H{+a8^((>A%XbqXHzP;H)K&(!N>0}~e(M>6;c3s{!(Zn&8AcW)hTGShhMyWN%{ zx8_uYyNSBb#=?qlh&qTfTPv8LqanaA5W3r%`l76OEhi7ecCA zS%;%{PakvB4jV?-j#<<$f0x_v(ZFq5KmNm6QZ&$3@d~{f^_>#fx!^i7rKId90}H|l zZ$Dus>@5AaXYhLEjdLG)>lc2B*WGLcdH+~ra>s?B6*G_tSP6g$jf<6X)Kp{pf%eEf z%nS<7Myn6PCkMqkE@(gQH}T`?2yzXi^INCVXEvVd&Chi$f|ax`hwRIO`ka#gnFV zg<`5xsCp1d@>@yvkT0e&&|Ds8Lg*&=SA5!8$ z3=Aspr;QFJxI}lo-PUBVwPvYo5ys3SB#o6HG%C7>Epw}e9W~C+4XMinR-QbX7h&U@ zXdq^u%!caB>vGIr&B%>}&YeFgk?&a_RdNN>CsX;p)4nvOlQS`0@7JyQ-mrDlsU_4j z?m95%Yz*1HUg9FTxCHgEQNUj%u_K%DB$Lx8#vA{ZWJw9b$^zF|9f1E<1fDgWkA6_k z_aNk(^t0)vtjn7^Yr>i9pDKpVPCC_qJp8N^#R=m(H+Nev@8tQQO>*o1NvwIsx9bve%w?VLDr*;9^T;pSAy~oj7nP6) zNNiGZ@lq1Tj0haPR&;`;ODD@10}(mlYu((W?Wf-nY*RG>vAUM5F&(+PAUD~3FHDm@p&E*%&Wo+5CP zThT?-&{Dm<^xyJwu$U6hOKZNj;Qyt9pH)CDr1K)lLV0Ow&NadJb;jHc%Hz*-XS8kd zj`O|DbY2HR1q?1v=<-yaM)V>_H4RQziZ5LvjRNlg18&1*E6;g-SRW^TGSiZK#M9!j7e^V zS7EVQT^G|;0uo@pXfa=?X^8i(xSI5f(!PlaNAD}9Z%pM)D(zZ5;ND8)w40jlHfvf< zc}|BlnI@`Vz4au3`j=Ba6#s18L5O>*CRvQB(7I(?>=zK7sg_2;K|x_rp*YwB9t1Fu;W7|%VqS%jD$+PI+r{t=0 zUD&p5csD=K2}x#n=d;6xy0!pf&FTYu8DyX|ZLxwLyFfTdd z3aqZkK2`hE(aFz%(F`xvM+9~9e}1m(H2f#LmnVGjwG&iMGhzDzw?$rQMG;$9S}FK- zlYdwYwaB5=7-8A++|JDpo~z|GJ`1qcIdSI(KR!__eLx=)NaHt?&E^?5i|BPNyBe`$ zXKnpsrcw;^;*qb1^bRm2NBOz(jG4$ukx1c~6v-y8n>Y5gz*36gj$B?DXVmR__mRHR z*jsw^DAiUcY}r6a&+IM0(NW1eG8md)UtCdkS0Z`4msi76_UOss*-PaYeXCu0MJ{;& z>UQq({6{6h{n7pG@kh!(p2#UsF2;r2kO{HP2u1OQJ{Qi^iicmSG-DyOVTw8@Pw@5# z_BEfY&uXEQ#*0$%rAAfGPffZs)#$a*riMwa_-HfFF z!B(pmR{so zW_@#?c-q~p_Bq#QwMcx6bcznkhpGcV9%(lBHCH0rzl32E4=ZU#w72cRvhEzb}(bl%Hj)2ac;J~E3Tbd+YG%xl!2eN zQVQwjqUw*LWFhTl{*a^tO1_4zV@B_7+l-oe@8D*TK!~q5{9)m>4?8u!wx#;LeJz|O zkfd#;5~r-@CB2foeh2pdX6Cqm{am-?GDn(CS8=UdozSryXB{ml#q8U6^;8Dl-TKZO?5J7aFKDM7-qjxb= zLzzHO{G_~~< z>YU9(AIh4`PF~CF1eCPZtjz62u7#T;Cx2pa9Oxf%# zz;+W;e_zzMF}vVpq=P@-vvKYCp%ZT~A4`ID6nq2|xiR|DZB{YTLAy&(Zb6Vj}JJg}~Ge(A5+Pi|fOZ(oNmqVIZbt(_L*of5Czpcs9g z!=0V>wz_i>VF3E=V$kLpAFDZVQgsItAK~ca4_Qm;Y3BS64yp<#Y~ii8D?xTZnvvmN z!8|PiRctSc3nC}xS&IQ)KZI3(xt&$wMhHhc8BLIkS%JYK0>_3 zWaAxD!>+$-W{b*e_Gh5sti!VGsaqNI$@psjLU7*EDu&ufH>s|W4$0RZFPs}x@Q_e5 z47$?VIg;yPW-h%#N?|Jrj(|P&RAAMYL>LCgP)E*GQdnZ(Td1#R&hFrck)S)PFF$)x z!FP@%`=t<}&;Cb~9VxH_1)K&mZyC*E`L9)>?EW8V-yP3pyFT9Ppo3DK_Nb~Vs`d(P z?OCmV*MWPd(QiwbH3-C&(C{4pZxJ8 zd7k^ZuRZSjzAh?ekfXFsb$6coE|TdKJ_qKq_PX#z+OoLDr9!5!DWv`hs<^c2nwn(p z&ifdqeCBjB;cJ*UmrI4}hCWMd)1w+z4zZCT+83-!D~XV&>NZ_MW78UjbgvtS^A7h% zAKg4&ByYZV?$(6E{(henyN?Ax44Wx0tN7QALw?sMG^Qcx)#h~pS&_5v#-^LmK5#G; zr}Fp)uF;=3hLMifC~^(a^Jd?8$>HTTZp+S1Z)vdjR;U)9^im!~U5 zfd~pb@CVN5m|X#SX>0>M89qVwez_jrx!d^M34VqaDNo9-9J>-@mt~Kyd#~K0bT%3! z@-wV8vtc8$CCV-_8S-#c%p;Mjx4;N3hF^FLnka3(iMq+@HUfFGixr8E=O0`CX~fI^ZeYQ7S6v^ zem50)xltbNl-wl8@}glFL7LOsf%aT3&f>5z-M&}-F_P381|+;GUomdG&DcV`ULh)K zCYF{&lRM$9t?R?kVW45l!et^*aPhp{YMU?K-W3UMyb5$L;08_!&vCdpZ$DFP*Uw#$` zvl@tjjp(c7^x2cOI`wP@x?UM=S|+j@9U5+H6Kj6mI@CbOo?T)lzXQ$LEjlAbOfp%9 zq94lQMy^=Gmy>S zu4shj3vv{3`8Q!Es{E=)r2GgkShU76QAGo7*#tLQm8C`F-Na4*FHe)PzeszJT*o9^ zLq5wnIMh}vmL;WC#+GB&GKz|;xJdPc8o7jJTlV2}!hV8Xv(w)27M$RGb0(z)Kl#!3P8Z%|!lQG`O~Th_o&nYxJUi(k{Za%O~H*I+eLQo?w+5BP{om z>ZIgwu5tVB_WcszK-frP$MxV5va0D&Gw-!6+|GsG8TYE2$2;OCoGjzkGd@?CZd*Vf zihu`18@XG#jkqP(%moAor4R^>s$m{u@ywX=-PXAzVO{lwhNxj~r9dr+YwOU?ob!hN z`jY$bpwOuq+ArTvjn0(q3tt03TBE-O2Z<~&#QIUceSD{kzGAQ#h`>EZ<4dF2neJjO zGw=`8AWwIGDK0Q|71{`!v5k-oWeP`n?FmsfjMQ~4*$L7UnY|{p$DmqnkQLx8&rtVe7)u7Q9YS`G5;ulBI zRQZyo{MUJE8?rJhW~cS*mnie*@I&2VY{dKDsh)&7)q~Pm-6mRuHkFdjP4N%3UIJsg zPtolbVz!#cL6wESrscSfxmy-ENH38jlio5um@^~K&7m!e3kbtTKA5?dn^|z^?-H9V z-z*f{^xExmqr_dJ=v&mlg;)tEg9(O5XxygbzS&uQVcJXpBAEG|=ofBc5#jW9dsz>&ZJ9dyB6s4hDCnc}yk z9w~J;Z_)Qd-4(NqM}pPy-J6)PdZRK7)sZ71!+0cE&toNAwtY`1NKI+&rfuTwn58T) z9nT2@NY)baK|WJRWN4fkrj(+{?2#*}SWev%TzC zoq*BRBec%mCyhzjh-=*blLuQKqM1(gQoObAnJC()NPB;LD&KG*qrWORBWt9B=Jjl- z)t{4G`{a?~kR5^Q^~#DsoniByi<~gJf3dT^5jcrl#k=`*CJY6?!S3db@EA;uo?ojn zo;=08Gd62%$*8erxufMJ2)@vV>hq}ab{Z%45H~feM^Im*Zv#r-xldI?&-tZ(yDnIV zQ+G+9T&j-MG9r!|CUT+GX)k4Fub)mFd(3hY)GuY^%C582JYZ|=f!!@)siOf)?6{ZUq7|;wz_{->yl&;&ajlrF6r*&rbFTGDrCw#E zUgU67Xa{rsx@|mFrbL4DL?pvdIqJaW#>wLusS`makInON8G!cv%RQtj zug` zZHLEG6EbE-X}s?H-P-3Gdo1xx-YZ|v4T^tg_qGd50|N)8)+Stu^{Y&YN0=n7GF)t5 zYqkCdoL%R>6y*k(H3W-OqTs<7-4@E4NcvN(7F0xZRl zgsAwa%r#cT?somQ+u(IQ9yVgD0y>EW2>E&p$RQhm=D~!aX8pLw+Ez&(YJRYplypbg z#Y}3B)93V4b(g_38MWLXazWbW!Ny`Ot2*_+kn`7{M1k z>1}1r3G0LFP66k^!;atV^_rAiqAddDEOTZnxW#8y5A@W;)lYaVaVn!>B32=8Bxfr8*gL@-?gvHDR;V z4NgL=i;C3HSpu;wkqAbjIN>qwNAy)#a#E5bzsVh4U0ve^(<+1%#YR-XP0Hc%@nrmJ zD}E-2{CGQbfbMkF1%TcjfH8CR4hsWkAq)-gEwA(+3dH&i+;Z9pEb05!aq(nhK*8`L zd$`{eh$$9mYG4uI`jNz+ipJEMl{j98_1S23+7JuENe>hi@HI zqEAvpUOl<>gKoROt!0viBfc-4491%jWUN-4k!jM1s8HWtOCQF^cET+b;=0+{yyHCj zjWF_f{#rD@jeURi_Lo`{=N=>_Kt%^?iAZ3%iY8Qb3pUEq4M0#3`AwLbF>8i%M zB?G(V(Wc{%Oy8mmJAv&t7tC3ocbod`xSHyA*NpYbl5j`|;c;s;?)8VTV|koM@}I-T zVx^TG1?J}F4q=el@r~n(e=hJWP%BQx{YsKI3K7O=VS20FYIm#v-gQZBlYc?_Y8kb?u4A#y+^8H=%ja} zYLRuKf4}NJGXcflHEy{tRgflci1b7Q)tne?8tdq&jAY<~Xf_CoGSwoMR&r4RcP1hsNL?GbRP3Uf5b7EkV zMGxvN+mC6Gd@4bQvf-?IodBaBdN%)ElGWCOhF-A{fIMtDi0SEJt*cMHeU)2A&}S9I z{4_e=exEjoZ^Xxbl>(p4k4Jzt!2A3>$klRB1FMn?r}k%D*&5~f=EXrZIebX6vSQRp z;iBN2%#rUzeStcV#4bF^?f6W$MRCi{t-`fpX`Raqmks@KFkb4;zJs$eOz$0EUZ_2d zE3o*O>!$L(?b%6~v%aho-rB$^_mD3cmN;DRDdFXW{6bdM+Cp;GV$26fwSW1CP2YKM zi`gPhtrcI3#qceN!DI{;h6_UpFhwCJG9S2jFj0$3t>Zao?YS_R#ws5eKr)AytZokN zwh<)(3o_<5*4AOQnS(DqnRmBMh-1bS#|{)EqGT@Lo}kl_X7f(a{GkgGwlp#|;7v-> zTeoCuR9G_IRz!*1yR}hUg6ieIos$0LCS6f(^LglA6Y4)HKcCI@SA!QT&Gm<~(E~l@(^S+N91>&gmyb zsu3IU)2CM|C}1}UTf4fG@p$T|>VNBy{C9fY2)dDPRmfXjgqK=cPvCy-1+vl z2YaMnBQ$pVsYjU4Ker$EmJH_Me!mtjRkg;)U{ z)ym7TmNPif2%sJ6q<1|63)BiyUP$Vz2t$!5oJOvH%%M*Ib}4W6;cku4lBKe<8m9jI zv8lBv&vIpj5HKdyvM1Tvsu+*pS#cH+4xfqx{GzL=ekJ<#@OrG3ikWbb(W*F=53(2O zvuK@b`&5ZtL|UVpMx?35IKc)G>5IxT8oCIS_4(u~)qGE`ESU=ac&`UssYZUiS=BLY zu7m372q|zo*?l`q^h#Oe)y;0yp|;7}I{muR$di}6LJ;uh&RzGAWTl@aFZ#GnQ?x=I z7BA#a!1cM?Cp;GunY4UcedU~ww|WR5!@P{*PN`QB_O>4Hu*Do#X7s22h`kDG*i}!X zPf%HG!PWR?ZkQ2UW64a9K%GFR^M()TM}m1ErTpiP#lymfh58?|HXeL0b?CKN2iD(% zC=?O6YcQ467XK?8F5K6M35=?#T9QIm*`Gj!i);RdSwCgnq5vx!A;k3sm6aehv9xp6 z3u%gu{w!|uT;vmY)F6rgkJ)cwtGGT>_ioGVSqPkhp>@P8ATP7x!?A@n~AIqVjT@myI zgL;|Th-;ZZW3S{Sz;%d80a&i3L5ZqgWJ2N8TdYc^wh;2#)5htr);;@Z))PT^>aXk~ z?;F_t7UEhn*HrWOe%VPvZs}z6cQ93)akok(IPC_oSa3Pv%yv zHW%+)xBD4Fvobx=i?+kJl6*`DUodxW zHNJ&grgm{K5zF|IqC3ddKswd=1ww7E8y~V;4tL94<(pRt^7BUFZ1}@ccEXHrN#(g% z2}TSbJ>a4TkO97wSs04f2|2y+pmD=9BiA{!v*HeFurGgJ8W#;E)$%BJiix1Zrx@*s=s3Kk0Ss};mtaTzt zlh);=QIukwpwX}wZY`urZ%(DBvdD+g>)HAiXk}?@Ta%&X1K17qw*Db~sMqGfcKgy2 zaM0*#Dl*uKwq=P!89lDRp;z*TF6J%`bz(JqqDXKzpJGOtxV|Qt`SUs%wf?1`XhbFx zTp0~Lbq@72wHAPrDbcSqrFOgIDLShk3O92<-orA-0T4!>XA=2fp6#}3dsiN5 zwrg>o>s$3KX)AJuBW>AkKnkbSuXR}1AuW2Mc$M-$$=Qn_` zcG!d0ji}<7Qj5QjS!+B@*WxYQTdtSq{_m#X-{flU2b{j8+BIJFVn(|%OIJEZSiT_{ zbKT8zUCYR8`LKYc_U$y*io>l;rWRJ`RDf8_(yRS>2Q}uZLOD$SA@R+A);>sw6%ingES>vlO8kBFHklb!OQbox-YttB!7MThY1 z{62HnmAq^|QaXE>zu@d#H&W3Q=~BwjO54EQ)vRIpNs&9{8b8=_E9L)OhWONm1SpMp(F|{u&hwHd2$#p*m4HY$Pq$_J@bfMdR z_Tjzfx?;72k_ChLQ#H0r+IO^F>Ed?_O=C>5e~Bn($A7_@7`Pc91kR5cKW*=N9z9Y7 zD$){5?Xu#HTJ+|F(7Eu|P}P&sV;byK$DL1>i$vN_j^*rrYha zTVmywVg#JGFUAv83RVD?tum++w*-OCu*Q`W;*&mAZIrsbDG3Avc#xf8d;Ka``1@s_ zSmC#jcF1)3fo-Hu_8Jzt6!l7DbZXqm$kbT+Eu_k8%cH@_M)Yl4@c`5dxhX%!Od6G* zg6@taO$1(eo$Tl!GKehSG0E1zd#!4%FcB?ci^@DQpB25CXKH9bCW)X-JtoGFM)T(B zkaBQnx|`y0M0fA_WyQ(AIfEZVU7XbIlGnaR$Gkv&n#1`P*Ml zSC0?@ZN(Y>GyFcY*8Lp#-|oT1QWDGmb(~p)*+TpN6;-3zAHLPig-4_7P6)Op99_Jb zcs(X`1Z!h+;e0`TLpoqz9A8w398Ju~)E(TR1U@2yDDca0*^H!#p3^w|h%8k;9{>2) zpEn=T7nZO28!;K!`Wxzx@Mw&5d5SzjmO7@);R{mR31l<<`y32g0t{8?O)V{-#)qTC zNTzV*8|xeDD?=AftWr-}knOmqF_#(bz@iuVD>l;KP+O&Kj;BKxWu1bGFO1~9Yr=pp z`koT@-Os@a@rQwAO*4pk-m2Y`L~k9_(gR`L)^fB^(wMwqz)W%UmRDYst@L0-{T+cG z$gER6EmqG58i&51l0ZM!W-sE9vlee-*kw)Rnr2)9S*cp!$Q#R%>{5|lW%A#(LfEJb zR~pYJx`PEaW}sLt;r3S>cZg-njY8LmB|XSBkQFw@J^K~%&W4_;_h+`t^rB$T?A-l5 zpUC>}ggZo^mLc`}Y`|b9+*|@Vi%f?TwptY<9^|1`u<{Z^0YZbdJ6EmbVO7)pbT?ao zmzN$`+ZjwKcOV&E&KBOHuz2{q@H&XkAOV6?jO*PN4*JR#3Rby|iVjR&C5G~Riz z2B|k{5O@PQf4ltDn8}(UExYH{qIFaGR88F0E-h9YvZ<;vvksDL(Ftw65TM(?&%dta z65$l%YgQBX=E3b)5nf2o%9J&M%enE>^Nji>pKY#}qI8F}susN99dUCwT?Vuk*M4OeJ$6DN=(UD58 zyye>c%2pE;XZxaBqmC|O?|8G#d4ikTiHYqQ-(fy^NRYSFDwM}at`W9 zr3VM6%%wNHaW1ln#@D9t2YkiM{&Y^DD=M@ZcpEtb+~=TG1?v>hOIHyGr)f|+h#tzX zS{K#T_$_zUXc#q7eP_axyyG{Umjk9WG??d^#|sa>IdQ7*(HHDy34`2qpRbSi0`q~6 z>@Cl!HSAQ3ya^NmuZn*iFKVqy-mMzp`pu6A?w5op+nV6anNWSQOn^y0Bsmb#)}(pyRBbJJHI zHokrsHPe^>xFt#kF})v#vY^$lG}c=w*xLgV-UwiIU;cZ=;`BT(8uR_b6Ta{giBaW} zh_{Kwzm>{_6GKui+KE z)Hr6I)s1-K@q=T>R)0n+G!$YI1hGV4LqPSXGqby`2#UTrZc^dTY|>Ym9A4XQpY_E! zU`PA}F%0F&mht$%_JI7a>x0EK#TTYsnRf4(O2=%Im||PhgWZoEO#GP*5hC+^C^2Bk z{28+nYdXj7HWBV}%w3*;;tg2UkpCUlycheD!x%qVoH)(Ev6n{v#LXzh=ozeN4CG2x zwt_Dt24TYaN$juFHHa>#IfrqjCT0PM+p0EbU>I4^V<(ILr0Wy*C)evI#H&o&)&ymp ztiHcUL3V!nv=EiLy2>W*!%kJJ+d9O=qp__x$h&gP_Jx0v2MBi&=LCzt&RXYpx6MT> zCQ_M*6ZV14BCx|tCr%t5wv)oZf3K&y4iC+{CI`itq-WHsxB7fOxdnN`|J)>4z7eU%5s8<%d-Jm>)Q80{$#s9R;WQQ?d7?Y|K;;xo|Q$(BYk8R z+5n3e&NmiM_ETw*My5SQz_}fgUf~RRqb#t#9CVFiURw&Z_fP&$AJMhb_D@&$OD{C9 zHhcNDd(A2=6!N@9%;?a-v=EGmeQClQs5E^E?$K!|Bw@AxwmI4V^l_0rC#aZ@)bX+} zL?*tFrZqsovp-~@POEFLXCB0@r$O-~4m2;_poHR~p3mdw%Lke2fp zn0CwKY$m;d<0J8#{;jV`)KhF|LS{eoS54zhXv}-9)ZZlE!~cj@d+unuzc6J6%cCh9 z1*6(%r>;p8ggXfQ9(RAoM6?EgiRi#+;K7jM1a#5$}-RY{-S~S?fvA8+EJS zwJYZpK*wklk6>RP2}uy`krw1!FChl~6*VZ+eaoEHyIYd4 zGVyoDe9vLdN_@Yxiu!BHnJVr`C!hP;^7~{MXiwimgKn$GbKCtD6T(S#1Zsvmmi>27 zQ;$I1oFdlzB~ZzaKqZ&ts*L*`R8~#^R91zxi)w#K8B)DD0`>9W?(a;;zX5tq>P=zkojTLKW07@~lGB;#DA31itO zH8sXEP-WC_viR>j%nXXt;%S-Hmi$$b#cIN)UgaYT)Bj5A4>WAm7z)ZXDV*dZ&e6}m z|DEyUh#d2QWQ-Iq_bxH2cAKnUSe0qNr1$whAoW`?t4px|aQFQ|lUft;^Pl{XuKARJZ0Rr-A zPuo8SpshkOKxl>Jc9p;R)r4WJ65{jSZ$cSNUq`kAXi0HnJz#Xijciu6AMG0}k| zg;q&x&+6TBUo7_TOe5oWWa~|VBa25^R7gAm;cEMZBM^cX-<%-E-=@DI(YI3%2vx>; zz0OBYhiCtbW_}AymP9i-ILxc6{)gWDY6@tH#^i*NKcG6|vJd=fEX?O?kLV*0=>7qi z=)6O%2&*@($HE}%r%`pAcbTlAVU2bC|A?l4ybdB9%bn!bQ1z6+e8udDL$;c0z|BTx{fDTUA&5=CFkT2nX;AdI|pg$qbP5hr8 z6tzB=FE4cI{_&_sX}qT+jEgaV8A6lt`8{;E>Tdq3lD ze;(gibYgFTR9YYQy}s~zGev)*!C_*!!l*GRq98yM%e~lF$3Js`9%|Z{CsjTMyRUtH z9lEomdaR_LIU292Rd-HSp9p zZAe;lFTeWZ*65>|Hg2^FzbX~WL8iWrZ^)K0*`lTkwcf+meFM4f`iK|&U4CDl8$Fj3 zQ_wCa&AmeM3Iw?UoH%=FJghMg=&xA&tm93s(@b-%b${As$0g~6rQYQ3k#oDTI=A{C zDwtj@N57ImM41Bxn1ZZ6ygX_3IcK0wXSgK9H{bi(a&r~Z#M zjoW?_P|596Gwu8v<7?w7U333YO#^T$$&^Mm-_OhR^DKVuS0_~n+5EQit+ujF@QrZ;RF-|h{`qM*qGj^`@Aix7iDTx>u|ewtfFEBy1gI2KKUKv%Ay)Yu3s^;-xzgjHq$&t!v*2v zJ=@azjk9C0Agv7Vx93klR5b7zY>=`J5_WJ3iN|P4h zWvkw##EtgDHWoCKgqPz@ehXRIW!x;>?;=Vu`})n^*6L!dO6T&q8n+?-U-rcOdN4-} ztIk%_U2TyND9!GPJt@|KTjqy`%8G&E~*a|>jX3Wc{Ep(@J+<638j zo%Mmb+Bf6a$d<;P_#>J!hz2DQbPPNagx`o`olQ1H3Eu6eI41~M2Fa>JG;0Z-KZj2n zQfQk*Bvn;FSd=Vl*PR=ZK+p#0`v%Vzul8~Ha4*zmqf~f1wG?z^_q&R&TTD*_XZ!fw zuI4_MB9OiHTe31qZ)}e_80ccs(j8Fa?VjZL6-3!GWIc+6`=-ZYl^`bW(VTYE5h1MiE5lEjA@a9q9pZqXh)22OrS08=~+k53tbia4S4 zQ}ikfuhPJFhNze%n^>0Rjr;v`w2Y*wdSMf?rBlfHfiK)EAGVpYJ3=g4arUlER(PQQJ#8xKOb2?LNf8^i-J;rah?g(vyml4p0 zwgcN;hBeRW>}GVpZk@({)1|SMas{dOJd+|{lf9|auU?Yau&IhkN_B^y0>F58x@I9i zu19USO!U3j`Uho|v7}jpHI2GoCEs-I1bnpbZGBnN*eV`JtHa<~S>V133K$2_GSGfT zn4&gPozd=B-+8sMXuoxZbWLXwb4_OfHzSBi;?(>O($J?IX%fJsnq;<;gqe;nys3%H zU8=NpJskK2MtdoCFsNS@4_3$&o!E2c1tmAE8}2B=Oa1lZ9x~~9qF_$mQ_th;~ilfuu3xGTU4BQZ%UX2838_(xTLF$HZGA{?@d@bGtZnKVcp;? z6~hgSH9$)w0mi6dPoEeED2$~`A)o zLUsa>2I9xVnXO-Cd-@a8Vs>B?_}Qr4v8~yOHoQMTt3S-_zg$p>1j!qXxAk1uHK~JC zI~7?<`7B+QxIt*w?+VRcHZGU}V;uUIC*t1BAfH{o1yM)q9cE!LnA$$$@sW4YcT`_bKt>SPhfO7`s5i)b+YPfe|eCM%VUE=02gp zIcdJ&9D^5G1E{~Pht009fA@L&?d08QX%b%r$1wh_t%$g`G4thRU0T2Tgz4$57h&vQ zm?nVD7tI8396&|0hS`|oh}7YOU2oj5+BzFzT1Dbr`}`Sy+R`O@g;9J1OVO&g$oMq zN2IWhOpz#3ZESQC;OX`CZEMttA^8q74up#@b*_+V(4^?v`geZJHe{^$I1ZH_px=VZ zfw{nWkyki-nJ1noVysSjI(=Y9)SL)-`pR`m3*1$1-QXraG$J-cEYR48^@(ETqWgX~ zf|xTS^05z&M4u_7OobOb&b{}FA7*ft09I;cW-i_I61lui*WER= zUg?Hzs@h6WeGp^39FQ*J%)1w$zsYi9)y@UuzE8XyLW=pjcK$yxJOj(gb&W~#8YP+= zr~H^{OkQ-DXM-RFgb-N2yi>gi5GD%yp#}$$DcV%O&d4YldXwqZ>MBij`Q!1o)Cn~fmRn~9)SxkhW?5@KA=ZNNbSCsTxlW=L4gf>B)QENgp%M|XbnOd!B!E9x{ zkW$0AkyT0p&$e>SehRCIUMyWNN`zOs7Y+1VZOoP(gp-8S4Z6GV2Tn)9Rn+Mpv|k5? zK_l|~BX9T@2fu_1$E0#0M~$u1UK_-TIOkg}3xGMm&w%dI9We<&MRXVE5ACDvfrm+c zif~?o!Ez0VQ6;;Y5e85?P*h`sm0wT!hMtDmp(2n1iOz z>-1!Q=M?Rm2Rc?y^Ge+)w7iO)6|!S7G2-5d&sjcwulB^;;)@sd@fGK`<px?VuN z8R=w*c|R%xe*Sss8b#fmY@t{dOjfTV13nB+=*yp;+)O_8taKwCQ_8~=B9~UUoa2RD zr1xCEf-hfVgnhr%&KaN=$>+8HQfWx)nas@`SV_FT++60OBsHryz9q-hmG|XGXFmG` zqz~m0$YbIq8GOIrMAW?Uo1dxlkE(w8Qo@Zpl1!tz@{QO=7IQmiKUwZizkVv|9PRCdpNrjfFkq|B$FT{yR~>ty zZd+t#fXlG6obKGCX9l_2)&B7{Ie8-uyLj#HaqzHRnn zJr8^~4XlRql`eY_?U*F2Pj#3lMx=IG5Lu#oqa31r@5l2YQ}qY)b@=t{NA7LZwGUYy`x3EkOcJ8iZ@xhXz3f^I5YzH}nPt%qyq zmz(K>T>q?O)SSvch4OzSSAdI2GqTC-<-u%b;YaJKWqMUqerqAuP4x#PxB9NpGwl@K z4;d5{E>zDnEL7dOwI0^-hLU@-vf7f8LA3aDTWAoa16?kq>|OtdoiskNn$yF>=QypX6=Agq% zUP)+az_xD!)u4h8FT58%U|)NPlm=>QJpiovkDIKs5qf* zQd=}jLS~pP8=Zr!VXxu8Y~Wje8E4A|D|eO7V2zj@bq1OnFuiFFnA|;6xZh{z6VIgG z)g!qpUpSUtUEsYd<#9-%UP}AOiRp6K$Yo>mWLSM0TC{YI0lzT& zu-U$-bO4XnVd@XPvQl99SZw#s9a>y>T=SCZK}I_|i)dGC3E4;C6lm1{osjFr|jiaIn=TZ|bq1@30bQ2t0v2EwyQ|al8nz8_27KG!!3?eUrNB^Dg~n`fUL( zWwPseo*a~9POs<$w4~Rw0Dm#MMnr|Q962VS(Nm#e5Nf;(Rn5k@r(z5~u}!Q5gt{xG zs>0Cj@I8_Zl(GNIBOK)GibDUl6N}f*R@4oGBrYoNoI-Zvb{oV3Z@~I8S0e@ZOqvBF z^+cry#8w#2WpQr&?C4=OUOY%0oY%Kb6_jz9sHCg`I_1FiW?w8u*}AAp?{B~Qp3&^~ zej@uv4rmUdkSA2+4>ZdS37TwmUP(j-`uM>BWz)HHi_uJ|3t3~?LYI`;uQBrAh0TXg zId|yU-0~g{zm$`hZw*(PvkXf#OU!Yb)V|J9ZlSeBurHE2d>Z;XD-EMLSX6-0g<3Ty z3Bn%rJ~x#fghg`*MBTr1!h9V~lmO)C0eHGz-+M^zdZPQFlt||$EvI_qNlA^JuTQK& z_*MtG-Mqi6?f>HdtS6ojF5%KDOaNNSumlfYxOOk>16lj-%tjBT2#VT^+c9%8d)Q^r zW>)ssuSJan>BAj9D^!!KRj%xuO{S-=m#h;&)ndm&{R1x{Kr3D2@W@ZP3iId3B!X^i#KSJw7ZeFB(@?q`$h&6Ze|bO%ahMEt%gg+M|@?{^y!oued=jB5N=uK__C!@PD&ss`6#OYdiLbrX${cM^ZKEV&`W%MdM2Ja zqW0&^6KJvA*o$BlC?Bj(a8(7dfw!`pCJBRRZ+yp3nDOzo{j!if(eXa=5WCsScPVh# z$QiC<>a&#iY5!EW2=Ub}uBFP@58J?Fpo9J96l(-;OBmc=QdIN2=CcmtZP!XRPVOZM zLq!5?GC65QElz*;xZlV>CQ^_?f6{FmmbQD>y*&3#C%}o`)Q0hi{3uAHqW+oa} z;`gb?pf6bk?X!I-jfwr@k6iZ#A_@tFS5~IdWcNHhhyUshzvI7R5LH&`0n> zZLb$rA=q|&C|Yo^MZfT6OHcZh0;O(*a48E5>w-G7=ZTOEVH_*^Hs2mnZEf&@!`z8; zrFRX*3`VXAe9XZLN9}oQl<8JHZGp*NQmnibON9!($n^B=rJD@7PE_2{YCcZ`_r4@N zKKLk^Q)@+3ol$%&wYHMC0AFdI5t>iA%MX#&i}9c8;n!Swi;3fyQ<1C}>|qvk$q6_G zQK_1}Ou~4_(H>xv%>oQA`Kxm~Tw$RaAnqR!mntPT?!xgBkHY$gzRt8hO?~Qt|dp_m(#epsD!P2bCwl=A3f%Yzr-KQes0s>IQA)U%z-?}Vp)*oS+IMqqn z;nS~VF6;m;%dUAOpz2Zwr4Rk3-g!No1f4T|_!6~#!R7Q+V==Ru)1XyhNuUkm-0I^e zp#4W48IkQZW1C+E1<1g+p+8-OnjblyIX#hM>Wy+$uzh;7i2VWQ;62V{15oc?2EKnA zkN3Bh^0Zv=vpj_IZ}i11%|gdU8t`JoK`SYfae-t!SYfd)1htzJa4Q6*%>ihn=Zw;~ ztbqJS;e9X9ht6R{0 zxjZGj_P+aapW6nUDBtmtCN?Ov(oE>y#arH;!~tJl(X*!2L(;)hqwndSFI*|n4FBlg zASHI!ZnR=))uLdZMmQzGieNqe&1e6%SVsu_?Ni6_x{T2Ue$KI_s? zz0;y!(o|QEYNs7{=M}xwCrfy?e2$sUgw8hW>GH!9Svc07Gn7KvTU^=Wn**MI25J8% zpJ{aZK7tm%1muqIZM5w=0lA#LxB5H8%f={=5w&OZ>?T(9Nbrxt`OvFN%!j`ec47{H z4Z2uU!|8(rsHg}U5{6c)>H_Cl54wl(Ir_LmAzIFBg^T@sX*0liZqIimJrwqhw+#!CYO24m=7k4 z-R~51xX#zbDa;@7T1X?sMP6(d z<=&$xcYFQzysHR4k6PAU%{B9D#*=}{Wrv2#6e|t04E>je?(5i5yYZRSYLg{Sy;%_23$o~J=R(Wp!{Oq^)XsXZ7H>0EU*H$EaDvw5OR_0&Q`$YA zuk3-=2aYPbuP2i&7Z;3cnR1}zzh-KAw)#6ewcEd3{X85XD8wHx>-ejPwD`EForG<5 z?y(suQqroHwIEf}^f}aSpN_vWS!Oe6`cX$}*gnvzMr$YJ1RQo=XNSRkJX~BCoUd!& zZagSbxF&)BCg6!qDf5c9f`bd-a)Oi*Z!z0;0zKVYh4M2Biha@+oi6Yw8`*nxY8>LX zh*i#QC9{U=f%|C=mArb!0df?vUOyg>UU`JOMgzD1SaYV`UTI8VaPnc`WEL^nIC&)} z05!!omuZuz2g%;c=>FCm9<*$?Mshbdq+vCAEL-|-a7_w9!Bf#r(DnRRbLg3g*kyia38 zH0U9L=Ignq!p_(7%|1rX#Qqc(S1(jc7nTzQI)=$L1MOq(Iwnk#zxbL4q7cx6ukZE6 zk)ArK8-V5vvOO1mrMS&Ws<+kgjL7Fs1Lrpz#)iyFH+wp{ksAD=EP)pRpO6p1wbvh{Zv1`@V{ zk*@FXsu9=u&b>x$uVyFO`Nk1WiTNB19`4MAP)G^tUYxus4wy zn*xP<^Opj{D8^felUuZ};Gs;uO?z{PyUtb_>pC_WKJv1!V80e3ED!7Zb;v@mbZA4h z*ZmU13TjVUXxw6B#<*Dk%bA`x_ByhPtfo(lm|jz z7Fq4~y5B`=*_jIi@aPj_*bBwLpZ<7RK*+K;GB|^Ng(RtRI@4$+tG@b(!G_*RX#XTg z$gX8jOm?=&G0Skvt{x5;pfc^&zdk}g1I?VU|MgyUqF`hw0%L^OGAK04^OV0_3rD_I zmEAQfO;^b|iFhuDRvsxFMcUjnjX>!U@C^0o41sBgz+lH%p(&*P_xUr~CP)5)Q-7;B z0P+(NCvVyur+Jz4mP-~Axc!2gr*Z~8A{M%&yK~4Z*nW~PR?Pe@?mCoW$K9!7rMj{_ zmCPe`+4RQ?7fXfezprNZIMa>aI!|SNh97c3AKvIr-tB-V<;v2AjD4L}zP>k}mB5%* znjl|;Onl6I4%mY{fw(wF721H!)Y+knU2sZ$OTH_Df7T@MC(!-~Mn7@k^tY)cZm_*fHeifUvv$5?!Yu%;P@ee}m zpt0lp@I=0L$*Yqn_SXi5Zcp{j1Px$YywTp5PVNkx(Zj}|VyYY(gPE)Y$EmPpZZZMP8QlqdwDy;yHsygxfaxbMY@Idd?af}}R^ z4&x?RD19~R;Ie5)C-~q0v3LgTqv@sN8aDycR&h2Hc7XmkY=AEVJ(>AT)mB`t_ks** z_iY3DP{gR_e!D;x)PqZQYsTBNbW=Z`gW3zhjKG^=v%ZWhH?J$N$f41U>{H0)x8t!n z1}QEt`wAxC*pxxT3ybWF`u7tft4~h*^%a3?;}ta5)jq&bRhjUXSZMASMlO5sy zoZ!$)I(!G2jR1?Ip8a;@XKzwn(~0hv|eL2&emtj8{_1WLk4Ss)^X%c8x{se2r$%r`?~Ar?D@{ zcc?NmBjl)+1*Dbad#;pv)v(|%=( zqGzKy1}&m{ez7~<(;fvv<;Ms_%QLX6<_av47#m@Zt#KiZsD9-@9-NApO+?C2I)la#=+kLa)ESGlL zeEZO)hGoC^%H7}N^0mK_uo)6N+0+@m)s@op*FfpCXlI4Ug=c!#3lv&QVeZ?8&W&7i zUXQ`%#X8Q0_%`S5^5WbLA--oD#Y9fB?IcHx*HVwEnfn{v;Q3D~7TUh2i4=o(MV_pH z-FYt6(0z#PslhBoiTHFYn?ae_%!}*jM(*m7$Hgh5r^2;?XZen2A92B28S{es-R&uD z4N;=GTC2t6>L$3uXf2gJxj}UKH-RAaM}z0JX&&v_`newt+z6Y@UND!IDNmaW_kzb# zo^Otj+qulM=bP zsEXA3qv`m=Yw`6@O{1QM7`cr7l=nFpGtTv5W%&M|idP)nf)}o9QqdQ8VU?OHPmh*c zjNNy`{5~ET|KwpGT4lv8FuAW#7y6qXQaOw3zd!t)`IO2bMoP%7X02mj)Pg|-OvEGTqSid3)Jd`gZ{$lR4y z-28dhz}VkT*IGFp-}L4U7Ixj>89hxO6-Iy%4{*GzJ-?4iR_R97WV z!6*{?3W<$G80FuMxfD%NxH4SdlnE<(3ED@a=$s_HmK#s|uX8rr5-tH$Dn?Y=SOc}J z4?Sa&DXY)9VibP;UVoYIao%`}n#`FLf`rCBp^#yZ$j;DpU4k6{{s+pCb1?4AKd()fB`A|Ur)0|Y@XpYZnlp}d+tnR5bn7C^_u;OcRDJo zbTz$0G;uuS#tnBv5|`WMGrzc;E)BTRS_zf~+jIa82tV{-OKg;P^W(5bWNzSkRc3YM z?=pk_*vKq5aYbk4t-1RHd9nmv=HIK-nZw%euapqLO&x0(sG+HKBoh#gM{n@=)s@;A z2>O%jar>6%s0}|c-`FxoxM$2qoMk2xI;{Sxu{PszYC(DQ4Hgw>0m>`5)(I{eeLfD) zU*`ek;;UYNW;}r&z@BHFM|>K#(ipfMU{C1%ManH87TOK(PY?vata0{kTxu@rCw+~& zayfLC&fQ4%JGUdhZ* zW8({5ue)kv-DXia8AU546MN!;h#*ScfN7A>$ITMgY{)@>tL3lg$9(rDX{gA*>*H^8 zj+4xa;tm4r^)K0jhPSGbp1d`8Ulec)4wBe)-$%hlvC^R6JlvbE3ByyVZ|!K+G(H^i zIB*;XKsC9bt1|3x3YOIrHFRC+sN8!LFr?Uk7_hZ6(GoLJO{ z9-}i?PxEAYh_^AVjIAea1zb)de}$9`wH+vTZSH#)Le~8!Hj zw`;DPCqynL-~RT^$}d8ZyG0Sq_d0Mf_qfo6>}v6`V)s{wyTuvmwd$s?`7+$_lBwg3 zu6}hz2jVjuEpwmzUX@cAfKF>eRI-MYbTN-|BiNYSbUtlviKipV6VpEjn4~FS!4cDdncD3g5 zMlCwJngweLjknsgeI5NZ;Z9txh9K*PSR2^)FW=uf+%);pYFu)!3%u8ubso!>*sM`w zP8LFZ6Oj*Uz`r;)63~~6_ubJ@Di~LM>6GtYl{A3F{i)&7fp7-7MyOXy=|S_2W=M^p zzh8~2u+uH`CiSQBM*a@IXoj8quehOp_HcdX70Ae}n z-@ulBzVi^{RrKOi&l7@K(#joa-4k(n(b8Z7{rl8r_tUp`W@!=EjDa4<%b+Cx;zOp= ztCN?%ZK|JrUDENJaIINb{51t0dCXj#FlgkqV>N9`bx4tf?$w~`anV{4Sv*MHBU8(J zJItI$b?Wz?fehyc8}|dv_o{dV2cw{6-q9Z1JG%a6(WT=Fz|lXy@sn4vJTq-WrnE!x zmFb%-e~HLkG(N-lMCG-3y@p1F-Q~14d++C;H@#WU7aT6ntmSn6Tw(44RvG{sQcUU6 z3iIc?>t+bOwh9{RkK6G-TB;gGtUQSsfJg3)OTM?v6ulXl`sM+tSS*peE%puwV{5D8 zFPnWCT*!#5n6#Z1qXLm$0ZiVL9#7p}2B#l*cg@tjr)4rl+WigVw{E?W`a_btU790&|;Z?&jgK2BC`_=o?=Cxc9VJ4w%q429?Xg$JqpqCU0GWW$H#Hg$K*q=@dDyy%}=PWa`bFvIcySN-iux&AxewT$?3X zvn!px{T)kM&FPgdcU^|LenLlp;wtudx9T~WBepc2n(2k!^@!}n^t3?dsXjLn99y+3 zXW`GnjFcKV2;3_v>Z8<&E!ka3)D_{B+0-W!KaoKx94OsvP>H?1^zmTRb|3X(^mSlG z$c~Z9l^1Yse1CUiWjT0H&E#5PJM3q|9&^no&GcEmFXm>OoeiQYCLmA8ARrWLrd-0> zjT=X$Ywen(og?{#fks^VA4_?rdv~F`$<&tiVA2zDBqM76x4i3$jYN?;EkK*c*TY#T z!)N~|wHMlb;zfaPj43*9|p5WCk}LJE0Sc(j2}g@LSv3Aq{G!bVrk$+N15 zql(USJ($~zAvTh;50nhsLxb*klaHscChaGx%~J|3P#BbrTl`!?YsNXD*<<96m|kk-7sO@T(!3=!9w&?oU0Kb!qLD_0UT;{17Xk7b(BsV|l@g>ECcV&|1Jsl6coeiM~qL(s` zNi-6Bx;zHdWM(}A*9(RE%n@c*%Y7#ylVbL6OxB^JylOb2G%d`DM+Wc+lf;uVP>3^U z++^e5O)mIkht8#je2vCA2Pk)yP^Dn@=LQGF?GMg z=_cN9K;$LdFji&Pl%SQ^j|xjv>YD!X$M|>p^<8!S*M-IGs80l5)^ca7rroD&+h23V zg9u3I9_Tp6lXOX;MC@n}#&5A~Be6~wZ8oYds$7lh;7fw;^oRaN`bgZxg`-O@*E!Ia zAzRDLyc?!lVp;oln_vy4feJaP%dmR(&?Ub()X7)MyHaNF(N_i$LjIwZ1!E?q4NqNw zJMHEtWaaUA+u4gZ{ANdDCV`gEI$8fG3&2V>PM~9&J}VG5{N$ZoX#{tpSh}Fu_xD~J zZ-fF(0T_=RtXJVSLbm{o#>rayEGNcstMEO=A0iXrn4seGB#KT|wHeVY+f&t>hRZr7A=N^tZ5?D&u|1 zLLUWX6S-b%2YU6wr6Vcc+!pV70W`m2KVK}ZI2z8qgRSwiYTL{e2Fmm)z0|4Dk)x=MDw@n zJ2AM_G0_MhfPuF7*0cgs4in4q-ffM2$yBt@s!8}IHNp>L1AxiDHsEKM2Irhpxy*>P z_FKPQCSmnkE5gJ6IDze4kK9n@?Z~cYIX#&+Yr!g$s^>-f^~-zP2#(P0lX_o@R%L&UKtWv( zw#mAv5E@4&k>9SJ&i&PVI%S)nE{L@9i2qQi|54IMq+=oOT4%vmTr_xa)1&>^0*bd_ zFvePXuxG?)+1C!Qdf?RZihhpZkl9MRuOe1z@={p!dxE@VU+X+%z)b?0Ni5R!fgaoI zO7n$WM*Zu)rr4rE`kJR=SbY<%oH6JLhN)Q=5|8+fTG9z?DJU&c=i+O1!IXi8mht7_ z4n&8ql`)o(9`CYGwe$3wiCF*G#WYPwGBZ^CtVkNEC4s+9Lz@nsRf@gFU(_VE@ zG5E=5OfQNQ(+_a}*(^Hf$p`NGZD=<)Jr3%aN(`BDX`pMt3+3MTtlo-x{Q(3E$@<5PxU;p#Iekqs%}b;#ZB>aJO@H)c*x43dNu zy;V=JNX8dXxki;|Q2loo_YgsgViLs%a&4$<@4p*Y-4FyeLQPM}QdA??!Nb*A%+>RON*< z4+BYR&v&~ST>qR6W{cS#!XWa1)(^3CGQ4MF886_`}^pKH+%$&ayv$$h6^v%AAh zPu3!sRe{9&Ag^7t&b9=GBAz0UbnIR5!A2Y2wpg$mnte&Cm>NN8+n!9|AcypprtYvsKdwwg zV-YIEe+Uwn-n3|D@x%}M^=Ud(iltNIyU+KZxyFArYjpQ*N<#W|upwi9Y z?A89^tbS3Ly#MT&2`em9{8Z~QroV&{FzqQKf7XRXrchjKz3M#!__VUaMbqZTls?C% z(9esC6^rdEPIDg)<+zYvi%Ki{8+7Q$V^0EZk#Q8XEq3FwlYOUUb4@XzPxO|m12Q_@ z2Q^kt-_NU&3B6W4;-MsY!oL#GChzO~8~TmXohr_M>3VYAEk6F(F{&(`*LYr1OF?c zh^NCox{TG#Tmh6(XBX4D5%QngsS(ah?`&=PB~0oSJbm#ZSG7Zz&NFL|J41$fDs2aG zXC19<4+clKDT#9vpkCfq>GqpKuSP z=v+FGP*y&GXV%9NlNrQG8U7QU3N{~ekv0H{;7GmGts6mjGqGb)y+0YE08JO-rF5Y0ANJkO zD=Nw#7x)Kc6Hh$iH#FQDHzOyZjw93U%a%rO1lzYRSD#ovi90KdN*>y%(th;#2)U%#2s(5y z?{|)r$KfXEZk#pqFOWVz@c5w_M}~S39|o8J7+9rPyw&n66OZ3lwFyZ5Ndp~p8eLR( zSxhUH$1zg2uwDt{@Fo(7J&V1au}L!(lr&kF*$cA5j(7&bg}Cqx2O2WP6Xsi+1q}XL z3&;?%FqXJX(I4KYbnm!WAKqN_{r!G|aOxz+49%9!DXvCNYjM}}C)|l*P3)>k!#$V< z&=@E`k8qgg*htGcy4g14m)UUK6{sQX;&D~%bSVpEGxBs4vgpPRX5U=$7*EpQIxuf) z@K|;FkFe|!cIXnB%*FG_LI-X)_p_@p;3J4-7u8usCKJBxDa!!=5Cw$kOzS^xq79$* z3M^=E$ze_tHiHP5BE{0ha^xZ7vM9$J)XE*xp_JFxoL`XOD%N{T*Ltpv>sVZY3_p1` z`tpMEqQn@t1(Swg1&`aVQ%}Ic9e^-o^R%Bi0XczzlXEenpIw(06Ho=q1~9h%K$ezO zMmNJjTjEUyYq5Prij0}QhWPzL4#k29va2FTwq!Tv{2aT$Bej!{?3*tL(3Ufq{*TZg z$55r5=A`)M?<-uqJ91nT6rjQnSpe!}x?4M916amhd>VE#B5c|`zU`O8=$ZQMZi>96 z$oBnsc>fYEkSuPKv4FJzy4`-tdwQ~jVul(n`zB!44@7j?KD9T)j+#);KQ$q|Y@V0q zzwGuPZD&Jer!33yjDGANnVJi9y`n35dS`*NQ*Z(AOqCyokm=y8n1!{Sv%{lq9f0S)wU%sL<5G%$WSCJi5PM-EI<29r^VsjNK`!mR22{oz!E6L9!^1ptnE zsfUwwWvkjvI6+6V4P^`MNAIXnB?(gM{a`J!Lu~-CR84su5e_LqO;^<=7_kH&|`Dnt_aS&*S+AKiNBhavG zQa00ct8~o6HthZ#*hTo2D>%w2*&)j>%unSd zQ2O^BrW=6s{v;RR2jZuCF9NVHVHa3Prbvj|4DPbo=m|aXc z^lv8httgYREr45ngyGb`LXvETo5(dSksPLah>dw}vuO-Dwu>a_7`*1bPt`!Pr76*-BaF!jG1H%7)p2W`5=Kj+v@vD zx0X3E4$gG?SC^p-`bQf*jX9SLxVP*`NJ`-7)7f6}oEk|lC3OIeZ~_i8N4ym5{8c@fV6z58$C=k}`u=hM1!Po|$Hnl->oQbP12gJ8@ge&iD8p@a?}hC+~9pGA4- zM4ykW#+^A}dHsO%Ec+}&b+_B2J3oT5kO@Kkg~DcuZMCsaW9qH^2#e;i@GSg=7fl4 z8;{U%SSZ3rSL0Pbt&mWWz(YnUn(hERnUhWyKNcM?0g>iZf}#7ab0JuCy0I~!W|{vY zm;dWV)42>Xf8TBKu@*asUXg|vIaeHct!Vv__&l;fem0bwJglt>&XVcke$ zq%fjRDhD9_$&EgB zJOMYpFAGcS#Z*kenB>e>$)-|(u93O(<$qL%dw8xJn9HibCob(bw%6+Raun`nMa2u( z_gpD~?DFV5JT<{!@m|Zi3h{TDZp70}5pr9({xe+h(fO{q?S=NOy3(i0*R&^8HFELq z8S1Y^?BR`at65!$kI&=$@g}<{JgMdg?1Y`fVB8B+E10W>wN+KThygkZF&NnqYu#i@ zUhuonx$Dvse^=w{3)od{-Y+Aj6KXwg)$?wG6a@EPaNfObAq?-{GYkW+qHE|D19;4N zFJ7qqNV!ig8OFCKpQY92I^bc}RTVZDe>rcu{R5T@xG3YrgAR16N5)^~y9;AH=Z(9x z1Psb5Qz?TF)G()!l`H9uB+&Kl9w3xJOBUz^aU>)c=FnpKS;z*iVvvv=el{Ql)pG?qQ^y_v1Z? z?CzUdTRyg{tUZI#PpjV|(CuwQHtA-?vRh4?7kcbUXdp+UhR1gSSfA2x5(ZFUhvFY}H$+IRJxTP}5eu0b>sTbt00ZXTIp(C}sCS#to~ ztdG9nbkfZK@YUI{866o{uBfNsn6!6t*;_E-&Nk1u%gor>#OfPAqY-kqsl|8o zj`8)0V#(e~SEm4p0ag$$ys6w1kGrlwE8c4Z~rR1^x}@Cp~A(ma#SV zr)K?_r|1ks?uyoA6h;Vfr%-T{QTsb6UL5=X)j#=4MdOS? z`}%42K4}(Qs)Ql0r_`kVE)KrdnrbgWXsCq0hOB}YK(K_RyNpRP7A-|^BAerpQA!sy zYDL@%Z;aW2#>|aut6@+k@m$#NV&@UF?8-hOt&|J%-{=?DM#2r5Je1It^wIAN?n!PD z0oBEb`b0N@dhNc@R%o>SU#&T0wG&&p>hOSa?i`+PW2P{-Z*{!1dOtMpIM#oPHMQ}; z=@UA4p--NvPk@9({+K6Yg?QubjgDOBMzrk`Sr~N60uSYYogRJs8*|)md4m`{d|L)> zzdj-cBCm}g@w0NGSqaj*f)rh5B4LU09V=GHA~{=DA9P)Of*d*#*oF{I)>z#tT2`I+ zuK!WzSi!0h;lcPsb|8EafH~n+oCg$W`i8EV9Y?+;f2G8~~7XT1>JS=p;OI6&y$XcC{0dzck>J=S#2wpP4G#}}mZQSM@MF1qbtP<+o9-|xeDB7`5hM+%55((TgC`Rw z<-t}5y)_{2JxKe%YyUrFUpk(1-F z_6$?%$raiEZ5@Lyip5?Tq?tV{Rti<%GWEq^P|S}@SEVQ~_#bQ9?9jaQaQZ$+Y%8Ed?Z($o9{SFOC#$_Sa#MAel4xW@LNg(^%y8Y}A4tun zz#u7vetp%X_ONL?sy{DrVCTZs)R^69@8_(EzG|+di0JEQ-=pMGpX1S+LEoWGSGLB1 zWr&+AC_xS*j>c1N?Q@7uz;hEhe2DEe||chVHmCZHX^M=|G%k&7*xoQ&9>%J2ow~lUB5`DJTb%do+^l9VW+Tybn&HBlvP?KBmicDrF z1^aU^&b`0Ur8@_2fg|YWGaEs`mr?5p#h75$s5v%d59{y+yWay_zV0vbXa0i`e%4%hR_^v4#z%YzxP~5sOSPi{;*_CJ*XM zMk%$d%l67`+GZ|anncXnao`=wJz_y%*<0}~pmdcz)w)@2OVETwHTqxf6&yCC7&*5o zkogivzyBo5bBIj_+Ze%RsRJStO+00)(wO}n0HH*~FuhAV?^<*|c{~#>8QP>A(hXli&I0JyyccdCQt-n)3{Nb&6}oGa&RZo_@Fei^_pIVTo8>Pu zn_oklo2oY}$4%QuB5qT_w@F38)=;X8e(ihn&ETO%K+wYNF~>wM$Jg7c;fL=MzG?hv z)kH+653Ml6q(c#)wtq$+<0srPC84LyYFY*yM}X7lNqkhhbo(LdVG>G!u*nyWs2y+n zvNAz!GoA424&(eAzTNPbS6g9xN`(!2#0jb^(u_FyZGiVNOaG|lczMTvzvi=uZuf!U zrBwFWV3D7zXSm^ChFg@dmFm3&Mk$|S7mO2VaZVgI?$Lcm^3Vyf#}8Ud5xe^NQP#$BSXsiHh#je;Ms~-T09zQqZ>(#-xsale2EVqI zj{YElq5yUIg6O5#;l_Q7|MZQq&~w(R(u8w|Sj}B2A@)RDbZl|v70ep}A z%h6b-^Gv(ksBs29wtWCm6LP#4i^!a(rpJM@_8mD!kn}5wI>(1eeo8x)u=HShrUrRt z29grA3J`6lMW=a=T=cs$z?5MR2;M}pyC36z0hS}InO2FgC1aYK1!-b)z6ddT{>W!q3G9uK1kD8%r!>SZn_dP--vTkuqJqdaG6SLQ1&U1!N8s>yQK}1zor8o;cdI)IKMAzJ;3L+X$xK?P z%IdR>rLI}797TKkJ8*I@4+i(G15NAswZ`EDcy7GDo(D9YpJ+rU4vkTGyYet4X0 zF93M0Q}xZ=LC1hLT!;Y08T_S!5@l}CBNjoDLA%}C&#cSS#?c7>y}$jK4e`prgweYj zviv~?N77ub$L(Vxy&iKF%D6ctQc81(K=R07@Ei}swt99XZa`ktJfUW5PgnZyg z8UBe>%~SH-@PdNP8TpfaBAn@nyZA(u%FRbLb$h=bcvZ8iQe8%$e3H_Uia9Cu$!WEA zW4vbSJBrFjfG0MRX?e8a?|1dV*%XB{Uw4^`#DDOaDUcOVQb}(jcfss4fOz(?s>x&f z9V+;Ta{FlI8RN<#{{*9`VLqvh5JVGag_|kx?QK;8=fPpc{tuU%O~!`ewk8GGt6;;^ zp$}}%28Z`%FAyt&$rFWHQ&)Nt-^$;%nJR9)dgo)iyO5`+BX#@WlB&PBO_F;d_w?P$ zdUIN$l-uq}wCB>^tiVeng!RpNUf#>lZT+DW3-%#^T`L@zNiT5;_;I#kEQV>+fjQoj zLfMz%{aPu__SGBTs}O>7bRLx)%RETUZXn@Lk~4CN%!R16pXqG=F*x15Jlj?XjbZ%Fd>tZEm`R@prA!r-)@mxuDJGieP!R7X(|Z- zlJE#iFCInGf`_-kvef$}lqq-L(Z(>$SdOHcQ9>s@1)A8Y<6IzA0g|Q{%`4|QN3No| z^4)5}MH|t9`1bSzKy0#oM*ItYGG>;%;JBPcm9z9xz zZ~$mOa1*t9cl5iJ5HHAcd3HWtC=8&gi+x3luv}C= z(3Xvb_<~|FLJIF|t2(bAN%0{2RaqhHc0(FcyksKvoEuyuR+fE#`FFS3Cu|SmIzOM$ zEKd2@s@9G5EZ9qza9g^td4suZ0}>LmU}SLb&?Qi_1lIxi2xyF1U!Exbo{^9@_LK$D zmySHK`+K+$Cb}YGq{2o_c#15|CRnT}@0Vog2G9EUs+V$R0mqTa|A00=&)g;;t(^)d zmzgT(y^ixO-Z6WYx6>H2#`~KeBtcKSy0B7{h!;{%Db$s%f=>Y_uJT5ogp#te0F$go zl4WPlbJdXUUjr=GogWq4PKAWF@&i_bQ0)0vAPJFX&t2ZN^Kcsko?ymVA;$Xs(wqB#6{% z?G~}Z92czPuDsi2uP(|ow5eEB)2Q8t7awDFiwL|+pxN=rMPBTd1s#2{Pb_99dED2% zDaTKy=L3<59Q(BPNnNU8H){3hZ%f$p7*(t5F3ju`^Nc|Ns5xDeLRbk9XhkU^J+jCJXA3fj>0J=|~+@ znuVG?*4A8+&14Wx&?I!GSdX<*32&~}E`i+5YK84SyYI!$qzvAyi8T>XS16v$zO_rQ z&cObB2@xp{8<>sDxIstn(JOq_yYbEE3ZNhvDu3oa=+>*U*Xgg^LBx79b8d6NoE!hl zvfV-Qc#G5zE!@|F!zeGx@3TU>73-}EjG8Z6ic!OYZRL>(f8nDaQ#y(gTSDVLeI#m? zNo=@fb#6Use!scXDB>6(6f_T@p#>jl#|Qh0284h7{mWkiQsAxDvl`x?De;O>`n#-V z`k$|Xh44UoAny1JQ&h}2UTfEEH!e4*??iJ#mUXMIN|8f2yH5Q3Y1Z0#mrIfh_w_G9 z7{!=9!?-f(FFPgwgu8!ZzW>em9eTXc?m%-O&P*U|>Du>kR;T=kcy9?eBHD4dgd0qD z&Ej1!+DrY*F1x3$t8Q^7jKGgPX5G$$?Fr&ueArgY(XNML^4uzEF$OoPH5tfuTt*p!#7ex0JP zlO|+i*knp^x4TWe-PN*%BgyAvcZkTu`>8MY`9=Fg2!+QM>`n8YU#p4JnAbafq(4>rlQMcwZA|eoV6S=HktClkZwwSat#nl3 zI6*18x5#8O*yP?Zo9y2{^hI+0G@9kn4tyubdg&Jh?T8r|T^TFOzh7zAphuD0Fre1@ zYSZZzZ5AhxK?Z3ppLr@{ijxMubRG0P3o)aCN|@YEnk{O7?-mbSX1j;${{y{QrK>?B z{T2r?&1o%LW$bZyZ;zeyVZ)OleOyENeN#h5>wKL^FwBL96>+i_GCas0UR?2_!;ji!biQ2nlul*Qlo93lw;9YP zziy{Cr(_fha@ZW&thnFQ;5h$%Co1ppsM@=-?V&qNYVUCY`#T7b@!+-hDEJug1e#8A zJyh4m+zuv>y_`9U+PllEsz2xdR?J}jvIj{~{ao67=Pt(Xd zIczw$q)%!C;Fgx2Ksomf(5)&`#g~X$VA3tXXxR_-B#yvzriai#Lr6z?=-GhG^Fvvi z$vUvWAV%Ctw!?r4OVK`+3ggo!wIpcCFfGlr-ri#vE_@&5F33ufM|&5#EbsDj6;Ke6 z+yOiN;36W8K1+3=U4OFIm- zS>QwM0d2D|zx>oq@tuD1(X4hB97nFb&bbU)#>dz^8KCQ!IeK~=@YT!su+_RbGI9qi zqHluAe>2zmLY7l{+$?eBCq_TyVaWC;hb>*WKm^1z!v)EOa>{v8a5`Y~IMBQive>%b z)!g*8%-qlKW<2;0f5U2Q54cirKZ#jAcn|pF!g+~(mHWOcbb|&|#_vg34@?MITF@w^ z+;%cEJ;(JjpcVO`E-KBpASEZY99de?H4Ri7~$6UWkayb z)O2Gp^}vA`15v|cdXoD|%`1NUhvaVm7UWajLb+nn8UG@FGEqrsiL<(VjH6-4SI?<1 z*q;pzx}7`ev#!-^01pf7m9H!d-IiX{W)E6RKnh2!xt^zWf5{BD1|G?NPo{sedICPg z_o8DcaHAy)feW20gN3xk3u;$8rEFi|oUY89cidRZ4A+Sdq9=lo>Hg!y|36R&Zd$EoIsVW9N_&y5`MSLH-cZBAsobV^bCL5Vk z60-jSw?4 zU2Yv5tUf;TQ_d57Tl)ZjHeYIb^SnEqfRjxbt%yaOEfLu$m|Reh*DF>mPwv0!svu6P z>*&?6clpg{$ybf@(I=|>A9pI3i0crgV4lNZf5!{zf6XMHJKIB-uFX#tS4FqLF1o)m zwNix81M~$>yKD{ZV*f&~fA( zrR4mwC5Z|u#`ainLWv^_VxX7+z>Bmw-H=$a^!Khm@c=Vx;O%AAanjvYy~u|NtjjXw zo<+?3{h3Pu-s<6Dy}MmBR3O+y!wMmr|GA9#EEo87`5J9~vv?<6((qI_F~h$+pvNwK zxKiA}srUI$o<$B0voiH@=Cp?8fQA4RN$|?IcU6&%Bf=MaGX9V0YMI_SzI*ZQK3T@e z$Dw!;`NeU`{Uwppk=mJdb#b`^Yn2>~v5RzZ=u(cC>vaN{&1XEf?ZJ&-QRtEfN1boJ zG-`?H{^xobWDmOPVyU|1{CE#!FajY zWAv$k{fL`G-7j?fKcRJ|%r>8jU0y-t=5!(Twc%SFCQ;;@SiZC;ModCsulAYXX7kx*1b;mo>VtJmO0!rK=k$o)Dz@|Ee3(3u z;=cNdzd@ee_wPk0elU;-m+AYqwB)F?VY#%qrKP!f?@&@Y(2c<#{6A^_Gz{1>=Qb}# zTUa`@e(g=i?$;#pG8>=DemH8{jz{3qaNDFhf!qJt-e~D!faqwSxU=(sHiOMt8=aH}aZ^p^29#DtJhtthUbmjCW- zUcFMqyS#6yb#ippxAvo#L?15|wmRej#+Vr#m@wDLWRX>Spfe!FL^}n}<-utvE^Jt?X;U=BKY4Zsz)BKXchXQ#yi7edLg{@jF@F zKXq^7H{}qF5o~tR6x=quIWvgSh3)C@zBPO(I4vp;1mmc@i}YI0bnTjyKd~9uqKOqp zw0gwUT5-mFJEeT4v@om+|Fl1|$}HLV+Er9~p;>rC)lRliBcxxGZS(MFz|d^_|F~9x z#FYR0t8<;@3n4KVqqTj60x|BP*0X9$-m-cT+b}EsXSn|dUGEvxWZ$*@s(7I&MFb>t z5v52;g7hjPO`3`{1p)*_L`o>qL8S=#F+@o7GBXjwjWmYcz*&!$5I*1C^7; zp+eYWI8LI)(*b5lzl!A5d<0Jf`@(K!gR|$z$NbQF$yxyUeG!Hsxx};^rpPYF2}M#u7Yes z`}J7TjBf1%-0QN(;WKTgc;F}nZCe)3R-cw=V{9g5!WoX)kC?PBwxkIi7oO z&ndUQlvQQWSK7>^|D6`{kcoEuoXB?$R)G!JiTb6D6>N5DXEq;&z2tCbK|fLpkm~G` z9RgtSqVA&E^<=p)pbX)%Su{M5g@7b)qA~3pcS-*SVwY9Ke z=M#@o&5^2oXWU6NQkDi>eLdc0m~`E-DV5+s#)k&M4R_GF`mZ?7{yexDO~nX>UF>Tzt3CdB zBupAiGYS^2g#v14VCa+R!?}a(G;avPM!hBr!3K#8+Mo3fR$OU@#ww&)1fPv}?^9*J zcau%>Bzb09Y2pT5<#X8*uW}3XGM@(LX~$l<@SsDgw)TqZjy%ub4ds{JJe;R&Ug@kL zSG_sWT(aQ4zKo2><7Rpsgg~9135*D-2>l3+felsJpLXnF>^vzDB*j8!bG8uD78yijQlAJ}Q`gHDI6v$RMw;M)Zo{KNPY9>bZyxV0yzA$V2an=*3s(_9 zCIby6T`T%m2(8XD3FT>Czn#UUy2BRPIbD~?_rc}>zf@88b5d&Clkgcn@>NmWR@FJM zf%xw-a}LeZh|cVb(Cwnpw~4238Mm+0x1LxQ@Ozgk4PDzgH5*L$4P4pD94>Sa7@}8J zudrE|#GG8^@*s8Xo<7Vzr`?VVb&2|EaEqp!QNFy%w`8n-RtvPw$tiF$HK$z%=gBQ@j;?4sft?+167zfSK zi$9j69I5Sds#@mD$+XhxOv)|WDF9wpM6m*Z4YovDoib1=LN_MsN^ppa#@l`J9 z-G8@ko;7`g%iVi{`o}YH-SunKqwzXG%RA+ zZ(99*Q=>wRM4)x?JZrQ?gTYtR`Ou~<=O=Bd(LmX`1ln;QX2tz6=V2xZ;9VM+)rvVW zBUOXLUG;FLdQJ3Y>rIuv9>Id!)mrAxpI%Nv$RKD|H1106f8@u_X6$$QT=$l#X_Y}W#4DQ>Qzu^}U$ z-FO`vewSG$n)t}!lSsPLb?llE+GXtWk+$=QKl-sa(=RY0E15Tecx&neOz zJr*}Ly#N!IeBm>Iky$pV8zhD(AM?uQa>tW$fJ|Nl-DQ+J42}lVMqHE)XT$BsxCT5h z`FrvR!f6(<;4go-e9N#Z7D6rkz*3{z`fYTB1L{@bKMQF-lip z;bn|82%jl^SHRXdd47aeRdMNwxhT0Q%2K3v?Eyz0H&gi3X_sX9R86g3HZ}+xsjE0( z_h3_G=%Pu>>#Afr=!$VGfEJn0FP_-wMZGB658_X9m;!~BS<+~*QHx?7p%-ODQ^D;e zb(!xId*2CqfTN(XAxM{%P9FNM^cz~h2>>5{C!RU{cmLS}C^xSRfPJRYQL`j6B$sIy zH+r`U-kAQbC{ypv1M`8)(r}kn^2!TMONJ&?N$b)kwPi=wv-P4?asq1E+VKEc#*I`t zl`wgLiaRL--B1yQHw?iMn-np6t8+1v>iJu=_t!#xu2%CXo6KpceumiQh%cyER=Rt^ zM!MMxPW}Z`>xhb}vbE)RoCO({P7PRWQY8|;&GzK(*f)lnTJ>7z8)3Gl{hJgO`b4T| zrPTpAeF#{X%igO0@k+M)elOQ^)xB5q$1ajM6?dH59y_9zzHy#cvub)N+rqN>2zwpO z6kuAzexDO-EMhINGpw+lMW2*CtEpqFe$=4YFIof^c8w{^}FjSIHvJNF^y z>mT2sV;@hZ3aPtzf`tZvT2Ochm~_~B7=k^LyB6#LsGASiRm9470?3cWbXtKy8vf^$ zz+yfFN~v&f`Jy$b3$3Jd#je?i)BW7JFV0`*tq@<(k*p&dT8r<@vNZEZyC#f<`YnM8 zsc)5|qh^qlcgJst3Az$+1UE270g|PX+jS%2;IJLkOxf~og`D1pcR>7|T}x9`ZfQOa zMwYRuJZz#)IE+d^^luRfl0K;ey$mM&bkX4(9HywA{N3#iVN&7_pFyx1qOPR@0^5Kq zNAn!vGrB3UtV3vYS}2S$j@%`r@^XgwQPDPR_s6rtBROGea3gl7Mi*rt8xhZrhDluW zN83xzl}m>|=k^EfdIJ3@SUW2l%l8u(}@6S~|etMIbbDgVP4)hDl5q&L+3TJanM_P)XqeM!iel|YZTuk+Fr zG`j}y(kc*)R4QCf%PZ4Dut#sM-%%3LBv8i1y&bw2=@T1FP@Io!?U2PP-hrkop?!tA zCJMc^IRr=N7qeIu)lK?PM5})x8}dJ*V$>vKA)m4Sz_WjjX#ns<*)u_u8rkQjy7R3` zdw8O^2TC^{eIrAKzb>O^w5<>r)-F@xI=cOu`|%da>^NIaK0DW?>E zZ)b}^wZjdBE@SmKL|p+mS-<;u3SNCJ_K}iO=72APW+mE?oT-~P-??3^c@>|5QyAfS z;RXL4_PhwuKz8XF(ErvhZsKWe>=KmwnVqrqy(K5`UUM2q&PVIiV!o<@=xA}I^8?v+ zw`VB$V0SN3f1-_iJoW}e*ZEdTic^Z0$}!d>q5%&Bww<@BWua6k$p3zjOZ0ei@U6A& zhGkdm5SchyDs_8im0DogPOxY+-Z}gkg$UXcSSwtyxJ~`$1}jU4CdFrDr#9;X8>Iv- zgEBeqatd)qdJM|T21e>G*9vm37%zUiqMG%~P@QQ10O;WAXGBi);{n1gZqup@vz#{w zexIO+T}w1{=hWj=rUj-;EekQN*X1jER_LVe^_1%pK>u};$6ASw=AR!uSzPYt%c>Np zBrg2svL1bdlUNiK?xV7sK6LAx1UyfGCsPT@2{lUxuR3nHb%uM{XrFba-5|RFP?b}C zTQ!^viQE7}r7cg%7Fean>^2K4pgxl$k-fCA#lCN)W=i%rRl@vbsbzg#!)ix#lv7!Y zAcy$_N(407l%&S4%I0TJe>Cs5%;0G6LT}PaePj*0%>+dsTltKH5l1)9g*d=fr2DbP zb!}%^u0kOU)E$EAc2!|?QTrPc1-&$anp7K~YWl5|Uk$|EVRdDYk6m*@40yobR%tui zY~=O)T%eG{<5Am?B*Lu+-u3?MjAm_`q%NO z8NRbst;8Je#CE+P$u{*@T8bsTe^e34@4*XHMR+%1gqx+P^bsn5_YEL*gfjBG@)IC@ zMo(%K6@O*unqWo-ePg<7yWE#LR&^J_AL&fFnfT11A?#b|Y3{{nIxraCUMrXg&VTKm z*?5P3FulvZ00d`BPrbt{&{&U-YS%lE?1i^0c-a|T5v-{Xu**YM73zJto+|-Z1&_q? z3iiPIMdn!emYy;S{<8U%e<)r*lR#P^Yn{M9=CETUv>W{Y!vZkRJY0JEyD5`X>?gbL z#qpsyB)axsEd@snz#j&v9l}b^1?dTktgUhenXsex?0}`-H`Tlb;ao#q5$|76 zy)`btIhMdeBg0v4NiHMa`Liu1EH}|9p3r*c-}wPQYx9_NaD>NIM~i3376jq5yk|K) z+P!1vGfXYn+OBz=y@J|r8$sjC)kh2iYmd5zwt+#O^M<)CT;dX~t5~Ta>h*n}W zPITW)-Z9klryjY;^XEkJP>_fUZ*x9iHb||;euCB2cSCyQfLML7mxW+Ope*D(R>laF zli)0P(L50&1$#L2$Fd1QFWuQXus7e+llHN=w&AraXLP zw=xgC1pW0YI-~RJN`LjBHX+QIp2}R-8lgRRT_44k2kpKpGmI{_ISPR(QVS3wqLh*H zxDWKkm-@?CtcyPNOm7h~c^2$V%xOD&dR)r^PxrMtRiG~i#|HhaoR9FAQf2|wps&&` zAK@!8N3t)&XZ$;Re5HcgS8}6)aHbDs40Wd69uCgyR2I`K@8F^%dq5G?Y%r2?lEN>G zKN)E`WJ7dSfApqu+uNs9qI3eLGklMBa$=mEtJcp8i?iaCTPeN8ZZc zK)_;oqY)QD`N69>^PyQCS_#VS91fYc$klhel}>CmTue;Q+nIuI@TJbN@maSvHr%6c z_YN1o(8gF}uUp!aHMJMXRq(f*eQj2o`p*iuauX5>f!C0fDi}hnE=J*_bSm zEvkfE(3Kl{ovo^L{zayMC*|-`as;VKPv1dYVqmY;6vf0xC zo-4(JAX(cM&Z^j-NedIl??=#~<+2y|EXfP$8sa|lU4n6k*4c;6K+^SRJ*0jDPeP^9 zF&M5+a(TjMww>9m2z@+&nQQKZ-Y`IZIJ8oExB^h#6B%w-ydNkxr)+-u5vD%pNdBCD z_#6`f1=G*za1`Y$tx}JH!Wq9qdv2B!15EeZ=>U3tKa5&1ufSWC*DK0^pHJc4V3bK9 zhvKh>EZ9Hvzd3+BF_o847i(;K26EX~J1=DHHJ|Th8=OZF?dRGm7wtnt_U)TL9Qt8? zp~sq`zdrBnz49&pkR`LO*N6$4&L8~wcx9%Z-#fwx#7E!Q*z<>W1SGw<6!uZ`6Y8O; zh}-V&Xpxi*eJ+fu&@b@%!^`u8TGtl}c{w71PRmY`QScE9st=*gt0(UU<-dAaU zIP2awu=dY(jSs&$Ux-71IcfCB`~|#{l|1*FzY2wM$^}g&E2aHy{7dRf z;C1n94YhGgM9O@JMs`ha|W<|P`d85)DyJtG=@YsT$Y73T;r5He5& z89&(7UDishOg(%9i}eov2MhXt)lA_T7g%n>J8i@w+IbS0raM1xA}MP~6hou*R%kg` z2oC%=FtJh<^>c2h1AD%)NqO7Wu~{$Cx%Vm)H1TBX1jd<39kiQA+g@`hY7eF_8;ZJk z8UtoLj7FZ{Ijfp)4>^^zil_a{A=He+>PD3Z;6;fKvXHT0w0qdkXkeq9{rc03uc~gv{6Jka=?v!p+FA}Y5|UG3}`kQVCvtpgiDKFK)MiN{|Y9c$!2lx_7TG}sUeY<{^r?sYcIK_<_hGy%^6 zqR!iXwx(hM-S?0BqDyR54p3Y!dbI3>P&Krp4UYJ?S}bnAXo-@>wIZMhw{)?ghN*rt zX4+q(CZ+I`o%*$jOz-Z~ElS`OED#*LK(o!X9RDhBzP8{`lfBVfV9-&ge*(7x_;TZB z9q1jS`qp&`IaPRuA%eU)v6OwL+3$MpUHR5NV3_)&M$wdQ!XnUeX4EN9Y>0hG>Fgab z0u5EGFaw`tdT2#cshPsZ5vKWUjX-+!txGEIbh;1a8 z2+HQG+*t)h4rKKsd9OghsozMg!0i@-mLq@lbqS?5In}9Jo{{k3)S&xQt*S~Cv-M~! zqpjP(_s`qDLy%h=d7CfWf}DvzR|U?8n6_iaOZec)g^abB_m!p8QoewNoH4_5OMu*i?Ey5R#{5;s)!@Hf#G0xl!ywXfTC~+o=W*=VV9R04 zwmu47(*~INJGuKd)0Do^Rey8O-iGPM=<0|J`u_6a`S&C9(!rjezsf9wKg;;}mwKMb zI)=R;R7B0p9~}o5u{AohFuuZ)7GBAG56-AH7*1j4n7sxUUF!Yi3jS7iEQ zsKw>U=C)ypBa~*xt)>?MZ964xtr*TpPd3>7XTfIRYxS$IB0T0qd{kDJFirvjL-u*r+6j zOll@zlUU(tM`~8ymX|BDq;K@iwyFOm-Q)4kG#$L+o)FUEl2?~0KqUja`?sC;BU6l9 z0R2c!{QI=*2j8Rn#yn9NH;=bNT9qqzZ*B(G(`=G&Y&aD^5b(IqvG$UgDHhC&DvLjK zocpKJwabhn&vEX-8iY8)w*9d&k7<_*zm$nVN~-jni1C>}(Ckr^-A?)=Y0KX0FHS8& zWp<;vaeZ>QkddO!-|>ge91nLXdg*{pyLTqgl;%Uy+;>R-(rgmd6VCm?z50&qa(aMu zc#HsQp;XEMNkij`={-^9%a2cy+}VnOo+d&1b2DFRWVl=R#4JjqaJcppMHFH0iW{Jf z+A}-%m&6J3Dg*3{gysgw|J*{|tk5r@jD-*{-opRV&Y}ozlgHR-+^}HA_DjvwoIm}D zY4>LXL!)Aima)1^>A`+MXu^u@_m0A44_I#Wplqvz*&0D3B;x~@282mC_B0MzA})*j zZ8$mtfO$MO;k;%gDFTemh{#Ag9^SFOe0IEX#YjEQ1jiWSM7z_l_~vU`%|aiW)w_nq zi84!e;84jDo?v$ToHMY23U-!nsUY3yy-p8QH|)Nv_f`yA6@h$YEB%&s&@Aku{UOKB z@&+^Q_T^05;=cja@$X-%@!KHepwtKb zUA*+o=aFkDU-*7Jlje1RDiqh=z_lC$Y~HbjpEy5plxR&umbVGUpl8{aqgAiwu?Q-p z4O`#lTUHa~1yxvQ5+gzPZEZhPD#LsKZB5!OppOI-`SippFpO)!hJ}u$ZOWFqW3uTF zmyr0oC7LSdryjj&2%?Ul?7vpS5>PmJkgSOHSrt+x7fbQ_+N}Ozwkc)pyL)*#r@~r~ zr4vT#TBV6#zMaUw+KIUq)U#A0y(v0JmDYZB+>5I(9)J1W*_!R0N?ML{&KYzmDRL~s zrAcI900haCNjbWw}?__D&;(i=4SVs%^v-^SZO?O(10xG5RF z*`UjdDSD?D{s7q-z=B7A&XDI>nbuaH#75Osc8K^se-A%Pb91Qr;y|}+2^g{YVBnXn zfiooAblHIzIkE%a0oK93rM{AwF7Df0;eL|8buUt&mpt^p*05#0jEXEnjWt}=>0KJi zv(Y{&00!4K?3THX8bziZ-MF*znEZxOeIbzA#`Jx1>?U`t%++D(V>3yZQfX4&UzB-g z#ur}hAAJS`QjR}fiT2-W1a8%)=JC)8Y=aIgn`%zO2(v#SI?i;cpZ>Y7bqZVainiT^ zw$nauG}@ym6t;=lQ$wjUX-uck2L|)m)92x{*oz>KCpjq8eP-N&#dm(Z_c=dn(>_$) z?5J}t(opnu0YnWS@aUGfL*JcwmSW)ihh#;G&&d@)(Xy7~@2mALz?7cPrlq+^lT4-f zsZ3z-nrL@w>rV3Obt{XCJBPIsR+bj$D^9s84w}Jb!m$z0n0#9~x4aQboX&mfBjGIx zY2roB(G(m?pKICl~RoXV&a z!jtKc)UM!C>6`5#rCoT>;TMokqeLyC20GLgMJToE`Mz4?UD63s3XHqSEniYfIm7nZ zb9o=HO+WY7RQxQqDTSITZ+Vh#6&=<`xfAI2d4z(!;D_s3G*de^NbeNGXAeb zy@t)YL$(M3B>R%@ql^p_2PWjkH7$9^M5gewi+n5G3@(`^BO@bwC5np6lceL16P-H* zB8es&Liv8=9KgM^$h6Bm3SoE1?5%e7vMABYwG5W`ITKMXCori>ZT|(c9kU`G)^~Xy~>oG~i~1pr2-*Y>EyQ zdgL873L6<^OBngG)hvH)DBPXe-q1cya-D57%Ixccrl?p1a{Xv1JW8q<3Rb`uA2sbx z5pvGC9-yVi1+d_*W zyLW=;$h{%NtAABR6d5@<+;`^&Ppa218_vzHZG&SU&USqw{)6dJR9e1}6aoq67bh={ z8Dhzwc<5mv4KAK7o*Ps@1dRx7QbR?fFsK!8Tg>UM8^(E+=WyNQqg(sx^$V7AO=%6c z8{-=5VyKmG6M9suyl#8^I2V4tjKS(YC0y=%Z#56lTb)%;xLRd$a=*6omTmTOwC-Sg2!3d?-C=Xe3l`#{&v1!_TPkYi zh^kDcf_Lgv7`}}Nk1Xs+9{aIcZ9P4BnQ#Pu{GNChx5yZ zgpWmrhm%GtS`L-B#mi#YrPpKgnC)&Sg~wjhrad;Z3Ak&eu#fG*F;CoJk&psN!9)Ih zkUg^NzSG&sl_~5kqw!npiJ2(GVkm$ed^@!Fc$k>*<_<2UbG4x zW#e-i1`Oa2)b7AZ#E$2a(?WI3sZ|mmO6f}YOpQlB)cQmJUGSy^OR-pN=L}Ov_W6O1E&y-+D8+$3u{J5W z*G|x5M1yyXFJh4k<=?LZ=2M?cn}=4VDJ=L1#$^*&y_Ps0b8>6W_OCrP$rf52d0b6D z;@Af^bD+faI&pjW_Y>Qr{Nj0+Juq=x{u`&OehKAnCxRdd-6^FK9bF!H))xJ zSp{s0LVyy@!c&RYCE#R&Xq2pFpvGJF8;Y*1)q<~*?mJtfl6R)ZGc^<_^+wj zQf>KLLx~QFBS+=Xd+ym+%0u(!Y9>n;T3ybX-k^t`@V#l3p0W?x_dh#kfEjof9jo3P zx_nGTpw@Bcgza+)4B50&^0dw~>uU(&pKRZi$)#4lSNF~+y_B&$Cvh)z>I_}4}Z9*%`vJ29O*8Quel*UI! z%Qv1)Y?^W*Iu=P5e=Ytbg^9QIkgk_l`nor!Cz(4o>NGAkKHHO~l@*i~c9F8O^=&k2 zC6Sld>*)Q~bYrNq+u+%_0JZj1X1y0*S>&zmb2Nl7bIA(GD$+P{isgM=K)wD7Pn9;Y zTKFe3;|nU9D~tAHxtd%X{TMYu{aFC822kf8vj=5;cRrym2*n5ipC)B#hH1tnb1%T5 zxe?*lAv6&^OGh2FS^}7_d^E{PJhGo-F6somy^!b0X$df`Z`#lx6yM)~{#Q-uo4~II zo@iOnoZzw?%80`i`sg7v`$=CKYa#$nu57xW$O57&Q6zw=oy}F+ zz4ROHe?EdAS!Bn8A1=ieDCs>1g`)wY%RxyIFl~~Z7&AJJ$5*nfH~%`+Z|n-qO)MCN zO1n!7hu?cH!3~Ym6Mia#y3e8enDGoQV9RYF4|?7eMB%xE(nX1yw!c z_@M1@XGzp|eJDa<@JuasgE~~Bwvs+k+f+_!&@+}yL<~@6(ZW0%TvDnuH3voZy+O*~ zoHC!{;}9LK8;%_r1l51+{Po3`jlL*d$^KJXul0(N6rqFKMN?*}t%3BXgRXQu7GW9g zul*V=W4bBm4E7MHDgQF02HccdObU6QT8EH{BV5AYi9JYZC&4-$!A@YPq4^^5JNAW*5lXcC~Xhf{)_7R~f630rNcVyk>{$ zkn_L0MK;RYu9n`?p*J_AtGCw$)S$qJ+EX1{51JghwT}$Z_j?ehTTGSMz3OgiTP=!a zWLuHrkmHc*So7NWUFh#R-+Jb#VP}%ndaQ5Seb!WH*5=~1zR_|k90hEtQKUEIHAD%4 zx%duedu^q*wzCw0I){qRR}UqfJE??OOYWE|q#R6gK-zW2^GW6F{V!vLyXWE=^|y*N z&tutYhpy*sIZxZ#ciD@pyL`-Z1+4wsC?xJuySN&X`R zo%1dJhJe|w@|SHp&xwx!^Kq1(XSY}tIXzbTMw`iX&aV=gw>ccLdBs8?az2ZU+;V^t zyX|Poif(tA(E9?JD6+pvLF@4sSvJ`&;IGWyCkyS{+It~LgvY^*2OelRTE-(+p~_JS zjSF~kx->vtKAxL3pMCs0Q)rMm(C9DQDpwn?-$|dDb%fk2BH&xr-J`+}dE8p5bF+|@|##o!-XkbfPwk%*Z6{R>KI8AeW zzE@t{_vcUKQj=5pb?8yaC>e6=uVaY0dFE*XKkLj=diVXA5|c79nQpBwHEVl~;9mrV zyR%N^oBBh{gNoN%mHEU#r;S_38y$c*&>bsgG7o|<4%HdX~?-hOn(U-X)1a^aMX(eF8V0=N41=6pz=c-b$1Z>S~ z{mShJt3NPy0L-eSmp2=|q7X!Pp8)$a#8q(6xZ7AFG;FzAVYzHE)NDw)9{7iC{rI)9 zv#mn+Ajk8`!^kb88+?D!k5 zm=FB2N~_!8_rC;JFh#xFCS@Xls-0Bxz(+xLKH1BfT^4$oZZ3ej^~kY*uxj>DspDaO zG*~ul&)w)LqXgCj^87fN39Rd&-dnTruiFa;U+pLJYo=;Ss!h z&xB(5C21p-mf?D=-wammGZUMK)mw%`#QXs4cVl&KqL_Olz>hpZg1ula{B59TrD0GH zs7@#SPg2n-YucIPwV}t&RNzv@g{yB=BEL(`9@cpEjBE{~BqP-P)u;8QL-MjlPH8t; zotd0vHW@crH-()!7rRDPMhr$&Bn@-r*$38!Y2TKGO63;>^x!weZIZ$czPNvDDHtfw zt0Cs+h~e^%pUB!j^W9e(alFUlI2;*7GWJ`4!n^uRQI}&xEzm}t6H5|X0%-ZdY7YC& z7mWI#gchy(as8MDB_+@k&-hDGm1SKc*lLzs#fmZ$YcG~HaGVu4aq z;#-c~c!$n~R3D6{zvt+u}_WBLftW#c=d9CD1b?1734nP&*`pkyD(OfNHsDxOl zRl{ClQhCj5&$aW2o5pjU$2cZDPcO`SbvS-0CIqh)5OK$O5)xAcPEXjG- zMD%3IarlN+B@;`elj0%2P$S`3l>~`xb|ZtSDd!>=-6jzs0$1M7*$LBQo^R;I{Afsv zIs4h-S2nyQ=VC#-YJa24n;}QlUc#7>uK&k8n9D(UyL8)B%tyJ2@!#UFmMze`j{gLn zV^c0bod&_8^+tU3t*}y3tDjfzJLu{00E(eB+j7dpDH5U?Jrg|Fcz7*ls)=~h1CBWRjt6+Y2>ptD)iM**d+UiIB<|4ZmDA7T*hu*u=blEnA^M)FW@Ze`(2{Pi4P*d78doY{QNf9Dka5rS6A9B25~;iYcEL{tzaE6Xq@Fl$>pvGc zw;RjW>R?0YZKl3AB?A}b3eeP(t_i2x7O?g_APXU8Q1zt5?X-FJg*i@FNG}d*l(|xO zH&1CjfTVV%6)T#xoINE|^P(}YddTgVkbdM=y z@&&!+!x7QV)d{gs>z18ZR(HHF}eeG^W1SLmWR? z)iD%*ct%SyznT2x@NM>o+^4Qd%t%wE4kKP)3tg%4R8>a@_mPfpg>`xPQa{%2^o7x= z7lAvlijLLH_Y&RRNPC+JY@McC1DxMbSEOf4TCS1Nl_v#Fq$H&p4W2v$jP=o-ed?#!Me z(sc4kFR45&d7puXJgftt)$gB$4*&2g&IYL21YCY)2cjSKc#V$bS(}Ke0q`{9$oJct z=|g79SCFaBarmBH_jGs8{S#wN?ScJ9kM#8+W9b@CbN3C zI!T#!thC}2SQc`1b)%>0dP4|CWTfdKZsQX6pFVbmfXT1G8^SDo=-ukvCXr`Ty5>m} zNgaFqw}C9ODRnNpFItirA$p{WrodilyoM5flmVG)NFFONPRE(<3dWEG4HR$_y|UfL z_9w9Z{k&8)D+-CdIcT+>@xNAP!+*?y%U1a#K6jvAUwl>{8~Kj+8l%@<@OIQfQe!I$ zeUdA`F!~n6Ly~(DBI=G72^@8}OOrvCzR3}%dI1<0SxKXt^3+bF_1ul8uY}qx+sCtq zCB|wQuZYA zz^UV=AK$N=J2$#?Q$2N#OEi#YMw49j<7*)HUYqnp#hHC!+)w8?jGN zU0RIzG7bjayvrA#q^wlS+~cIHIR}fw=68q`;#o{4d-%ehBef9GM&-ZT8B3!5;MbJ_`Y!mwew=lMb};mR(UQgBE}{!&uDORvChy1^i}vF7P12u4l7Kd=hJ@1Y zlx9H-k%*jgvl4Fmnb2Fw&Uf;~xX|}A?96nYm%OY(K#koysB+t_Fom~a1}6Bia-|#S zR?lZH7QJ>I5(2=8Lw#$Nn+u%4X*k);FLjc7S1Or@J><*1YZ*!~!d7RYO3t$?Ya5<% zRxsZH<6ukfvnw6PK=^yQ{ndGVpzqS&>Kkitl)GNh-*4Z`1R=K%P?k4-5hAMZQta&bG9R3YX*AO@2o% zct`dKY{^#iG|s~0x#7!|cKsarAZsMhcESEQ8hPA~jsFV_p1AHA(^(hmnpI|CgDFW=>D7VA zlJeW}c;lwGnX$ylq}V|0@Bsl9T8(e&i(6?MSGt?N@s!LuF=*<0cU1RfL8a@Z&c`2q zB3{K&FlK-T)DQN+lr&kyiQy|t#O;E_hewXatHU32>b8LNm7{NmndhrYcnsUY5)%R( zepFlD<>l~&%6XVm?L8e2Vcp$cEyO*VWqScUwWx-^UZ?7D#n+4mF3Za--OISNWXM}o z;dsRQQwoJb96X3Ze_uTsolIyw3gJ_py?I5buC{pgdz}Nf2T#QaWyKj}^8~FjjoZUQ z9u;G9Q+L(A5I6FYBkYn@TS-8V^=(M9;!r9GU$RqZk(}*gXFk9U-8?%vWcNS#tHa2Z zG>{=H1a)4I<-T6C_>Bwg%j2u{5i!T1kepNe?u_14IU+~wdurdmCW`;qDF#|Oq^ow7^`69Mv;=L_{jI+k~?mt+>eMBCLup&=-#_-8~oPecATswIu!kY>~AC)))W8ju0_ReQ_0Cm+?r#a-@prVOr-AjBnrO zd4YR|!WtYIc@&ans2Ch0N+^+SC7gllIVDLAHMhFz8G=9FAoVZ%PfmnE#*4Z5EW1Rk zT1npuCn}eSBVnT{6KrH9{p{2Zwv>&L&}d8~z279sQ(N}{ZTsr#SG?^p1H)8ksghta|j2Fp-@|WmKa+V8Z5*5NcN|jhm84?ZF z@MbnWPf{_T%PRuN3FVRagO4!}7rOa0z_0m;_(yi^;g;XW>|;49E{r zd*p`mpFSyis-a(AD7BdUok!Ea*YT&k_ZWY|&T$q%0S}$Rs@VRf9Ew)@7G0_?TQ#Y}H)?{(T=1HCpRc_#;a~mic zuUY18Cp1Zz;<795{GRPp{`CZPq`Nv@y8=zuxF?R95$d=jKPwu4HN?$Re@4`PIuSeS zdX3#1D<`uFPR#VY!R6P-&^A8*WbI`Rb8v!WH~+C2-OKyMTxZgE64m7K`jS5~^Ivjo zG`z^dI?9q0`e>>_n2T5HSZ6T0`$Y!sI)Xsm{M(M{nY$kz7wa|;3s`nNSCv`opDulF zS}bIkxSd$mlw(9*DB8$l3lEaP3(0rn+C7`93gUK3q<0)HsOb`Agk)eIr;!8<)^>~y zll2{qzQ~;eHgpZgwd%fkcnQZw^)~&)*eL-46cdng_CTQ$P{qwgrS^ zmE3*tI%cXeq>(~NSg9Lc00O(ux9Hng;FNRnZ?+KLsAfjTbx5Tzv;Vs#;^n(f$tl_Wm30rbyKB|c={&rx&h>JM5Vl+Sxdom zbc4wJTlw|aMn}@#cm`Z5wA}y*NV=}or1ZnCpqjJDXFXB^-+h^OUnAR%guNW)sa^N| z)nZR2MBgx*oI+6Zu~Z%o$>Z)ZwP!Wtc6rK$=Xb>`_B3BH)*?Xv3-JS_bQuthL5p!9 zFldaFL2_z?!s~7yo>;dSvQ9R!Y#orf)bn#Tg%A=HBmX@>^2;DTADC*Kp2VjT>-j_! zvW6EZQV`A}X=vJz)9w-9VI%d5^sBOP+^&}dL2Xw(Jjq(^CRc*K9WT<%)sA9bTTEud0nnoIEUCzV}A|g6LJ#Il={`ZiYAIk1D54 zA*sAVp+3s%lD2!!Gw1Jgg2&1?O`=lnkXvu!ZN4QqbsKH7{HVK;@?F5*STVoOeDm{q z?(x?rRgK7v1h}V8*;LTYvHB_5@3j9kh<^RO@y1W4zE6KBxFFe(bLq>mg5O+;Ylu=H z%Q5?th_cf(w|&((*QvO~AWxhocoTm6Uv1a{OiQMj6G>vaLh{8FuTn`WB-@ zQZ#OLgj&G$P^U*5eHO#mpA)F~*VZ6hw>prI=Y@XMZ;kt^h`WZF3dfRng;e1UR60^Z zkbwgple`S9Ssh{cvhjARysuuTj?-Y0w8vd-+pESHGPM)1S=w4eo_n$}i ze{1IdX0@N`K(P@YLs9MSNL!Cg*YV{5u~^tIrR++Hr4oe#Am4Q(vn&LoKBIOjihcJq zNhWomFP|tIu*R>S#n|EDOI0kFCSL?Ev6+r*S^WHQf8O9XjOyaujeb~kqFT;SRdc20 z9@(UbM!o}9qy9FSq&=7J=LSiQhj848u;;NpM2mB-^^CC~(4aOV3 ztBgZb?3c}R+=VkeSFtG(qW!IO{aTmEX8_q7=TQ5N;&9F}=TiIpf$yVuWvnDF_VJpP z(~q7^lg(IvkpUWD)+)^%^_TE!bNLM8s=naF-em8Wld%OS5|@^Qds9kW3~MYM+5V1E z9ydb&**T>kOKYPAW|v6Q_j0(D=GY`y?xsLTS~Y#Xv+wKb>8gBF*L03t!ioNr%sw{U zlP@-vNHweT)Iko?`LSwp%8=K-?qA`sD3e7vKmrNRy~*^?d9;P*{^}x0f`Jm9U|7DE zYc}xYotI6hC{HWq6Jw@K*s_37(Vnq9kLj`qjo&_Rmlu4{GbrzGmq&l0dr`=5zOdr{ zrR1%M;qd{@m#)++`v1N{_`f=M0~?ExIW?q_9Jw_C1EL>lc~r~Vh)>Y4hyu6ME~iB7 znFJOL?Up1yOT*+ZE3_pVES30OH9FVwDj98w>wMQU+dYZDlQSB)@atpEqXWv>$>_D& zD@?rHvUMxA>+eUygjEfNl)G#iejqV6(k=(!+k_&IqxG9K?l@RaAO85u!3N1C!TFl0 zc?l8w!zYlIFhMb4D+$5*pY7%y92^RxA~J$QACDxqEN0ujlYnXcR6P zLd6xHEUdbe%}Gg>>ndpok}sD!jM_?IOxDQo9{ApvNzXc{`ya1T~>dZw#{Iq^HK0tq=bjLe~?vv&n~$ni_!1e@V5S$d~oB|A#| zK6d!5dqi~YI?0_|@#=VhK{`}peGf1sE|v+0Ku&!fWEV&x=RV~~n6*FRGB+SyKcI12 z;2!*Me_}X7xM2+3Le$~wo3Z~EKcBRPrX07zSvGeQiY*G-dIiPEjy9l(!K7f0d@>zK zoTY4G%ibjQq39)aGoWih0I((Pczv>NGTdrDmSqGjo3(pA{#h)(b_;7JFJxB4*Ky}g zJpN5zav~n5jkF!K%jz=}W4nlkiBt=n}$$mjA@Q0mG=P!S|Ud?11@bbu!R5($HeJ|7iVCnZL%&tIRg4C zQT~X|r`egd*?Y1fe(b`n2Dm?Rl~@n-kSX-R(4>cJEOc0wnki!}rDZek!>I3=_*N?$ z8@zqzK|&#?*#@LA>2G4AfH*Y2`y;UZyhLB_5yOy;|7qMBeVa~=*_QvjXu%IICB90KUt7rr#Rr2WhH>oB5<5!zg;>IdH4 z)PT~m8g)3Mq0t2gNLhFZlC?&!tK)a0E{#>lStTWb3#*Hv!Y-C7G>_Sfd+C-Y1KPFd z4Ys}Uv;wqpT3>IS!{>|Y-Okae?0{rc^%cD;BMH`TNoZUUK>yaw%hAK{JeBeQ2}sHSA?|Q&fI${*n9jIMDjO-o$`s=ztKt7bKFYn;|e6xz|z19 zI&f}S7*C$FZ`yuuu?xk~^#}mTEHwE}__QvX_HgJpyG`AV<0br(2hO;z0Hp$<`i!!` zkwkr3y=Q4t{nSFFyqdjG)Lm+Y^FYOHvO`AaXZC!Xt{C_yfe+hY*3szy% z`r%8T&i8V>J8Q1dx$=mxcqRczNd&pZ%d4uNWF|zo;ZQ0*>o~n5ht%((>cC&^v{(U<8DVmIc0N<>WWdjruN)~RR$c>GHt6|>9B zNZJ-x*$1|p5(@AZ_*cAFG<^04+cmMLXI2a;-^vj;@QvE~pPejzdg(u@oGchUC1|y0 z;&cJBAzVH5rFvM|!(n=k=(q{<`sKr5Wsd8hVb+H>AqfY?r_+?p4~LKJAzD{7^p?N* z65|XoS29f$9BNU5VK)lfMq)=r@>XJI`>>++nRcDERbxue9APvxz)A8IzC!l!iFKKy zb|#)>VqB1iVU4E6Z91H)7$w}EIyvA_L%h>Jj9deD#;-{0EKVH(i~QT9n+F8v;drj6$h3ql7MS;HCjKrB= zn^$$@kbQIO+yw$|sdu;%fiFRYtf}a6ZVq=oXfwN%^r(f<7RoWrMB4596Va?MYfw&s zia3TVww}P`-B+%-zZvD9jw<-yND6P7B3bg6>bvZzMQ?$ZIKDxk5Q-cmtjd?(K%l@H zK(p=uj&o9x8IdDDNEZ$l&h&R`2$~59;>LKo*W~G{{g}G7WJ{RD=8x##WUeXE9sF;e zHAM`)VM}4BxF|TNouesRiYrhhr#rmjGHOO~;ks#%tJrhRm8uS3ISw*!$G&Mf56`>! zL%8n0wD8}r4)gk_VsnpquH&+%L>XSl@9|{b{BfB6fl?ho%eon=$R3W^3n-oA z^;X>cnfxWZ#WNXEqs37@faOrrcJAwp|HC&UM|8+XWIR`v##2H4Sbg^IBOm{|NTGy* zMn7Q~xwZ&CRoP|{^`t9isS(UrDN32E1xNqgZq(mWxIg+C1;3025N~)+jGUdA9LD)` zD0y;^&d#)DK7SnfpQic$`=@^@S|y2F^1TVRz>B9A+xa;PRAa>`yKzDUnX=Y@mo5Cq zyZhTgK-;TBr=LOyM-sL3$$O6?i>ZN{V(4a=l2qa3;@_R1`IB7#?#~n)sJzD_*2W9! zNv_7TaIuE}#zp(z?ltV0Wj_PiPEX5TEjGLEW_YRs2F8Eb-@mo*|8UWf{bD}G>uuii zjOdr3=g7+of7^=gzm#2`!~r1Ne*j0HN>Ddz^&9N{=ET&<8hph8fZ>P3J!W<@#8zI{ zrYuaKI{e!w|F4hx^GyqXKtLW0Nmqg9#kYe;7ymGQ77DgVztsZhfP)t^5Wxiuh2R)( z_gGYx-ZnIUdPA1%%8zgU=bQSs>HqnH*$Gx{^KkEU*eRv>vsJgQKvtTOCkb28kWHec zCx;{LAGO!tki_`x%;qNf)zm*%>vbJB#5MquLACb3B2i&Xb z^nz#y5><2|CyWwXDoJEToeZ2JX3;m*@%5jjmcRT6v65ZG%mN?&(TcF)U@mkAN`Md3 z5{CVmT~h{zuXsog|g>U_`9ZRA+^tC}B<7=%uF(h#CS} zk*sq#(oDPBy%>vyxvaj@-rhs5eO*DO&&7@=Q}D!&Fa%y(a~`0YiJ2=UBCZ=!i&dD` zxBTu4JI)bm_kXZNWmkPF*12e$L#LO9{u7lVW@A-Vl?)0utkw!vBfp*@(gbifu;x{2 zqvD@u={`O3FcbJv&O;XkL-PfkqIzj!q47OG z0ZO7x_#+;GDb*-%RgEXtfHvJ*6!7$Tz>_kMGI;FV0+Nb7M)dS3FU@USQOS!5(Ay2q-5`%Cn&20#lyX@$t_I3GH4mPw@Vj=&l9&ikt+qQ z>|eF_w1}}3qveKhZ4Ak>t;XTzpKw zO&fo^T^EG?=TW~DLI2_f`5)6cuXFxv1{1Pd5kL`L60zxDv;d#4lir2$5z*A!oNtd7dD~ zf6@kKRoKH`rKO$`cKyPi@a%shEqq8S7_@wih!=D_J#q(C3o$KS_@8P~3_YKUqJ)s1 zA2wP~!qt%T@$lpvIAY#Cw;IQi*Tpg8E7ivuw@IG5{im5Xh=F5wrEO`kc>jvA z(B0RRA>R(>Gu%r-+2*q#-P8Vl=`t9u3ze1e*!_X4LMOX786;fU%!_Zn;Kv|?U}oUF zcwd2y(=vv<>Dz~F{t0z!M6y@rEt?w`sKbACg@;jK6k0~3T&+3$dla(yrq;eP7pGB- z(a%v#X-76E7Dd$DX&0oJ>K}XpH&I|4wU`ZDTRTm4~jSEsAq!E z(ukRh^xR^0#9NgBYHW9~64NIJe@*dPHfUlB#FU;$QZiMq5*Ctf8 z{r0kI_3a~Nrx3cpd%<`1lC&y3LHW#V#RCrHS^HqwnMdRDAvS#Tp8Q`qjQ``SFM{S1 zd*-%oHYEq}BV}nHnw_K;9As55noCa4^WS8WT%AK(=KK!r|6wf`=7NJ45Lr@4rH5{^ zFgL>m_4x5sm|pol!jMt@{WY!U-p#=;bG$Xf+ybveobdb-X80FDKz;`0 zuBbhh4kZ7hfNv!_f@~j1Ed`(!h7{kSg98woD&fAmOM|dgtcjP6{YQ!6<)D5~!1m$t z|Me_;Gj*V+!5pjapJiTOvewVG zO)?eTV80V_ag_a866<#}@ekKQp?)tx!Miq%^puq+2=gIc>p{Heh)c%JB z{o@5ps`8#|{X~QR(Bl8obq78=RP6Yj`rKMWlbkgff;KP<{}jO*et#{9)6K%@6I=HQCMePU{t(XOCnjtEasBe`~N0XX}{jfE@XTjcZT<61+s zT(75GTSxE87qA^mcX;qTYQOTQE6)^(qoa4T*0sgHRaiv{33e>x;0rG%|-cNh&!c2lyp2? z_dvARt$iOFYsAf2(bI`5QD&idv0#T>+Z>yk`yox+J{ByG-OVDYt zn1|zXZ;TheC0)4H9zB@R?W~%ELYWbgzSV@F@$y8e7M=d3e$%#G^45M71)V;t)0Rd! zZ+g#{p%OggmHuHf5Y=u0y36ZSa?Iz4h%3+?pT1vi|G$l!nGdcBXZ_30vg^BCyg&}b zCK+{x=Y1T-d)@fIh(}B=TlG!@O9*rr&Qrp!ml>!ilh9M=T0(kLmA8$`!2j2=lWco9 z6EVII2YPqv7j3lFByK^fSZZ$)T$glB6fE;{wV&E;oq2o86t=)`g1Z0dE zj6nyJw9CjV1KSj~f$M6VmS5o9JT!rxMv0-f*D4-*qiG)l-!@Ov8lq76$t@_(eP+Mx(x9Q--GlGXgTtS;N+abcds37n#6a;NOU`}0XkXY| zH2#UJr*3M_YniyBa8(YB`IibfMG+G%q^R+H7lIb$-7W*RiL{b8ugzb}r3=87+MNSg z^Gc!o^zVq^nk=1`2IOn~F&HcmjbpQF*Gp!S)({cxNoTriglVM9F=eP=R{t6%*kK~0 zcQ%s|b2mJ$Kf<$g4KBp+Jx$RU)^g|zFQOsf4cXr@8a4&^c)05ofR@IBci-m|huDT9 zH*QPKlan3sZf?(3>WzHKnjTcrIw>q+P?GovX&f%V)lmD=Sv#m6dX&J6r5_j3AQOuR zfM$mhAPu~8`j%21BWbFFLq=VHpGG5Qc6^+dQ6h23vm~?Vsko9NY_-jWh(Zc04cbpI zSN)8Tyq{-$dm7M^RG=?g_?Mg}h0)(PlFLd{+P4{EQ7h)lx1hJdzA>qgwt%gMbIvW> zKB~*r@(b0>fa9$Lg(U;$L~R{*<#Os+-B`EOQ{Kzp&+14Xpwk8G$qXges*r7t{9#r9 zRdfc17~|MQy^K7gU6?4(deD-BnQTN?W=5efmf=jYt=!~*S-7cF(8Q$G*&HjSh(#x8 znZ%g*m8r@JWJw*xx^=f;I2W}1-GDl}j3ckGiN}Y{39!KC|W5tb!<6({Vqx-zb^Y;K>ga;+u8AJwIj*?#~BH_54=t0 zNs?WT9PdMn@VK@pp9E~p_OD)NuXX$BwEacS3(a5b3@GmNlq%(C!QMyGp;9SyL*??J3sF6gE;O7k7f#(VAY7|Th} z3*>LaS|B;b(O$q@wU?R^2xvH%`{O1{~=Za9wntZSR6CK++l&db&TuGMaT(YK!0v z-Fh1>)}td?OZ~uNF@V~3ca54i!A?-&SAfx!fdL_>#Ly>qYx`aLI0>I@OB&D_$q;g)Eu)uSbQebH+J4M}rHRHz zSQw)d(OgLyqK@p`n#)huO3@sa4_Ezn%ERwj8!{af9X>F>kF}pM-hiM*cnc|U#Xs@g42(|tSd)jy^S)yg1oz9rp*jB+fZ|V^Tyvwdv3R}=+X)`wTl|eBFah=-;032 zvD3YUva;b@;^B`zsGT;*e9;F(A*k&J!pTZt@$yoNNXdnaN7fD zPHvobgPm@b@58OhbmkCKug_0<;qo$N2rSx3AmGW4<4DBqkPU4ksdTzmQt{^Q@=#8( znqv<)0GgD69B`u&5z9PU=nIf5phBb79wg+9bIJ=GlSv6o^$m>KT~d%q~I%b$SaWHsqV@ zq{0;s7Wa`9vfsWU4y{d~WgU>N%ol?U(w;dH9;p6sMXt` zaFwwwUS4(asMCpZCBHJ^FS(01>G=Mdbms0dGtDHh>=TJ2-LYw}WdYjtaPSaaOM5A#nSr%rh+bq>H+(0`W zdRB)sa9ljPeLi1-h9?}3Tt1$H{4^uenc$h1%{c7T*LzPBpUAZ-nX3}ISb`i_=JndV zJ>Cc6b=kFv&qH&)A!qE9Y|Tt@$OD#|QOhNFW$Qc<-c+_E$?*4p8=ZngM!Xl~puBA` zZDP|Nm{s*^g4{M!RkoUa%j~4;zVM{jHxQgwC#3aeV0pVL?=9_3H(7=84H|D7E(a?K z+MN6prPhrC;LtgyVE8j75sZb(GGbx>MbGf-=>xi0?`<;prX!V!cWp^oR=D{u_4&6{ ze<&hXZ*A@(>}Ql;oF&Nk52jd`9D!M<12_3gY1e?pRo|mIt^d+@#pZ;~>|^$%a}!iD`}?ZDacrLHREs%+^BE6K$B)X%;I8|GPbtBkgk>r-r)uSDw}HZ z2wd@DX4H20+Y8P0YmfB)$2?>7`iRC7-r7p}-JwP=(E#-=MzOe@uuJaqUsfO67hjJ3 zLT*tJrySF_>Xc{Bc}D}GX-MUaj7s}|+&a-5hWpO7sfl@0RG3BUdc2w$Q7I+XLbAeW~O{Wva!i8Td?Hvic+z4kX2)8+0tb1lR zlEyJ`0YML5_1;W52caPMv0$60dbDYFRMB4DfPug^@wcAP^GAB84`(zr@T=p+ywn+$ z`Dpld-qr(o%ECBRin!>=1KyPt@+sE%8i|L}Vl!jnR<8Mp?__yMW@&fDg%1*GWL1wQ zBfPA5Dc*X@OoKnx4u1Br>{mrEFz(T~e;Y5W?sWK$eCl&K&y5S~r|mg|T=_yMjn5l? zCsfQ?=PLec4l?f1An103h3SI;agt zLv0v3<0XnS#(_f`OVq#Mp(R~kBTl;cO*(7f{e6h$qtzRve19cCUL2PBCtub0Is$4%03*{v?YC#S7`T0x-w7pyy^oR)7{#*sH zTGaJg$zDp_LetZ4yLr#_{{@nVzBHvG27kj~i`+pgN!DfU#gcdi*EsapJh{(Z*wT!$GKi$c> zr`F`Hy2h? z-c;NO8KhHP0}8VH*x&%jJ{-(fZE&^UD4dRqp}xk3KGBO~PVekYun*;M7b-UX!;g`^K%mGLLc4(vM-F)YZy_udSmwlDT!VG{c z;#4@fWn!EDw&g6@Q@b1F>qtzh-lU+@o6ipRQ2*^Uon%-dUGnRaIeOF&9tqK(TM=qj zs;V%0P93N^bG#O+V~xml3K>4+&$iv~)VA16xnmmUt>9+J2l{LAEMND2#;8KP4c$12 z?W%&k`)VN^X0%yQXIR>vh6eM9g{&S$AL>7JA{TkVqe2KO7MBgKo=w`?Kt=AYw26*P z{t)qPY8HLQgK`s)oiHCSQ$OXQLB?C2JDu@3n_q?`)<~nXM>!}VJs|Ix;N|_))0*yT z@Ju+(TTXuC(`PsGwigEDns{-=(azL%I(OGmMwEB5OoG!&`ac{{(e<*zA>qUWGz1V( zdZ`aYIkmu~a~G`XaZ;IMcX&;Tgj#>@FV0!X!Bdz@P-Mq8wK4sB?)LBNF4^pr>#oWK zElKVKHDfl~^j;%pemq}6!MJ0MGZifW&gn>(bRUbNC3*5bx!$HkoN5a9bpDXbyQ?9V z{7FB20)<^Q!R=^zV|BKL@qyU{u)H&083MgJ?6xk4vKPS+mE_&PoQQR)j@ZsMw**nR zgk2ONpB6q3poQD}Ix>ZfN2m0CYmO#?-8RH@U_NgB#v(s1e3nN=Dm`xG(^gX~{if*f zPlqgrs^gyb>>wDGNU>ddq(bFPZaU3cLtn(_xgT1=V`$W~IEM|#bG(TWCYxNWz6}fd z-d=pc2b|p;V6LEAIot7%S7QY&p}F_>VVp?qEK2Q$T|v`~lp;U=u!8$bi24b#J{VsB zi|TJjpfz4aPn*{s?4X_MpvCf}9s8?`fC@hd#EycAd>h?QzCI6!_Vg%hgeoPpJn!tC zunPVM?(!vf)D)$3XzASy2F48N;hu8$%LVhPp)9>o_bQVQoSd!lR-GFi{!e>;=B5vk zFokwIp`A&5w1XcMx&C2(%y+-UTVBe9H1Bl`E3T5~c^qd7a^o}o(X~R}oW4d`70+EB zN1@dko+GYu(CYUD;nfr6{^JY}{JqeEL9C@JF=);Av%!{B5AaLE=T})gXBGQ?_i;S2 zL45D;gnN2X7o2hMkoxB>p5z^{-@hGY7yllo68y?`*wcclU63418863-LOFuPPZ!Qs z!RA3XhHTw#jamId#OL@8#e91kR3&r8~T#cte0ciocVH}d*p)ok6Xn7CBkoPrs+1S?W#nI zDfsdldIvkchT`i2DY!a`^f);}(?a9$s4SnWO!H+o3s-`{0MW*fZ?+idAA7Q-d%T0&u|r_ z`?0vM?+mn^%6>r&8|Fv)-w$|L{zP3_Cu2FLWv{G9xN8R}4cPL0f~~@8Tjx72mn@2JgAdcYe!DT(jRkc$8S8-*yg;kGodZ1-!{B~hBdX)b&YWD z9okzsPDa?vW1-{mC}V0pvxk*)54OoMU44Cf={eU7ql!A?=SNExeM(0@dQz|r4Cy7h zs>EeEIi2()iu!F7PD}Ng?Z9UKEMrhT0e_zlA-FLC&Ka@Y@!8?vplne|3ESanqmuHm zJoI}oVI+kA_3$k@>PcT$EyiSdzb$-JbXi&K-6eNJVL?G&NeR+MyHmB#o$CS=-ivh; zz&I(uIVVy|nKc3e0y7(hi=(u(v>C|3F$|a%JQcOZlVxsFx#{Z0cif1d$Oq7Zyl=1KY-G~g}biUhuBuQ+Xvkp4Ezjy4HRy!IPkeNzNPfs^J+Zib# zNSUjc3!S683g+2~X-rPK2~@z&HSqN0^c^mc13$p*HgY{NvP#2z4VHFMD`MwRBV#Zo zi*y{vMJFKpe6~4%lT&sq?a<&M&;=AQQ|*(Ld7D9(ER!Iq^Iq}kVTL#$qvJ!tyYG}m zp3d}elfKgTkCq7VROCWJVy;IPNwZ0ks;|Ox&>L?0G81hJgZhB{JqFdKd30u~Vm|tk zcpJo(O!VCp)s+_03|}0SHDi$SFO2R_#-4>PIZ7-0r;?j#H_*&qz?=n6QPys2`7@Q) zF){3XFasK<(lHsS);C8LV*6!w*(74f@6iUN50Ga{$@NNiC36A!+Ev*L#)XYu9JCm# z3Ji7p_#*)#U&>v zs0?v)J8QmU_V^Q&WFwJiP3!|+EM6;*QS*zty-XERX0e$ zY{>n6^P!9U%9ZIA&^436tuHNubl$whFYA9RS|g1glBgTy~#q%%m)3l%WKu9 z;k6A7c&uEBAg#8S^%)m?Red(HSx;oZ=M(Wv<5N?{(lqaoO`19Wj6--YVfY>?L(@0i zCGD0r@xg3OPy5F3x#H&rWirT3*%lq^6l`&7A6S`&cC5JS>gvoS2(rwx8R8~(z??UM z_;XH16lBp=I_beQSGKyGE)8ekwleV}Fh9v-%oehr*VWNLfkL{;QoSwlQc{_q^VPPH z^YcoA5njf5e;I0UwBG3ZZ>Zxy#uKE}I1^gN&@Iw7%y)eQY~n}uOv|w~=JuP<-O>^? zFvG?)$oT3TLdLY)7j=9O)pO4X*n7LNSiTD|phj0@)tIK?%1rAj*Q_UJ$Sz-Ru%5J{ z2QP{v%fSnJvPTkqQ+WN;@_{}5}uk`d2Rki7#A2TJkE6C@|rl3OGd(Dz{-Q1pK3xHy;;V=H9qiV zK2{Gzy>01ZVo)i0J(J_yiqguGlHTc77T##$(g)V3iznh)i8V*Y!@|#Xy<>;cDg4e- zkWo?by5v<=F(PPYBoh~())B#j(bh*N@kzbXNE#|ITjIVYU?9U=j|vE0Y=v@owGb@{ z9T35SQ_BV#&E^ckjO9af?-oi2ZUYV%m+E|Ch!^S?&+pXh-(OxaNLOh54lIAg%wHb- zffTSzR;agCcuM}ku*GT(Lt&)GsT+g%WLR?^04(I>rJ;IaErmeQ94Btw!~b*hCUaj*|f5fcz&e8MB5i2>$x; zu=jULIl&aZ4l~xYXV89vQ+eY49;=@F3_L!!wOWB`Ci~0F%T&DzoJ41=i!3vXjy>PL zWqaQpHUJJ6IdWm8tFNI21IYWLiLiiU<}c>x*KVI^%`O@YIY)nRr$-OP{)lkA-W!Pr zxPh2@K;4q9!-h4^unp^>37_8v9xZ%62S* z3(2wNjl1_n#>2}tfSo?Xvi2uYxyQ`J163`7!_=f*}mbNZ9N7?-}w6(Suvl zA+_vW{cWw<`ETHrr)+fgzA&rOERrBLXR4QWfx=z(`sBhmDwYLn(tHYbuyKzB`!=n+ zrpmdX-2=ap(>*K0e_z%&uoS;4Yn07ITIHi3sR(W z-Pp|AjYE{>Z5C~=F;Goo1A%x9OzL5MSsdux4vFO=5FNu--RFoz^`5 zmCwT2D2rEb(YmF}!lPk@PwaG?n;v<^j`H8x(FY+oUtX`AfW#_~Z)92WDT&W>C$Mcy zku*V!uuB`H;!oaage;S4F<*m4Ed%N1x4~dJ7$Y;YptmnN;=(_K+DO31#N53TL zwOVjQWxsS%>U4C!Tt8PTaI~sl!GCpwMbnk}Az)NNL>Nk_i@4CSeKZ(z{5&CJY9 zJUtg*@WxswRIwJnX9Lu&3KJWIEHky=v#Pt;vC>@uO3nM+-r)0C@%!rf2?eo2KGf_h?R<%$OL8Gz!ONTh-m;2L2a4fHl&+w2%=rU%35dnBKla2fc zRp@@acBx)Y^-m^ckW1Xb!gBaAR2&w65DjAhSZ89nTIi)?*Wj_bh;i7snx*YE;;C7s zhv%Nzn$)L7II!&D;i00TL3MF)vBQ5h&(c!XrDv|Hz0)pM{Bud7a1}3hjPGytINMPp z%nV`j(MPf|YCko@IaAr;G@n^a{eN<`mb9-k6c}GJn_f zX6Jz51~Q}_Q!|#ZXY~r?Q%9V6mu;a3);zLkt(j1}<_%Vl^onjT9?dTw8ZO{?vFJR* z3IcTKpdwGG$iT1_UZBLLn;c;Z@)F6*Nl`^0iali&yp*OL;)je{_ibIrt`Hj-*CUXQ zbN+~!!BeRf?iBu0w!2iMmcJCsvcjJ zQ*gLVAVicvK4W@ItH`32GySX^F2A&`L&IVHs?W$Wt7ZUfcfq?vH}{@Z=q~1KrgQ~A zl_1{LnO0$fIU!(B{LOSvQgeVRRy4O#($ztEc!My76W;RJ8q}3)x@^6y_4d^H1Ka=I z0`@3?c5P`BilSbtK7J|OEQRbk2d$vO_NcY_3Y#eXi9fT2IDJyA{}1W!G;|D4VG4Ej zD!a`TTp}?sw`2@`vshMtyuC77gfs6dUa{Doz->muf9CwAG&vXr&MPWeGdS?BDKY19 z_tORI{Iu@)RqKbVi~9SEs4@ZY=%!tC>xY8wz=_Pc*26Z%hMy+bgXV5?;RC_-S}st^ z@@bLM2C$)d zO5{)@TiYYFv<-9*29K00S+|gstD+&+IN@T?VXmiz9H1?F8;FGU?Z>0as@Ji=%|*or zTXhI$;P>D^IjogmUwvy#i8c0$swk=+={KviO|F7F;h zNx7Cfoi6X4?6#i^D#&=s1-m8*>%PB~CU2rQA6rcvYj2EXihNG{GJkSflSD8EWNkfY z0N1VZUak@4&@8W}YjfaJlOiQ%3ywg^p^o)@O^{If*DU??v@^O?OCkC79`xYOZX@JvqO#T;e%MWCP#cDqNv6OFxFnTy6HbwdnWl}QjQ zwM^SWBCEnq7UyS_Nk|n&RzN}$NHi6*H5Z<%3ln!$hn)+pXd+^jUlIGDRi z9DxOsnzA8(4kP_zZs>EslD|fsmY!dj7|+`hGxo96kj;GQx(IQ861kL#(SBn&I;J7^ zmP%HCQ$jt>QFIfO>FzY{1ps{zQ`~#+)a0oNUw58P6$>*7-UNT@ww213(0;&_4DK!z?SzJ>T%QYl^Pn}S+py$6$%u@w3LIn-p zxC_I`d}*Z`Un6~+oYRSka5LQ324o#W9YZ^*vSTEZzrRMVIng!1)s7f2LcZ$oj0Nr; zykjxXgMGaZVKyu0?j1L)&lI;S=|AN60dp&+wsknSR<1VNwt9R1F(UBa7(rM6MmWij zA@nj&i<)J$W0Xfw5C(npg?a^E{LPI=?xe*b200)(ApzUVM!|(6WO+u#+9)JpbG>-2ZU2O3EWbH2`cGZJh3rahM|zan)^c$Wt2N zp;g|h#E3MQf{^bBzj&9@C&{be9ua#{W0xe=iLqIeMKkp6!iJA*pW{-zHu93D7ynBZ zTeZB`aFPrx#%m@Irc3^z%Iiw{)#qn55B2nRY!L3M%55JYIB)K!V`z+b{GQ$d!!@|> zE^1;P4Nj{A@x@B(!{o&meX{_TmT1F<(c!MU63oYRifg(c5^8o528N5t7LXz*$BeA1 zSXsiG022hE=L5^<2{FUAr6aOiu3{BSVI53_Fj0Jso$Semz_c`0af>L_FByF-$Pz<# zt1UDYZMVfO=aX_97oPdGwbK!n+-8-=e0+-4MFev5ENBQDr2!_6Vrna-d-8kt-bM$G8@}OEFbZgiwKbn&Y!-FJ6IJgI#>P2FS;;cqV+?M8Zoy|- zyXl?IT2U5m>>z1vz3svYd>HBQ@ft2@SzUp@;vD(`dQJfl0wYvyo=Op$pV}?fA`DJ0 z4VN0+eY2G^U=f<_YS@NIQ7?VnE~vL(L)kc1S1lCyk)eI#uXKjgFU{JJtENDDCF(5G z#ydp<^!3G(%QWk1)IbEw#=c9~mQglL`t4#vMt4 zkHKTUfSuR3?vP9n)}OMo6JlZx@A0&Cjq88v)PZLE;@j#|e=U9`MlZa+A(od+=*FE zM3m^gY!ktQRrK0gIL0iu4zXKtUli~~F0=GQ2E0wsXpiF^q!me_5x0b7S;BsBm4&#` zwP0m*FNPd(K$j)_Jz_YbpA6a&m7B{A&*nK&;Dqn224y}4H5ro>M8tD!Y=l+Q+$Ao`lLY@@rmj@x@EFK-ZxQX*F0*ni3? zpcCMIB_~?JyEKGA(I7tpWM0~JCkvv6vM&F@DDr0$pPAX%j6-B=i5lwbJKNN(-hTy@ zl2Rb-zOE!WeUxT4x=Dgl_=>WaSw!7+yZg$JP%>#mT9hL?-eRO_$bfANF-{OThVQj# z8!y}=oz8mfe^HIkt4F_^hL~FSmfsxDV;Fs67yGk9npLumo3e5&k9ifz*Yv}jnxIcq z^(9qR%DA|=$h+y9r;RR^{AH29&^n%%3 z!@duUCb!&^Ivp8aJ$*$+wEmz%wyH1_a+`yWgFB98d#_?-_DH|WEHoA^0UE7)IYm7WF#G@4HV{1 z_rs3$neqcwkpBJcIQCftl*>Q7gr?xo7 z68BMp%-m=pnU<)+q|bf?$+0x_6TC2e!9-E0;=DL-)|m(Uhm!`26^NaQp`*6Xq~5!`&li+W<-su`1GjkIFM&N0o|vr?IXzqefz6_JJfJIXh|OIoDJ4TW13i zLRZxOhf%!*V5oT*KXFtaDN=}GC}XDx_VZZ0UI;$+kerB3VVV!m^gy4KS6Um$(}~WOEVHw_7q;vvk$} zv;Y5Zc%0{cagSLqzDRDdwu$H=@&W@+Bm?y0oBwi)kG70XNm&eN#FDp~kJ`U?V`8b05+5+Qqfd8e&q7jwh-R52hxyl)K=nUb! z8R9xW^~VBI{lqgx2Z^Nu5Gjur zB49Ii+_j|sRwMWN3(!&{V9-bUr?YDiK-y7-Ocm>h-@}5`_ZR6Zf_K~;!rd|fZ#60Jy>uGrBEoYEgC59?hxD^3basMic2AQi@OGBi@TQI z^qIMH&w0;1@3-W`o=JAH*MF^_E$j$b>iWBP?+jLY&v3@f5#I4TJX&wulWy83MjR06 zuG|A>)ab7UznFG$_T0~XLC8Ms$POM{f1W8?2dm zt(;i1269=}cdGL@Fdg0*SEpq>$|i3tBE~wf*z|E0vI%^0{&{>XLMAL;@NrS#C|#t7 zXJx(__VQoP=4GR)|d7sS?oPG%1w~c8eo_R*I1xI&E{*sx%zopq7kb4U~m8O zEA}vfgQq|XFq>awZ_nIEyOLu#vqAoLGUSbSPB_yqxDeMFJlwICEBVG$nw_l(PKRwK zO~`c9B#S=)CdP11&SF#E>djUb*C}Du6^CId)XKfW3E%%F+!+ilTT)@ROP|$$9`e%| zgVaw=x%g3Cv#;m4a$~M9|X52kU$nNTL2*brt0Px?ff2l`gOQ1ql_2f-4Zy z#sfofJDd0vqJlV&(C;yfyq^fwjWd0t7LpZzK%ARPi$5YR%J&S*Kq4|ayOi^Ih}1QJ z%}^sozhI&eq%YHhMrBt0J@SoMOOZ?$ z4(3@zGCrZ!sN7!=KFC6>j|sjnZyYDr%Nra@l3^AR>h&yo31s&m@81*PhZBTS5l_aI zJqWecv9#PcwdS$%!~MN4{&GWyP(@E1UqFe!ntbH@X_Lo?Bn6uQyzm1Q@<@f0+|0!D zvpoJKVC+6Hg=dMLDp~I59w|(Qo5Uz9iN!&leg7s_@mRdF?dB$F1m#|f9Ye{b()VNW ziM4OESYDF%y#ABvoBq(0Di4QugBQvSwVzO*bY>?beqUPyImsGH+P!+roYxLgLGcJ1 zCGnTxb}0m;5?0(1EZ&(?4bfCl4btglocyRIR2zl%a2KZM9b}M{@haDXh=-{R!Bsy8 zw_3+Y&e$nqbT-S{3l^haWLYcW2HTJn)SkUu%R)Xa|NVynE1LhU$CM+vEha9C2%XQO znl-{29=3M1#P_T>U3Bra#v|K@+(P`O0&PjPUSpVZmzE1ar|^FB15jtmb-tp6&0brs?45lPYx&U1sEJM*Wdj^ zfJHYva3GzLYCDBNPwkILZ0W_j`SNm`NSuj>l0Q)(#%hxpM)@Cv87eD$FKz}S#0fUcj>1w38J9HYoZQ@mi5!rla!R?O$SYUwrJPTiDbdL5t3fe~^9AD%Kaa{^nMU zW^h42=@pYaVrT_0@0zwv+%Dff*pkpfk$c{Tg>3O|%Yo*9b^!xRBc=d#*Z4jG!X!7x zt8bKMa5H2W|J`O7Q2`LST5|Z=`(gYR6Z-wEFtXXo&La(9S`pAuz{9mXO4T&GZ4grO z6FC@gdujnj?2R&mQjH5GhPmRocSKm9Lsy4IgrJTCaES>qqPxSzq@Bj%Vn#6H>BuGM z{g7!-n%$?Hs{f`pIUSP^wz8lV$J!TLPRw*@MTq4f>)sZuMHyK$jjc`hcAA?omk-2h z_e3sS&sj%SLAEPyvKK<%Wi*UgoZY`M`DCmzFB;Jrlfa%*N%FJ z7z>Q)4cRh!gYs5{v)w^}1Fn^tv&>pm>l7Y}uR4FBBSEO!Zi6QqyVKz0m+I&=Je{k< zRr{qTCGYU_+)BfXdU|Cn_3f4__t_SlSW%!GQ5g5CEJa$!8W?)eSL8jNnvx1jIkzOP z;EF9u%wt3s+mMyOW)gW5o5d(FqE(oB)Gc^-+B^XLeX2$5v}GTPUe)16sz%N{y&%h^ zHv6_E<7e%ZP^wf%9QFv_fXUyBKDXR36O0iSD+Z1y%`$l=%4 zguWAnFJtPwn3@_>?m1O=2`bFqEg*dL>+rLN_@7Z!aUUq3sr}tguDmTYOhT*a_rKv62Hf|{96Zl#9`LB#;_}cy~|0wK(w&vLa(kqUW5nn14*uYWJSu| zpf{Czy6%YYDwQ{doWcIH0_&@sWy>W*#5*79=e^IqC;E%U#%grBm8y3?-d9`_;e$29 zwKz$>w4y1etIg6Ww_gZSss=X|5+35&{OuN5>iYPca~m`IZZAoN^xbi4Dr!5l8C?7J zAkCX8RF;Nh_EqSv#iz4j7KX$|a{VY$Zy6m9h8&SplI0Z|n`hBmJp8VinVIk`Sib&xcEYYt&Dt4wU#eP>*#oU=Y4NT6w$sijBes&z{9&z@qSc2 zXYOHvrp`{x{hBr(*e(lH$CwOu`&9ec5DHDL*#D6YR4QDel|d03NM5|JrS{HTBSapX(TAK2rbXpcx4<2|qv z@h4OEuTtR6!^RH%=I7u)p8+KVAPu2;Fm4A0Obr&V|A(H6iXU`LE9y)|`r@b;^m;}> z!QU3W@%(fPyjU8UuoE_!#H^B-rCLIY?zB9=B~N-?uAVu5rQcD^DK^Gr6zii}X(?GH zGvfZG=jkg)oDY=)<=^WSmxN_PWUa7xAuOELX|kU7VS1#$p|TT_uCd1`elQ`rA>87z z{3Xb7k(>vp-p1<+>bIgok ziuPL22jm{WP6-2`cv$L>0%b&tjlgbr3v$nIy6h>{e;{U1(l4^oKc}K5Adn1m+VYNeYW~#d`Ut*qR!8|e;Sy*CU(Hke zDF1)3Q@1R@UN;%$2xhV3lorKvJ8H;J+p<3yRI_J^qBAdvQaQ!SNTEBe?6yN71YpGwvw8t+ z3G{Tvu@}l|n-K!+vNr@#<#ssgAuqecnL#U^Hs?>Du#4bb z=OB_PZ6AFwm$H~#|P!M37N30dBvFedh43*)ogAOm6!C*7?t=9U1R0}>=HAs0xzCN zqJGe-U#wsAj zf>Syr?&Lmkg!-;ZH#|~zU)a+3a#G7lK*|3|<*XhnYe z=Dxb^$0t1SmcmpYAfDUWp#sGf3cDnj+NkxknF!;Mg*kqF8utv=<=K2 zWte!|;_XZJe6La|Hp^(zoi9uAHBjJijlSf8C3z*=64bdiN`8w?(_wycHLM>|>hf}5 zQVi05%)cRGGvl~_ZNe(I-h;|?GiUs#0cY62NM?2+1fq@(|M>xEr%q8JX$?6s)IYw8 zj6e62fv+en2{<@$r|n_M>`RTDB1-;p_l;1)VyZA9#&{v0=~B=i-XJ!+4Kz^LL%>9Y zTK)p62wPA&ZP2&m^Qu~zr8yqI!HOim5ND46f9Wf7hC7wQh>!T?lZ~&Nta$80h4`}C z!Ltzx6cdofdO>-Z;6A@ zAGS6!XwJQ>R2{Km)5;^Rev?u*p~`fFV$Je|3BCZ`$N-EnlX(jTx2NUr& z2T#_#HeQsuX7q{Z=BxZWf*x;iIv0(Xe7L6bElp5V9bS7yuj)am6_0bhv0GY1M)zzL z!3bwVM(D-bAlF^wwqH8z_bUG6$0d_BGrz+#qqUbvxl}?}Fw3jOG%FFT+(0v?)>r#G z-aF^JW6-BZpVDm=!51UTd7qVoIMcg>l5jmhZVZy13;q20+|DR4%)(-h` z5w=lJRp^<@n!w2r_(PIA7zibT07kide(5fmyaq;jij1^S5L;gqNu}^r8X%1(2CU4LmejFP7~MjFdAG6Lp-ZkvCa4PDHOTVmj5BZkB~e!#3kM~yBs4(s3;%U@eAeRr#u@+OsrL<&Unhr z%d@eX6(Yoaf|dpdWh?N&RxGFvftvRDnOwZxmby3A(^A^_t=>zWG)zJFM8s~RAh&1- zdDJ_S==CR1o|9Bg2%tI-em99v>aYzq-gv(v}e;e)WfH zaMQM9*ipd0gHhZ4>@=|M?TQY`ql7pZRR~+h`u=!~@8&{K87rNV5_o!x$mETvQ(kYT z^+6jNPRUUa{rx2-w=Kq#c?Y5AKD>FZ-*onBBft8G=`W;>d7uPfm0jloea%o|k_53Y z3DWPTP0Tz-j{e!>Wea@E&U1<_b*aPEIIn{!X_n?z_JHBJm8Wf3O=#E&0e>|SU2`1XXY-R zn%eRA;jfSXd#5?25KIwCNtAfRuNew-Ds1;P>TdHu{d6@!i^iY%i4B&Ea3YNnvLH5z ztihL@?2BG`?Q<|CQ!=c4u0|=o|;1X-xeNCKVi!0O_Tk`rlu2 zMdB;6WP2>ys@7FZWGTK&sf7ergjvkXa-DQOh(t>>#4a4XxB>4EnEkyMEyXv&-aTSX zp8Ur;4QsK-fah`u@s(*D7yFHMA|Edq{_^7`>u;}rZC(>otgfym5FbJR02OsuW)#d% zMaL$DpY042pvHtdNvNtS+C8Wi**Yq&fy!q3wpZ1^wo}#y4nr%=A))sL(thJ=$)JGL zS;Y^~lyX&kwzFIiN4006#D~$%0Xt=lpC*ibk7Im2gYN>(s9qc+H;OFHsz<1&wEqVc z?ZHCr)-HJdPqextqV$r2q7p7`L*DMP_LXCzCeIr=vpzW7S@ZI zm}|Jz`$p*~?tk!!IhYyw_YBjlIa#jQ$b?V0VX>vb3@6f+C-Wuk48E`$`s14qh@i|A3DLSuEXqmekh*Aq!@h0k4{+y_PZD5cQ14?Y1(5EefiVlN8 zFrn_7b-(kiD25z?$;iM;U-$otXR@R9*kf>`<^q%bC*WuNP8Cf))AOnitWFUCm6@TD z_w&l8E>1;&RAZ*r3A-s+=LBvh;AX~jL{*aY^$P@^jL4!}*V=|^)xV}v_>Q@zv~*w` zz8i1KnV=&c5kfaw@i$gWVIL)keOjQ`^AVxwyxwh9C1l$n=D9EL`gEDgc=s%NwaQR2 zwpewoyGl_jRHNflm#JW*T!S(QmW4PBDRxK>W!S>LAjXfl(18_X&hW`&J}jD+9@oY5 z?OJm4_US0@4e0C}MTS|#m_4$vlkh>5mm91@PVz9ftJyxH2r#@G1;yC~!=-u63VV1K zPA+4x)uam!r7GXIj@F~=@bYqzm!gJ=gcnsywYGumcyGq`DetYFDF)Ze#U0*rrg&+e zpu_w=fe}~`almt2D<6597kX`|-6m4mxMZ1U4UFG>0kNz3Nqh1`EHFRSJzK2Z0Z~FY zE#FCM?73xfqC7nj%dSNbx%Dw76)OGcsrg61>QVZF)=|2xOATWc*_-XbHe%Dz(Q8rw zlYZWiI>CqiU0z5a?_4aEJOviaj~HXKn55?)-}9p250C|EUsd{DME=$kE8^nPhB(B+ z9?5^8DyPS%V@FG@_7y+i;WsJ)*(i5pLiE`2($9?A-7Z2*y5?i3j}u%8UzlI3W0JS4 zm$k8Pu!regNrs9jmpJGwXr+esdjR&;#c`n?cv>|W(d)QK7YRtT_GTa^l^L6;nR?sZ z%?&HfUr;Vt8%$38Xtz6UQ(5h$1);+GS^tfAHlih@Xq^!1+f6QiBrPd9AysG($nw-w z-4DZMH<%iQ8xq^>%F9<8Ew{|(h#;&RXG+X3i|c;?m-(?8Zqq<|jDvNZ{?LyA*%tI0 z6C5pxOeaIE5NTYt=c*6Dal6fRsqCNbMmilx#F|5lYT%hbd*nl#M>!?vQiCcES`2@i zaPpHi`RgZZEYPt7@ruhjZSwn8UzXui#-nvLhl`A{`3aWkhAP97UeVC|+VzXEkNW~0 zJub@Uf_vp9K*lc&E}5B0j&Az>KL!lfOP`b~zf6sNH>Z4(^Bg@JsDYxj3D1VY2_mim z{v4{?lX4!at4MX_pBXIYCAd}6)FL1Ushs)$b6ovvJB2aqt$fbRm4KnJN`G9^n~kZ$ zh#D7*Id)_lfQdP6K|ETd7Pj&ZH8)q%x=$xPp_8qM=FjA>nd$Y_LzzEmPAxYh(D^ad zmm(t)O#>-S8ffQSUy|Vgvx}ii%F8U7Fs_inNYp!Fm!3Zq$U++ZuaF86 zC^?gs{k;kLyFeVHnz-KL#DziHVu9K2YZX)yAb@7_^K+Z$QO+Y0%o8GvV~$FK*RCc0 zev$s=7ct{VKBycM$G+|`jf*H=&SksFJb`!V`Wa=&WeykA zC1fZ0*N*RJShpT7reue!gY}euq*P~gc%6ud*e?$m%_F7oj4(a<$GO*L&r?cOw{59C zJ|~|l0-m03ddAoa{Vk){L9cZ=Qne^Ky;kn$2TYTYoqR7*RBz+@_iKROO$!S&sg}6i zB^17f#&m;hLD``XIlmL8A{Cw4raR0xK4}~kG#;%c_^pVS6e4eXodAjJ!?;h>wM(}q z4=mRz0=d~tmkaI7w~j`OhEBV8B+HWG!)jjHCWf zW@OcT5wqG9lHxcaAX3UazO{&h z8%Z~CL8Css=bbO2lz8V;(3jybyxD|oi!`ztoak$7!Nztn=@zfiE?j1d{>dASp6U=u zLcBGD41Uf?k#RHKzWBGtp!^m(XQw4ubtcgh{0*X<bv532U{xep zCHXM4Xgr?)%`1bf5waa=DOR8>p?agv@2~MdDKD{q^Nv{YSSImEtj`c)&Z#cp1qe57TJU z5EQ(`P#ZchzI<~&-r9&YnpR7jo7OiFOnsJ2Ql1r}DWgrD|9Svd#cK^P;{3sY}C@4*=0c z6xpx4^`Ik4jEMyo?Qm2<=6ZLzh|(arr3dYx`_%7S^66?7oeUD1pZ_dXfRtfn!byTv zP_V)*j9DdqTJ-rSpJ}H(fJ6VBmy1iNIvtecUT9;;Bzm+^7;Qir>`0CKbbv`-;#?9RS+%Vzt7Ixgtihw?ebQK&VIBwt&9%} zk=8?qw-)1f|CM*q9V#vMQdQbvIZzfq(aLa)yGcSg*^VLMuoftyQEDZy0^sh(y|A{% zqkm>Q>h?qOq_Z@3m9~ZLAmP8GIj-D}!vh)7PB#vch{e3oC6gCXi1&$i*-3qSJBB&1 z9jC+%ht#q~^lzz0E*J%NfmrnJWbH=+iB}GvJjmK7FljHr|0evp_c5!&MKh;Ige`jQ zNdbjUD;7qP9fyTNuoW?@)3uy9R>X;&JmRU7L4T=imOSRro)Nb4jL9pZ7<+(d=X*FT z$dVI8)bSA?H)HSs_5;`F&!w|CrVXMq&k+~1)RlBxZ8$9O*C9=K&Igf4u_Yh|8wj(+ zTOzHpu_!r@NYK@Nh2qTZ?sK(2#}_zOm5;(?Fnn!E94Gxi0-Wr8HK+Q_QdcIA0qHl4 zqzdW3x--^;?%k)wCnczZ#8^#<9cBHUk~%%w0@C4K#OQ9KUva(_g<}*?ONlZJ$AP$? z!F-ZCh2#;FX+{9r+=9%d-p(>Mw3L|JCoUkga5bzOT~6*LVKSV6)XVN9FFxTiczliJ z!Rt=WGRnOaSJ8P{r8}9DD)&^;@&D$ZJ!|zB~E=o5m^?x(%mSB>v7d8vnalmN=Bg zixM9|Bu5_gM#AtYB0|Zw#n-_>AbQmZ=w%0SgAnEkEqTLiX$3zJE?#G+Eh?*IUk?+P z62fTT9g2HTViPt|DIJfVJ63UziSXGge4mr%+V7^5XE@v0Ex`CCiKzl3;sodFXcGb~ z9}|gLtL@l!hiVwxp-!rgAvgt){^Y_>pW=#?3y4U)(SpQMf*HodRVi3;<*5BxF*M}2 zwRnT!bTBd?m_fSpIaWT}#hN;(GP;Q!79qw;!>uqOMff?jav?irN`jI}AUg+NE`q~6 zdfTVnVY<8gO(IyS!^d5Ewk=gK#(Up6$)snFdb`TQG4zp@e#S)zu7kWwwO zr>MImyekLs@y?Yqv0aNY2Q^yDJ4oV&d7IOZ8EbLNPsniR)7=zJ2N|16%(H&fK29FG z7aYNyU!t#!3WNVCW4eh&C%?)%-c_q_fA?;`l)H`3_RY!CW)w7cRX6galo;d3r| zzL1t%|2@!@v~*q9)MVd#h@p(vaUSEf>xfph;mJ}XHs10I(k&Ui?_dHU^sH@f_xXG| z?~=yoveC`F@Wm`sbOQu?U`~$oAF@?CC&E?gd01mG|G4r#t+Cb7;YDIxY%FxR7W3u( z?JdS27At}RKD$3vC|XvG1RBqLyCQq@yA`r*X&v}%O~O$|PT~A%4cOzO365+Ls;W5^ zp`di$DpiOIF0Ka1JXJ#z6Cm;WGeM4xu2NLX{B_xI%4m}jrJehC+& zM_95A4oW!u^e$#XE=BLT-F^mU`dH2KIt>&Ot6tL(IPdUftIPhM<1>f{&7W);p3^?j zd-+T8ShHctn6G>2gcEA4-gx2pp~vb-M6Y8aNBEyKN?ED*zR~I3Ftx}Bvi5d7oMDxd zp1<8vN$`mS)waE?IIAXOLxKphwKlvMA}cxN+WWqGoZ+`7ONgYP$I|S0Pvky7w&+!T zJcJlxx?J71nI!S8>gk?XgcWVZ*!7B^ISoFR4T}1(9&R28I`0y*ns;0A-`h$#l0|zQ zSAZjA9w3V}Qa3cJ0Tpc-Q`D~3monrfg9skaPA9(uTO-BTzoYEc>dhHMDZtESF zDI7kQd1DNpG=ZmEAbxv%jQ??i-kCuVEwZ+2nhmlPK8G8~>8IpkqlZ3OHejA_=@&;8 z*BHOzvDHJawV!b_7%|ipEb6p9i0Jf?&B-WOM;glMvBf{=uZQAYbMc*IOqcjWLfNH6 zDPc$=o|*>oGWc5{yO25~GFF4Q&I=6d&?JRt z^~O~PdB35TllJ$2M>uG~OZ^ptsX+xw!h(MO_m>U{HnS)d+Aj(66nk_1l0Fijlk|Xd ziRjhcU8M#LJEG_94A+xk<8+DvN%5RF-T9`;Za1N>rr6j700r;98l>zw6*K(g2g)=* z`9LW@pl&YlT!~)w@XkkECp|IOWofqe4Y zCK8eNjnv=F%?K^g4RdsIz^L%1=mi22tYY+KGay;0y3%M+q4XX-EYv8j2E`o*zeCG_J(c|y<{ z5Ng&YhNU1Hz1r@U1`9h*mj#K7wSrJrcTVAD@rfD` z5Z<(iC4C()+K=2I$Bd$W`Xs(Bq7h+MMva{$Rnp;1tA> zTx61Cr~Sp7I+R+PXVpIVBl;w$Eh-0l102PING)&oL&@R_`3Vwt=Wu_wi5WKSJkTYu)MLJ+pVhDp8FD)+R) z|BFD$n*;;Eg_JODHhw9_!H8Kcw%6cA!gl*ECIgB=vWlb%w$uJJ(MyS_g^6gxM#-V| zg$D7+OumE=1{>EN5Z`Mxi%!`(ckDyYC}8aO=+eZRonJNPehZBu;;#*9xgoR2895}M z!!~*tbe){s`QgA|hIdcMNnZED{sB1;BOP`l$lxf%Xgcy}?&0XUe;%`%tJxO`|6@no zUX1Uv*^63gtvPo%ghk9G?`S$CeJ=Cs5}2J^y+}D}#{Y>T1%7mK2N+hua`djeaQl;p zClWzN1p!%_wq#bPfLLKbB^KA%6#k5i44MYgkwuS4{x?w&sxseYavv9vuz)P4rt19h z&!7C(Y`poSf{IFJu=4GF&Bgw&Dah2nT9=f7 zvJrUu|Kpxdhz+Jw zUrMvWz{;&Y-EvHT~)M;ejzrfg^l_4B5xz2S7_ z%G2TZs$ci05UJWS0%T`xzDQu>ZT@%VbL;~ka;%q0%oO_(f939I6|?#|^QI4J_Vv6X zZ3#Jgn)?!HGh{aY>3@_-9Gu1aPRX~DGW%@>B8rKr5JZKb=d158C_g#q0u(d2Cu zhD+jt6Mf>yk@v>HGcTFJkI3p_2NWI$2|dad_ngrJqT-F2-v6M(4Hrq`60)8 zC8PQklNTu^zm~>?`V=!KR2$yAS$TPt1n+P;%3RPo!8~g->{WT8xWo};VvB~WawRds zB3Uewr~JW25xi%b5}G2#!9Gc*O`-Z-2SiT=NOj*ovCRG??@;}P-`bWmwN-dQokX?Q z0JcKf{>3vAVX303EQcV}xK@Ab@U`<|l(#a*1(n56b@CQKZ|EGV?hJY%(Xe(XTn?xjv0y~@$qgfuVbd0eH4X)$>v z9WHI9>kZ$)_cWVzsHb|n?6M>vzCjr0w|^c61TjKSg!!LM49mS_4Uy7&_l}d~)9LwG z@D7)SsUsN7tOH`mlm_CTtS)iuBWi0o1*;cl0*Skd74oqJ-Dzg47Kr>y)OF$~(U-)` z8XANMc*Ia2xCy@gSQH#3V#4Adv!!UXcm&!{g?7qt6}zI?hX@n+p0_yFGz6V!i8<1o zP4kAn)27c;2kwK79-jh8+aJt@N5cDTgz!Pl{1|o$)Hp)hXQ8am+ds0gQ+Cs#->oJ+ zB<$RW0~SxffyhU)`dCCP6&{eFc+7<^_yT=j&;AEuX?D=YD((mUM@@&67zm1MAY+9t z3=okV+}+v9UIR35ZPMQ+Qe#zJ<#-IqN|*HdiZC5!uH@;{je0UPHb&jSUw-ojO^QzN zX7hU)KEB4LKOiVrF4(0eJViWu_2+w`KJt5KHRlZ<944=iY0vjZ``%7W6Eb;eVxIDh z2_fmyV}t!B{|_+5V~3Fme29ZQTI{J4^b#fGM}!*(rathJ7i^`2V1;#D6nLz5SfPXf2)yUvy4`Lqtm75Iesc9kYJzmkS6^ zXsRdtw6X=*nL%H2A8Xl!m2bhzftnuK_ngh{2pQnMygOJ__gAS89#r21S^KqVcLZ6Zev((HLyXRGLCLBcd1^GLSEn%w zzr~TZFLc|r_gquGpZ%xwLWbjrE`{CLLXdgeJ^5tAs^djWtt`+Uq?y__ulFp5#cuJv z;Uvmz-N{Cei=~|%n|~WK6lsJM4}5H640nC(>vba-W0TMQ6y_>OGwhmos5~c|cyMSK zt>~P1Cx$zriP*E2q&-~xj{KVNfQvm%A-9t-A{qW2=|r;snbd# zZ*@CWo_cO47#?FosCcYh4F9tqgs)1YpE!p1G7=u}+dl4adA;skW9hW;qaZ)teK|VD z?r{N0cBEC<`K?Q*r<_(#6A|a+H`i$(L)Q3~su~3`nR<~EU+Q)T#xMbs-e7!_%?Mh( z__N=Wr+2DvlyfeTBh^Gi2lS&=FS`?mT_)&ef3=dcAYU0dVvKa7~N zLwo!!iS)yN%Mn+E;Z=(b;wE$wUHztA@cZ6Ee7wdiZnWf0vZt>3q5FHAt_}{Gk02f5 z3t`#Mdg%j@1mmrZarmT&vG12WLIYo2<8wzx*Hu$It&$&p5ZFP|j6h!kMtY zZ@Z-whaKX{cE-I2R`>Sd4H2dXD;jyrt1#*GMuvch@k2mJr_!3-UU&MO__94@ENR(B$db8*6LJbz{OhKG|Vm z{K0{*#z7Ip@U4g3@;V^pkq0YD9pAU2&jGDg`p2tu8TD@FvUEdZQbc$wreX!vo5YH; zA-<9v+sRs)`b>1yt8Hv}AHV@p1pDvaKhwaE+Tq92eKnF4gTS%N1CBG(6vgh@SZ~6S6hvil_yVX9$7cTZsp&wWj~1EY%Ms8DITMD z_yeH}!JtC6SG(;+S?&@FIjg_3GWhuLAq?bG&)iF28a05137g;+#A}WCFiCX9<0j;J zeFN6eZwneu9@3^;;Z10^NqHvKc-kqp6!L=+ii{$FvpEtQEyu*mKTWte6cOEINv#V;L<%ACQ}xQ+Yh*YJj?KvdkRiYUXA>ERE_7`sA#eY;bO-5o zGPmk(1THTw?jz3lEGXhM8Sl}rlb$kM z6+duqNdMf$+l-Regx?9Y6f;S^TwVpA=(V$9j#<_32ZfAQd!`a=kv{+Or+u+ObKnY0 zavQ<48OSH;BD1gAt;*z;#x_a+wzxp>t44d}`8yC z4;d{~k6yD~7{|;ihj)_7wZK57aKT%t+1e}|Jf6=ymu`6cEP;i`)6!R$Ugk~mdSG(D zJfeW=AjQi}7Wk!L89N19Kj^lBWHA_0jsr>CJFLA=8KYNo0ZgK^JkvAcU=}j5AK6UM z3uRGT*hXr5FVpXS4GQ+)R@8<03_;?BDjh9S(R>+>agFTMhy1s_mn62iqE_>FwpBG8 zSTT8Hgx)CR6OC;?P3eDEx6bGx^9H{EYG*s^b=0B^JnEId#%Z|g<9~erEFmAh%7g? z4mD9fzYs@2zzi?~+uBnkzTCywngIs^in80crh-ZqPV_4PCO?~sySHGMJy1=ul+l%4 zrVh7k-3ga$pztLlr7p)QpG4`^IQYfu#^Sp52$0jb{&str+#79qjz{I;G=Dc|o%~d%mNdU9X(L)%o(Fhp<{E*6+s$3ogCRUg)3j zudS52RL-BUPexpV9%xH#H9MGN~HpeN><9d zXa9!V1zm>}YiCYXgHZi^+#QOe-d}Yg)3^;7C6Xf=@%j~vXUn8TsuE0)*y>e~OKPWL zI)u|9b=K)Y``twx*!zQcJ!_y7Dqv}C9U%&k`Vewo_L(4Mm)+E1~o zo`9Y1jKDLbt9{__f)z%&Dph^oo>hKc@bDQvTcg`^l0+LrdbSi8d?LmsijXc8W&Ze; zg2)^?WY3m9vE%x9?fsA;cjm5J3GIcBbpq&keccrNyCrrtukwFjpSR^mf4k`LjY)Il zjz{g_0@@bdGJ$x8XVT2k^$b7sZ{X%)ZYr-4FWZ~rrN-e!R;O%{fHy|8TWv_)(iyqa za4G8FkF3x}9uNA{D5mFc#JI8|PYc!ASYWhLZ*X}*2xL;q`Pn`?DFHSMmo#@S?fLHJ zI_uKBK@`J8drDlEhGnF(6y}`OBTDyQ)ySwJS1q|vOt-5|-lZNF$=U2@uVXg+H9*BB zcx3I``}h;V^SV}aU@4V>9pX%s&Lw6q32b@(B#1d9r^;Bdj`mb9^(_oJ$n1cAHo;(SR8{ z)~xfbwuUoU;zb4#O16)Or9AyLMmf>D$%_yR;>@Vsn%7l=-LGSH`7nxDP?U~U!gMC- zaYi?L!PGww(rRj6K&}58P$?aln-}?d(&Mxzem%c>Pb5#?&C>`}d!6=*is`1M=7VeZ zY%$ShpxZ!WBMFt!KL3slmg1hSW6}WdM{M_WeD?mgm!=tPcXuAQD6P+@D9()e7I*YF zwfcTp7f>E2mF?Eo*TpCBs@l8~0YH}6g~<3O@xjhFv`k(YLOa;NV%9xut0Fl-zXexihI>2Lz+7AZVC=J`#v9 zAXhxD0YY2BZ=;b`T8a>O!hT!yld$mNaMzTY%Lc}iVSHFy0gK;_PkDfPJ~_e;5{OE^ za>hpxyISlcbrZv1;I)bv8@L-CH9;DBQ1^+S_%*;u7M^|NuG(OaPxP}O#;)+>}(q2z5neN_Bky(G?jbM z4_JJknj{PR`&9T>0Kr%dZEX@pqQaRLd1Y&&s2#M-`OgajuCwdWTbrAy*`ir2^*AIY zC7*eYuq>8Q;Gfx}ITcN0#U_7F4R@~AOG)$iHO*35ra3K|iMn^Mw~_6_ z{zlRGufcYa^vq?NKUhXHRYTs0Fms^T)*PU4}{?WaRAxuv{+r9MO(u_7$%K zUzg3R#Cqj_zegR6WHT!`=wi)6n(5}@q=)aH!^x<9fb-~#=BAmMnK@EQ^Cfo7G9YSc zHa~NRmlNs^Q0afc*6((nolP9&l=xAsWxH*VR7>ob761q*P8*J&@jP0#8BztX88L|- z?BB+dt%%7d9o?P|5d9|(Usop(}(J@g=4| z(;lLJEk85F4&pwKVk$?7TV=_z2l3{oDIQbJ=0Ffn+m3~gS7W9zyyJ0fJWRVf&dB{| zDgSR4fb-{x%&)%{Dji>k=$OPX%m6V{<)p<~!oYuAd2h?RYstfRUM>-adOT#^`5J)mxZDX+~$Itgxgn&7!KqZ7j@PjBg)raGoAFbAVW_V@tBur4R zyd*3+Ue{v3UPV`CX($ay6`psG=~tst8Y{y=Y%3%uA#cU+aUN=wC7*VmUSct)6=eR2 zEwiGQ)X>n!P3zxLKP&*@2kiW&gNJ7#Vw!L8Tb=_6QpX+pt+Mj|eDptmtGeqERR;)x zS*|DNKHRgGf2}|%JMi!Ha4)Z8U%6D0<BUiCx}wUHiEt%-H2_moI({Qo6+I)U zI;#%7(JDmBKqEXO&sN>bTVxoNlPn$VxkGYN$gr`&hC^;tcpE^dUHG@u=deN5jCcvnTu!8n9zWoV&S>%Xd>EGC12l46 z1NCo#Zgze)woeC6J@L_JwBk=6Y*?uBEiR{ z+1Wgeq7!`lV5#+kC_Qf8rFKYVxz*BJ;`Utm%ff;PwRTx7CNCmZRH?LvZK=wcR&(mj zH&s>RnblCL@b9?}Pu(lxJa&A9V<_b9apzWsG~P(7%7hMLox9-BF$yl7nbKD;c0LZd z8IU|bebN+1O~1=bL@v6j4JRXf2Px2abQ|=-Fr&A$Dh(Jfb8LLZfBvxJD4xHK*%8U3 z_P^$Y+$u52`Jshr=65w0BK0I=hAcd|4X#YMFMgTpRQzcaBqY7$R~-a-V28OQlTv9q z#z^rKBV1ARgY`|`>T+BeUU%1iReo0&Zlw7N6N9l~9M7jI$e#hJ+NquARqx<>UR%|v z`usNxrn{IlPA{s)tw(cId7~4 zpbCHOK|9~`Jh$F^{o&I=fkgV%-G1h{)=f4K%4y8-4FjCQOZJbQ`{5nm->D#hhiP#y z#U9V2>YCU;1D6|5D?#f2>KKpK>Ct6Fjn2hkqwU|uTq-DON`k{gUXsB?)x+x^-?8=4 z8=_zR*p#a<)x=roW8g?$A_AwT^Kg!`W+UnFs+@A1`U#oragj^A@#okpVAp9)7#5al zQ1}0k_TFJlZcE>=A_|H$6)ZFnr5K8U(u;}^rHV?80g+A!NPy5mL`AwtlP)4fLg*xf zP(*s~B_tGq03n3lL;13w=j`+B{XV|uJ=gj8>n3pJUTfwzWzEc*8Tfh?_IU4y;aOGa zzLnn48Tq6z!<8DJ?SkMxnkkK^YzI6?8&cxK4VjI4m@mu2+Hs(R_Gl2wUhMWc!cUa~ z4orQtBJCf7Fe%MM?t)}_P-EJdieanTiySsakV``nx{sB21h2J>#ChpN5{_aIiMFcJ zK0ETD&ZFnvy98HK*2S(7ydZ1hx22xgxkE>YIJ86XG1TH+q>?24)uN}T=l9fElSrFn z)xF-RPvdvbMG{QwB)Botqu1o@Pdb5pzE>9{&#-+eOgCJBgxY1sRs7+FQ}OrQc)pT3 zNJDp`3e?K(f8o{azjVP3c^$~UnL)lH?@d+!-W!&wOzU%xEjWEN$mI5K#1MHZBD1PgdPS0rDd(#mZ&$pWxAMA?uif`OK(&sC;Ewb2MIr`D=S*8M5Ri-auA(cV^bDXhR-`a2f^e_bm1y z4rf}Be6f`oUx?G_Hv(ubs-vT6xtE1|6Hjcw#sP;zEQ??r>b7yw&SZ@V z$eLPlnHq_Yh`0ed?N#?vjC$O^k>^GE%k_2%Wk_U44&_uUsf^b1DLgu-+$VlPO@h*H z^Yhk`wXG9!FjY3|nqNIPLUper#YMtQr*?Pp<=!SW{qy=(jT^xX<2}_==Qg!+&vhYH zhc8Kb>0P&T7J1vKuiOXTZ$)xS0#~P+`Ta89V?}-1+BQ3aRaziq0acsHto=v+(`Z!Q z2Gq4h*)9?lyx-LOJ*K%|%wwh|S=_ujRUC|g%BDJ!W(F1ELzo!<73bQ-nxkNIEg)2V z$p9p{8|hVTzE4Q0Q7tuyoA7GqE(%CwxtwmR$1Ck8M425ZCC!}1%DWL#vc#%ZXBkN8 zhr7s)U(E^qUy*y&ctZj2U1BLI%T>Zg?cHTvTAP`8ao-)6z5PB8P^vdBrNsz(k|dE; z=uhPEuu^fK`l1MvMO-!LQTY1e5|9==Vj_)hVo7~j$+O@5dei)C?IH(UNw4442hVjD zd)1gyY+FZ0&I}lZ2gyH6SMlrEBH3e2T>H{$&F)wYHtQ(XG0Bwb~K^>0Yd~E&K>B;=lxSn`KawHmg-@ zVdyKhW=1~bq5GA)jbG)X&C9q9E;^PkN-VNAiep#r*uKBwIwNDdv`EvqPS2$eFN)(3 zBTKlQG?}0-O2bpTGt+@ zO890$>$uAd+JwVSk6MdG-I363?@h{c>mO}`npoc21OXoNm$yT{&8PQqi!XCl11TK(5>bG$@aVYLHFCx@nkuBa8&Wtg zXSr1`59&45CD5V`($R)#H7*All^#cFiuWgq2OE|NnAF4m^rM`O^rIR)bs+RX6 zCuqY+WqdZ@;6wfhA#CNg^UgFJR4@B=^JEiSkUE=ZN`xRc9*t?s`)uVa^0a;jA&7+H zxSqja!|d346w29uJ^4_uMcTG&fTl^w0S6N3M>JimJ6WOyX@riYM2{&fj{a()mDC>$ z7pUp}qV?on=csp?fmiw7oh&}zgTtP^@^K9|Q)46#JBp!kTFZM=Yi_YF@?!t4tHif; z36(i#{c`DwM}UUxQfz+5=%|ayES)b+3o3rROK=``2g<>Y2F(u)_M(nfRkKwO`c*d} z=fSn}rZ);i-ijKa~ zbyh0>DWp^vN+2)(cr`PmIpRI`iRD7I*%R4wWsR&2?gQZlV;)7MF9reL)mdkEWfy1P zH)Z66=X55L{FG{7T7l3wA zV)FUf7gbeOvLf+@F@6cqcO>xE^^L=gh26P6FIQh>$H<{GSjwqKGE@`2y=W(XZp@wV z#fDU!AX5*y@OR@)Nd$9aUD+k2hY5ufy33yhw(+wq)vu_!re(?JsoLB6)sb%gdX>&B zW!oL64+=zTA%Fu>0u~Az?hDI_`ld>)L(_C=;WviaO9wA3Zd6kbb4ZF{}A5 z%63BYL8NzcdN>tjsH~@BCl0IKCMN?&t@2kFe7^c^ zOxA@Wb^Y7ZdN58x+X#3JDKui!W)$1pRPHf!iR}68mWNFkYm)2H?s^V|>epu03IT-U zLvigWiSnD4Xr=GE?XTirmoYfq#kh@aN|p^3XqtfMno?ZBVx6;)hg`>UqdxM;0$ zqsm#ozOX=&N<^9rWv6AbNvXz9D@ZtTpj^n8qs#UdKD2y zYChU2K%)F`yuQXVIm@^WigQg;&F-erV%_`d?0_L&VPoy(02rPdQ#cwrRa;_!2}rD= z?iuxM*IGsry3@**`>R&d+nYT7obysI=@QyP6KW*#2DgRhlzzF(Hwyhu70(N>aR%4y zJs&47CFqzy>~V{hTBwpKKaj;LN`8wV3Bn(|Yjo>AyItbmY5X}E50zJ+1}4bHoeqPFD-5qmLa$BY#AX}a=DnjCAaeO+>KK}-%tGjiwx?6kKTN>gmyaMK4tWhuIq+s;-n z_+rCBSh@J>v@f+8Qn)5!)S&Z;IJkfF8C5>DqkfP((WvieKiE90mYRh-b{5|=)&FMrQu=;sSq-tH$)?37flQ|k2`-IupjwU*5*>>_`r zk1Px)^b-o?L2_`JW39_$TT0=u?A2&`w)8K z`JL&XeG4$}IoahbbijdAmIj;w@zKu{Z&>2HZun+qR?=TMwwVvmN_hdutF0oC{1`nPxg@&fJ1- zdzJuNuF)_%lQwD+Vno2A?JO%^vJj2L_GPNK>#$pu>L4#d3dViH1`aPw_1(^yOZVwD zBktW-*q)L`IqZnIDmbU*7gL|sIEz2u>EJ1AT^c@OV9!AR- zPdpr^%;Mi5Z`YPp*)?|^_lYYs0=4P;?}ydM>0ULN;-k=2se z!b8Ok41vBO3j2FJ9=<8AC2L&&$NBQFPVVa;Y=K2mXARC?coq+kT_y&%l~V3)G1!0b zzVl=p8W8^x*{p466I9+PVD(M1Zmd+=9(^|0!I6Iy)W!F#kiwJKBZ%>S`s?n!rfj1E+LcIVY%@!>&2G9&iFgah35`vbpuI}w z@0dsj+vBoA?dpj1M^x5D4{8~Ap1BXjqqAT$uW$v)ah>>O3?)g<4mJD~YgswIlWQPk zy&kU?$DQpaKzw)6Y(-tW>|;nu1+A&z?j=+2=?Gb0cNh}(T{5^7ZP-t%jwK$Gn9HxF zVr$n2;coRr0>-uAXr$Ou%@U!=fBZP3k`$dw=(EK-l?&uuHnE>;BwM3CJ^wuklyIxW zB9;1F0m4GCHJ6L&Qg(JeRfLY+tG?>2pJuSWTfQ#};ls_$Gt#k*!06o~vc53%Zw#Fu zBOk6t1uPDqF^!-`9nMGXD`-R#vRs1;+6Qz&UJ^BasZVu{hi`g+)@JSTo93EW37+qw zDg9yb%J-*%OBKuFUJ<}Tr+~Rs55jlQun%k_K3qor+r0ZqWmliFH~1;3P8qx$H>^dJ zd#LsgC-;w7!wc;Bk6?W!P?ar{{=DffW-iSYo533zEA262+-Nayr)pvIaaKWRc~Nmn zMHXo50k~c3VkDtt^a5TRRdeA+#OZ8=)7_QR-Sd+@h`5bf_9*`q`2#6=}}g&o|1>F2F=3(3>p#PvE8YJlFPi^W!SYy(0^M+_D5n;UnTa zBz)7xy^dVXDW>nMcgI}q2UF}47Hxveq`j$2${wG~eJ;aYEuzLYMx0p?TS?DA$URa{ z38optQztygY7u^#F4{Ol@@z4tY2lP;3d`{h~TO1_f^j zIi*Jx_uo`BDtNBtp|kz7nhK9`NIY)Gi-vbWT2BZg{NHCUONfYd*-# zds!QitRw~FR)G!ZzFap9z#C@NSPHf0bli36FcckK2E1IBoP$9FTco$xxdkRT5S>#~ zp@nUJ2TkBG%60GcB#IFTS`8~qm$vnnFXvOR(wz>D8d(?(?t41hfIM)l-_U6;-)fIY zhF`0(;fAIYYFyhR$v!`A0p>YwLr$Eje!t|U7etevt`prh_bxWM4jS_4EqNm2a^|)> zsFL$B-vz&@40Hxj0kqARthTYT63RF_hKCH57Nxcn3bydf=z3IWKf9s^tx%{4Y3$Zg z#qWi$ox5mm3N*%hkAt=~hRk4!i4_>+vpC2h$T9AbzFy@q! zXi_ZCcMG_G9$p}~NYz0Un%BU_e%_&)E$RwIs>Vg~6&pmbjI0J+?N;kLNH;HzYv{a3 z9lGcdF`YD~om@VMi^{Rpn;VSX;2G9jAFlQMSTIqU>@-$n&?~0o({?DV;xlTLJ&MJf zZ7bRqmY4T7c&L*`-sVX>)NptFo09xTXzqm{T~=cT$P^V)r1yEy^@kuo_2a@f^({tp zoQZ~$v=esae*43oL%ddBpQCk^3n(K`)Gh`ItiwObkg;CSd3WNZd&D`~&4*+Dig6qn#+Id+X6hnw;Ch3&2jzah5%J z=c6X0U;ec2o@1zdG?gNWe^%P~#kCb&n(lBiNsDvlv48y=?$FyBIB8;$7;x>pP}&w7 z!WK~g6{zvuEm3qP+k9{gy;8c^3CsN?fKKTv*Cf&!(~sSI&Z4E+ob6)Qs40KPe!S$K z18=v|dU)7})V*gP*ZZ-{HDp>#Pem_}A%=sTO&xWH_5Q}mkq~+- zP&nBe>_g@UDl2`5>=o%PD5%JQgSVlTEW=+pOF#}T%-}*u?`cx3*b~scuCRM}pGlkY zBYVRcGh1iUkN*A>d%uM0OVh6QMAaB0zme~n0IhA^sSMv)W`$#pKR*RO$y2uQlmYRk z?~H;RAMyAVS&Owdab6Z*@GP(MDQIH_;seCZu`j(IIHMvNofYrER#wy^m zE~Xnnn7e5``>^n|J$FIBTS#q+w}(kS7Z~;ubu=!xU3-|bxsc2(v2E{aUM$(LN@5}A zmFf5i=U*c!;PDOwzS8c^UpP| zHk3Z6m;OOvFdiEvtcU@D%GVFd^0_oIAsH){PXIr=aU-*$oP6&_96GM#7jv$wQ`R-3 zDm`2vs+Rl#tc?a6)`Z@gc2=MHU#H5!gDTRtFv_qABy-Zogn6Kx`BkG;JcU1q!!jeF zPtKX@Jf2cojuZ8DYL^jjy$)hG|2TV=ke@PtM~G?2uk*g?g|hDm#__BK8EgKh9Ov)- z#d!Yt80gOUb}$2^jRKs6;^@D2r^MTP1zXD;90=fnIy!|vWpK^kFU@iAIHzHyCg?1_!P0GhPkQ4cbaUg zY=DR5E&b7B=|z*scq7$QQw)*$Z(L9~w^lr?Loruch+^6}jT2;gc z!(3-7=L#T9^1cr>_?PZ2?A;$emsK+_WObcy)8#M}-fOs-0kS{Yx=<$*AuQ8gbpCY9R!*?=G{2_28TlR%j3=9#40jXV-CBhUkeS-7+6!U< z-Yrjt=N)p?ua`~2>@x?-Ju&5*vF>^fSAa|GKDrFA&kmTnKe)#b$|9re&V@*Md(ZoA zQgPlug8!?g!2Bu}@5{S>gB&19bq2&P<$Y+AMLwSJjKL34DNu-6(#dxo))#|65f>E zWO@3cAy2=u3)_`H5js|EYUzHtv9e!4eRSY^ggTl({x(b4oVG?Cr@C&bU=GGX88^PIiK&1y}6MH8PpIk1Oe@WAj@7^)_G7)EPHsya{_ftnzHd&(1n2 zxLe)6+(;#0hgqT3;i=D2k#C2-y93}v$p-1YS(=)>{~bq}VM|;O+QxE_S=`x~=uqfK zoGz<+Nlhn;Md}B+FdLfu$^A!_p~7e z{YKM64w7dkjt;l`S^ZZ|fX>c@YfLF?!3TMp5}@)EKOQ^ETuV;Dzq@YIdFTBCoS*u^ zyHhT~ehY&f?S3ZHRKx`2_HHwF6`i|`vcV;ntNZaIuRg6n>Le~}QzHGw zn(fdvyvGdR_)hOxnyiB9Ty%P5?}HiU748j#&J;JgzboMRmU-5pzeXkm{wYQU>JpJ5 z*T;X+Wj?~2=-&C&*iU%g6nM`*0$`Pu>CByxe(7(KnRj>dTaX5r&7r&1jT+7~G1BOL z2X|;;&v+Y|oZAngx3(RmYp$#@!uIFr`8=+4%!F)@w5dEN_1x5CH3lDaZZlgdtKs#0KBT}0UL}m^IAIG+m`Wtklm1q5fjRBcvrRH zu>_WVPYA)k%yOmXn;%dgA_?X8=_2;Y3k%7Q3=3fqVhqeW2f3;VL&4DaUeCM$!LR4i zfd-J1<0j%8nuE7N9p;hk7~r4?vIhEK+)N?1TMAR7mYB2`PfSj6YWnOu2wal3dg z$;>b$9%y*ke7#1pHwC0Of0HZo`Ds0>Tjf}F5sZu(~Qv{yq4 z6-M!~p$@(28`nJztM~fC29*ZQ{N}jqy)Jm|4~}n)=Ov!as(#J-&m_oS zB&$d3ZhYvW*rL^^7I=u`yPh1=csLi@YXmrAK5%#TQ;|hWRaP0f4A$D5v&e_ri>9vT zNvc|b;=~N$lb+IF$Gdh4kM;^50e;d3!nKnYakNyYSOLG!VyL_|*`_fk-Je!d8fLze zN6_Q(X`7hZ*+ZM_P3(4g>}#lvRg;E|$GYs#_oWcbGUaC;%}BKmC&wXfF4hcMxw_w& z)9rSo?xE^t3=olCrC zn0PWZ$Bkv{r2gWCjM4`NR?s}x(H|>Wk8Dv>-)=wFJ4Lft_ViOp8T)uXd$5N{L-yLM zPm$!%lIn#_XjzEdOx$wJX_LlJtQb4Ic$jaCIu)+c7V=kBRMgMdBIEt*=evzM2ELtv zO}1~`7~*TOfKg9lS8nlNTmWo-LE|QVa;7zAi4X5z0ps@5D>)lUQ1;Pdc>l)#VUwol z+F`9{FLxZ&fKyrL%hl=XsR+luiE>i#m%YK8;@RRiIkwDR5A)dL($zuQ%ZeT)Kgsp6W+jX-j=(c+c8$JflD2jo}i@ znBZ)a3dmnR`QX{dhyZ?xTHI(MZqyo?ma7U2%Wm?6A|$OWUs`+T$6{7{uZS4pD~rE@=K-TyGWW-2amw-wgSSU7IGp+^m;@cZ3jn%XQ|H8xzK0z!&AH3v)NU~bs_bQow5tD_OJ^p|EV*@WO@hOglhn4yHXHuT!Ceay5T>j#zzFj96CI%=5-t7u1Rpr(m-;q zuQv1LAdCvLUFW*8E}HvF9&|1HiujSVYNvI_7R;wv@HR3o-j6QG)4QyA&E70u(Q$ke zb>`-I`vUHrd{5?BC{lYQh!4f=1L;iaz54=ku1CH(s7q4wV)2=0vvXXELc3gI9Ekig z<4xa8b3_q?oLkE&)8}dU#?Wt|&1nEP&jv<^kl)*>I$`w{z6brY2IUWnnhm^LQ_Z=Y;Y ze!1KjrG0soV@4a9bIhtIA*h_?I&?J%_BO98502g8&okD$I6pan5SyN)T@okuAiduK z%Uk7)oPPb0(clwBW<{yS(K3=GzJNV^nH9iIf~$}Ni@4^b19CNlN@W*F9hl6>0vr{( zen=g5r(!5F+ScsccBU*g;bmxKN_MG8@dDmEluPAFW*mlCK`Cc9+6$%@o*Dv~8+gVt zC!bKrmmpT7E5G*QzTMa3%u~-Caldd`PlNnXd^$I*Vhq&*RE&G)8+lMVnt@}+#K!q5 z@+su4JsuVs9(;b&7r_M#Z0^*l3Y^N|tpu^65=AdwS^t7}!s#m%=q7l?#lxC*jrB24 zcGIgr8ATGLXB)usZUL0?b&}o@Wt$M~)5VS9aD;l{UA@8TN}?aPzYPG3u5#=rx{COb6UwoYD@XNJ(W;SC;RTRF%)ozn8lnK?_nT6hiahBl@6|9cTWiB zF=q0d*LcfaHsMW1ZuA2Aa1n2*HcT~hh*yyE%MlQ#At#N+G*>**5mi{Vrl}BcNZi=y z7utW{Nb4w_rw`#>kj8V7Lp;t3IDWB?D-}L-(?Gp7Ci<+R=VHH*tgJA`GQE6`f!zSt_~KHFr%laNkke+(?Jj@R85BDhILp6T#-qFIuY2>3JMyL4 zaASmCh^f_tZ^CAW&Yx6Z+k8-WK;7>AacAe*D!l{4gXydB>!>vG4?<@nx#g!20puSM zaTN?#RA#jC+&P0495XG|b^w@pl^ft-!+b(VmAcC?+aLyXddJ~&QSX z7)r(S#6YeLU~C>7GzMDS{6G#3PO;;Y<)WBJlBCkYY%lc^8&Y?Z1hzX4xSvD@Z10DW zCVc0%`Te-2@pOH-((=y$c5?No3Nrv0gfsMKiFMLRr zE7n6#UAlb!kHC#w^WH8_MX7M)&A8{6PFk{AYV&DgBRG;^A|-3rg#~{XZKb7hi`HqZ zKl;2|F?`$_>wem5|ZQ5w#1_g5YX-gU^4pcOjsfve(GFmCk`ex|`776Jki|D?Xne~z^G zUdJG}MeL1ymkB72kQ|4$$bT_l!`v>(5*mNUpf-q3r@%A>bdvl~1`2<8Go=Fm99Cns z@A-N_K=_n#g7SNQ`%7FT)zO)X{ekOi3ro1b@h)ZQc6f@oiLtJps=HkE4ndkUvzm!I zwVfAiwL~zD0|8xq5#QADMJN2lH#JrMDi43M`G?@?w+3o132_qmGirGY9VNR+h(k0j zQJW83S_2!nk;JWqbja`Pksx%33+PvNaN|kPjHOH6#HU;i%r<3W4$xqCJaQTy^y~9G z&oNtR7#Rau;W_ci%MI(4)#-!niuSrU`H25xcLmM~=~^_$0aOrtId?rZ!I8qF8qp}y zMpVm=bS3@3bT53>5!0ai>sjEx*%%1DcNYO`pG|<~%8mtMH!^u_KRHOGkI-5*SET0~ z660(0_if6rpcm>3D$X0uHAQ2mb3;yiX_r)F`S>xUYQU}4BX?Qsi4Uniut105g%_KX zVSsZpSMEm>@w0HLCgjKy<}iT#43`+jjAd+e|LGg`ddG%Ry&Iwr;z}&IW2LvRgO;G5 zIO|Im%t7h>u!1pMdY!Par^+N6pf=CmAo;cgQBYEHXpwcIUSm(9R*kYsQvBtF8wyMn zUu?2jx~Gf7iYjB<-!!IeH;Y|I8nVB%%D`}245)KU{T--(27oQ;f3EddLm9asAXj6> zXXAt7pmPYl^PjT?JZLc+`(a<}%VI0h&~mTa5L3pVpAO&O)7nWsY270#gM$jf8W0G#UVzJsG&k*zjGXOTP`9YQiiwy*aqsvfOW6>F51P8>`(}Ufz!175!~F1U~RJ zr@CPeXH9{o2kv;iM(&;z&zge(Pjr+)ZQL;36^cP6_!XDmWXo8w^z>bl!WYfekAU_e z$kVp1tomqBWp+kkVR~mQRtr4W0 zV2#Jj@Lbf?6GVnemORN4Du^t`8zr=zKE5E9jQaDDg} zjl|niFGN_wStLWx&`IqfM}wWLL&Zq>A9^DV9@Tp;y^a*ly}-g|agZ}=+*HaF(4K75;u!$@AUuWOCl zemfN!?9H7vdxdz-MB6nNc~7UjENx*$7+~v=%zC4(r+J*~S&5%i>|e8c|8!(YY>bMF z8s*HR4`uSKQ{gt=(W#PqeVVtlwAz>GF+SVRtIui6(V8~BJO8b%-3=6}L^IEUz(;rI z@{2^#)0coyGt`?R?qJ6mHJK^6mm(kRcf#SwN@p?6OeK}s+^Ei4vL~kx$>n4*0GC)k z5s&xi>*zBam))~>Pa2I@xL_u416X(cxMCo35kM@Qj25sdt2Y{NyMEYwZpxozQNx7Wz;LqBktWF<=lU2(|=-$?X!4bQKN2Nd9jc7D9n{soM>id=Ca;S zc1b4LR2w3yV7@{={RT33Uo@Zp z5sj6j-*dX6RFF*9*n_$&rR@P$lYE+Da?qg1Z)nRRv`YPP z#rBzn9;4H=X33Jq0z45-xqxOqCgEfX zVZ(w=SKW2irG{fQe@6M4Od`Z4xJ9kMW)BR5Cmw!!^FQ1Di@{~2^A@o1x6TzaE^g0H zkq#4=; z^kfTok%t?@bV;psG=JE5_4>32k5y=?H#WpV3uYwNnN?QOgKSI<`2hZEx>*?w&&=S~ z&(!Dya_v?^O6w$3FoXBRTV2XdQ>;~3vqeMP3VxMpV8lZ|?Nf5&yy487`rFskKy>2H)l0zk>$Va?H`ZYF|YAvi)t>Y>f4;0dUtT+PE<#kYUbQO z{(Q@t&O&VnqYavIE!g3nP7x%rg~{D6Ti4g6MazEUWc9GoxM}h0J4#v>;lgk=arNid z9Jvl>PHHLxfOeqPY_h74##Uysd&u0k$WVjMcp?M50I3+n*GS=}TgGwjw<{N1uv>PS z71~;-bD+Bxt9#cu&eX}X6U*Ndeng~Akm?uGfyvXH96#~{F|V>uL>{kSp5AQ z_9uid>HJ9L?PNU9nRFcAb~oFcHIi`Pm`l@}yv%`e!QauxZI>jwEh+jMr5`eDQlnrm zf48S&f$xt+5}3mjIJyXOF2fH+e~EuJ;EvXQ#W~egpNDMX;Z#M~E2-bed;>#_8hY19 zfM~h@(w)bdPXe>B)!$#Ce&?i- zP6e?AdLxfHq~LTzF^@~aS?k|0ibn4-dd>58=!W^eTRD;OUpZ;){82B^gW?0KOg-{p z>Gy5u{eNwP>50JUx4Ysm&pX8vy2LEyuMn?uaw?1^esGJ=SIK<*GXj8qn`v{`_v-m` z>YO=AuAfU8W=J-o9@E z;+M95X^expDnAShu~iKxJb3w>E;CA#J<9nBx`|Mu)C7bq4&4{|owECfSSan%j~ec` z0C=0>gm_w?9^d;Nf0~ECY%Y-U-~}|5L&1i(Uz1x7=A4tcn9+Sh+jAjTsyR-*H^79` z5c@$bTOj^~gXjAwey5VLCm1S>E!r?&rdRwW;K~Th%bfCBIFX(H4V95SPw;BdA$wv- zbLLrgAXcsbze=xm_rmXJ@(=2wm;+S+{^nlVO8H%WyTU>=##AdgDtYdzCJ;;orw-~6OsMcRY16TGnwT>c%uFL3*SFvc_~wh%~n6B@(-z9{k;9` zsSt*VR4ZU6UI{zmTq{`KLHzEG;hy0mogF4$DQQbb6D zakS61=ZwE|#QslQ{c|yY6Ggoosx#Z@@AMSH{34}0ACT8R;vj;33i{SQC>-c9)pyZ%O@ zKUR>f^QB!vuI-vQ^;x2CfCpui*vKCw;PJ@*1nHQG3{=s?qcwJo>k*pzF>*c!;UwVK zLksKwJ74@8(q6jGrh>J$k5wlW08&wE8j)kH_+<}#VT)ML@v^rCWs_Tj<<>%-s*60b zB+rk*|96r3mjL(y7wRE$aa*$|oPCyER9R_S*g-27V8!!!((b@8!Cy!1SOkfAtYj`C z78Cv+K>Ir)|0QJM=K@za#OG*lL6hSX5@O!?`g<&or_RpKDvfO&kKCfHJ(;*pg3h(@ zyr$1!wzsy>> zuC5{fmQNvrmBfomIunynQp*?vpo(~+jk1ikA51uG4jL8ghJt`j@=yseF|QA9avn@Elcy%nffhfOIDT?V=z26N!1OOM z@vord7=AZF!r92=JDBMKZHACyT=A%I_;kSYg{14%6mHB(Ewg3Vl$o*d!?3V0cec+X z?9gBe-2dDMa7nM85zqg{CHps6`J!J!`S7Pw{88kGuPz(TxA+NCH?9&~|NU)IM{eaz^C$r>uVm zhLxwo$rWQxV_P&$iC^n)p{bQ6@f}*+*+7>0Lq;Yf*gfuD)zs#KmdSsJwF$r8AsNWzV@5fSzT_x^_-lzg~}qWP9$UcrCWp!|DK`d^GWd)|(E8ogdGmh=fF z;jB3EP*4fkE+q#Gd{$mDoe`2#hKa`D2xoZhRM- zm=itW5=nR?)7AC)g41x<&Gt4o&~|PmO7uUc)tBVEc)g0$Wv93&_;hsYG6bEt2L06{ zcI?KC)K~%jY`zPAi&9au=CXN>;Xm61ZSUd0FO;WBc?S4ybSmEPsZ_lJZ`&wqYqL-Q~7&e+8I;zg<;KpJ$r^xgT@ ze&f;qFUI~J584`ED0BF(ZF>lH8@297o2d{fuziZus7o1r1h($&%&q*$|1S z3bCowShv$iVManl(a;p1uHg?-fF-TMZno}gT>RzIm&=>BRe*U+s}T9!Rsx%KzCaQ6_qQsx84rHr&=;}O zL2D=ADsCvh0Q~O9WSc4q59TC-b;tWEkn*|*ccb3`WoC}At+~BvF^fN>CSSgPjBc7u zxI28hW%+IC+&AsY3Z3*AjjXGk{lnvT7}^vV!+l;Ag(*zx)BkQof-_9 zS?9-Tk;onk?yzocp)F@PcxXa8^28MTkBoK;$*8mS>#FwPW_HDoDbKKGsouB!X z`&Ykd~)JswJ4KvC73t&=w%hk;*;znoWd3^C-m(-YZ@FlRa`R*4~rlMH|Km3j?jOg6v^aLHa6nPYm2z# z)xgCUcECL5IUN`u!5g7@?}qdy?-e$2DUm;*P;YUCExW!43>vU-#e0I zbVd6=#S7k{^gGel92GxhAH2SlY zXYWORcRea_kGMAaY^;Ko{GMTyF}z$40O^t4*fOIk7A*;G(@m*~?hziM;5)Dg!8d0R zJ(3SOFz3-8H-(lMOXG*i-;`6v(hb3mJPTo?t)DiZ2|~6WX8DEp^X94z1bNgyy?h`v zeyZ<^R+dAqud%DgBP}qRE$gp7izgjpzO8G@)`iinXPXlSG8BL8Mmd%8$&1?>TkUIXjzR#Hj1{q86f^Vn!B(aI?%O*Z*6aHe{>H|AfyC~ zr-CV&wYg!wxEjEiObL>_IL~bL4RY2!$lH?G`(f}4;4I1k2KL6bC2_I}-H@$PS|VWe ze5VSkpo4DHr7dTEvP;g%6hNmQ<>OyiI*vMUi6y6liWX#~&_4bsZs9S_Qqw9rX&DF9fSJE(onYP78oPef&x%xEQBzc<9PVxGI)4y@TNF;*bR?g@bZ~2Vj}ra* zN4y}LOxS4#lXMjYM;=@V=nS5dWXIw0xJ|dducT2omdN(#HoXp^nC96@;0=Ld@vxk#806O z=BzZ^{82<~(K(fpDK({O_b{Z1PnQsSgDu1DIhTVSEJN`nU=5iXwHC}~1jd|xLhX!* zl{}2t9Om^?tgRX9xcPzT2@@l4m+FVqtwtcSQUW1GbWsr7)}Gi+V1c4|nL+EV%eFbK zONLQj%iM;<`lmO*9c_(44cdf^us1HT98wvwQ(qmJu3J81k5c^hn%%_+tM3k8gKLl8 z`fAWH_9-R~-mRPZUC@C8F)S5(@b-CZ95i#0ztLd$iivWl4&52$6RR3-m2c!u&|@QD zA4}jLYONOpSbMM=QF(k5QLEcM+!$NYYD3!PpFDyAJ|jBL`~A(FXGQDOgxY4{v&$=K>E!l#S4Dz)iu^?f_>L9<0Z#(fiye}&+N6>OJrD-Y7>!atE zmJP1W^M#-<=coCy;G&hv8b%iS#9R`xuf#?6Uz)#ETxxf`m12eAy>dzn#-ls0lLqTTOqzME%U1S88|b}dY5`(TI7_?nM`pq zZ+^f37CSTNHgjgF(k#y85&vJ$5^2Zih~=A^+)lfSldItwm?Xaau{B{4x9vOo$z`Db zkFU4zin8tAhG~>WB_u7nyGw)-kd~plV?bJBND(AOVx&vDW9Ss=7`nTL93%%A;+y-8 zweIKlKI{7ru63>R+~=|PKEk;W;taj3pG4xHW$_1B>3=AyUp%$Q_ZmDXKa<9DT+X!E z?n$Xp7GZiyLhSlqz+ydhFn3GYU~cON$KAocx4q**f{3Ek;!B!=`l%i5er5xbG~s0UWD(*8}VyMp!PD!W}pJ;{BpC^j0{(f2Ol@{p(rG zx+wYi?+dow&cnsa!}$cXJ2f01zOj)j5ug%IG9&saiRa=gcZ|HDu5tfVbrcEB;=Z^_ zbj7!Yi0MKD5u0WQ)-dJ-=su@S)=FKxt|Ns_%*`+xPi$OlZc(b@5<`sQV$&;R<7R|C zWoT3ZIY38(aXUp%pys{3)xs<1Ek?j#EPxo$XRVIBxZQu(VCKyDmOR`k%WIl+aOQP! z29tbWckCwh=$+u}`oz09P2cn=qp>qhv0k#;6+5yMYGT7zj3XFJ*Xxs4lL{#b8yW!%YIu?Z7!MN%4k#l%D&&xGgD4#{@x-Z_j z*S_L0$MRFcZ^8c!BFMfttY;@OEI(|B2hxo$sHmtY1yr4a(Mbc9sII|qp!I3Oaz|i? zgb%65*x;9y3B{!+^TPUcO|$b!;?#q*c!LMU81#08goKFfa_lk}ZNq65FDU>*)GXhy z#ti{XAu+}fNjp)Jn+37k!C(tz zT$Xf9lD}Zyb*Gck)P*d5MGM>ZC-zlK;+gvJI`H6dOV0qleK#T+{}0(8ApFS{FtL}5 zCCB|=>usEj@4N*$Bln#TYS!;rrfF6KWeS15n=YL;aZ$cMUq9&Gy)7;aZygVgo>#Lh&({Ba8RWer=xnNig6imGD3sz+ZYMf= zd@Cu?6qZxI6I_j3s**~sZ(hHdiP-0>H>}879X6Vojm7!*vm^}21|PMGfoNUmXScR5 zS_hvo98)*tu3nksGoksj6psaGAWorL(vYOA?fyK4*YnZ>H*Fz<&9BqSvN}{Ke*Kru z_H*vzFEPR!KcmwdwqKWiI$n_?J+0Omd)tf7Zm*yc1|#yPdusw#QoFY7 zi_Omi#Lbuo1hfMg@e8fwD`|UXT8hn2!G^l3yUCAT#`sEqaMPvE%m`lcF0+mD8w^b8 z3M2unj4L&*o~Ow%8SEX>n(0VqPKhg*I53v8MQfId9yFxMTi6Q?LN0VB<2C)FJ=--t zLWu1L-|kBYtLtO;_`JI-&l{Pt+#HYYuS1cQ)OZUO0JziM+ zh!ZA{bCAert|{D+u*^4E2YgN%wSFnXokeZ!Y+HWb29tPE!W^ZlDG#bCAXO3nDfl`n zGsHAnbdak8#^SJS$NDDnv?%VgMoETyj1J^lVm6tYM@Djg{XX29X8GvMxsH1%&9cIt ziVl!|skqTc&Q6Zg3qWTY2xFHzZUwA z=ICP@-<&!uwItTv>!QsCwa&JT{k&H!r=gebO_eGaV0$61h8%Muio}U{XUA`+>vK%u zB!bIDw23hXl&V@A)}D8>Jp}GPlYwj^Mb#3shaEFFbO?Fj$=uy*@cE|1>IXFW%&Vll&; zs(T`oi*{Iso~tnoLQpd5tWHH3wm5hfMZQYDEtLpQTF533dzmj_nYdkTL#saBd1w>R z&JalR9zEgupFQhe9J1Y-ZnUgG#JJ5@$hgI8By{35Fo~y3Sj@#y+hjW&9l~vkc*>&o z3oVjq(rzHElg{Otr>;kReT|HeD#MnO!w(_Jb;EU*lFG*Zdj^wHM~hNuCZ6DHeylq? zI&H)x2k)_k7H0YmW;A)OvD@i5CplUpL`Cx(z}oW7sGA`#sNAN3RPy_0u6ZmC1FMh0 ze4NJ|Ny5!DGdAU`x7$M`$v@)BHHesI`+XIb0p8Tu2$(ba@gJtNAk)v0pl9k+m!@&=j z&~jE&2Ki~m#C}nWl5KH=pf&C6Sh1IOSl6%TIs(o9AI^()haMU8=QPoK4^UdqfbEut zk>x20yRoiYw;Eu5vt0CXeb%eD;Sy9Q2;bYy`v#a6(%LM<1IWx(Jl{6E_7!unjU2j^ z-^+=0@6xFDOom}_g&h#*79lGk+X5}EG~VtA?Km%0GhBiNpp%$$`CvJALHt?CXxvgs zvBQqRv|ey;w%eZLPy^w@1yovh>|DEG0W>JC-sj_3l*?v!Q>?9O9si`yACzZu^r!+; z<$a)ix#R=#F}zwx+`V0+QLSdibU>l~cC;+-ci&=vo#Hb~5Kga}syL*Y;`O6{4L*a} z$RZr!nkBvQ&!^jsbxjH(_5%Ye&+i7fOMSN~5=pMVe`{Rs^EhoztvpmUJo>#7AT899 zT_CV{RAi0z(OPs}8McR$J#rrs6{vtgrm5sL{Q(}*@f8tm{=2Pks26b9b>0>PIeWWZ z;%>|Kj^_V4xfsD9z}n5l!5&&Xp^Of!`_SOLe44EgwS7QC$ifj@6Y-CK(D~^QPfL~) zRbrLp7pX=So0de73IJh$m@fM0Szqvr!XtH=yD8nRWbKPgDZg8LOesE%6jR zx?3M3SAX^HdSc=SvUqz-Jxls%bXk+dI6>Tvbbk4RzNqrxxp1pzVrqh}@RT)+2vpxN ztrn)QvfHa{ed{j9VKU3cc@rT@OBjCqPVi#i-N10*oxtSC^NVM-oCkeJ?q(d3<4G#j@c0^N(X`?jyp{1=fxq+PK^m8a(W16N{}6-sLQ%{-Gs4 z)n{vWm45qlIHp&cfpGdMOX&&m=qtzK@D&}g+&nwo-V+^7{pY_F;|PigdE&mXM{<#N zNz2=7im37H^{zU^sp{rAGmk9D?%24iBl?YnA`imu18OK`!c#RSAqH09dy$X&m1TpR zC0ZpX(eS+1GCFEUN|(UojU#bbB}&Aw+cBcf^WF6Rexzw0n$jM5;Pr%nLOO2WMdbuv zans1V>}WK~e_2(&jFDCj{_4ICA)fYVbeG14PHSztF{Zq z){J!1%#O8MxzCkQU0jeR=V)rbz?u$NENO|$zzH6CQlyUQnJaI3fjcJVK26E=U0?As zAgs!>sA=#nHV({s>?IDRzrre3R9N{vc@p1~D_3txB1kOv-)Nm^ylE_~e?8rPce&Wk zdRlGSx748%Md5e>n)lr{E%mCl>)ReevpwjV&@Nd}kJGJzmkh>tiS_F_c0$|)K07HMTm9su;=3N%?^vf5Q8gd-PR%$L*~+o6dShrA z>NXna`r{a+_VFu7*5(wKryiy7BLq?hWLYMSgu9ZsnkNMW_~F}T1u|5rN-e5A&LWrg z&OU1Mb0V2L=G~^zrGC#Q+30OvJ+=Dzvb^4nJ#8RVzeC$`Vu$)VLSCtIwOAMZE`Mau zp;nR~>W=dCXR}*`s#Hq!_cbbJ^Yt`32m-VDvuyopv@FmK(aclgXW=^$ZT822xjR&t%$v{Lxu0jX0;<^wD3|&Fhk&? z>7^o7NI_wKOL2-x2}EJlzi(IkD}7&HI@FWYzK0zM(z+(H3Ip225#;ON#njE2Hb05s zVoZx0=eN9i1xN_iqsyPM%ooBt(C`7dm1RLO5t)-EOOE#X9BzPo;)w?MYfQ}p9gUdYlH2-7Pa5LMH z*?tMFY#IT2zAV&}Yk%X?dh&H>i1dW{nngb;PKQT(*tfXD)tdc~XdCMzgJyL4 zUL^)M{A9B)&d)$)IIg*v{AN(vu)e-oxwmOz4X9i9M1NR%q*x<1Y`#Yg;lV#NvIqfj zFSoQfZJG$PG|bo89+z>&yWq(gmKnynfDIve1%rd;7Y&F>@c^IVMO589YqDuZdNVD) z1BJ~uxucw}{nimPP+f%RY%0)2Sa07GDsTE;G2PlyvdNb@A~XAJDV*i8PC>ADRV z*uD;V~jnh=x>H#QGS zFMuRX_iANo_X@z_&P%N+44l+ti|5Trg@?ffmXu#)ho=f0Hu!I-`_npQ+kc$4fBoW~ ze@wVn@dY@X=WlM_)4hp6fP)!>V{R+e$LJVgd$qoiimT4SWy`%fAaoNk#-FL0;E z{6ACFxfL1DiW=u}ZRcijH{I7PIZCMyC2#J!h#1w5dv&L>{#y#wx%pDV#8h1ptEJ+& zg?Y4t{IDV9PI1h$C1hgUG!cr5_BuM@cPf!0~_9i<3QR%n@;-jwBs5QE5oZ_b1KQ@**fk}xi_a%@|0vU5!44+GWy*Nu$to09i*}ZS_d7AgG>$IEpl6<)(F=pZ6CC(lN%lR;^d9SvE_$d;uc&{y9Gi%R zBGA(@`cq!CPcOKkZ_H7!Hm3s4z2{!dcU9)^sVDZyF-0rNGlA)-nrGLj*=ss|g{y!3 zXe_5~XII*Gv~1^#$m*hB(WM^a{%ic>5rJ{>4THwlnoyde=Y__CMJS%K+jU8)R(|i| zc>{y`<-jc|Ao$HxjtJ>D2OLDv{BF!gsjoQ<7F$Q61|-QMZEVwAd6XsJgv9FLP=X4) z$`kmeuE^{gh^5Y~;iQuBuP^9}Su70W>qWOCxuwMrrKu<5ujpJkvSt;#eil~*Onr1m zHoxs0N~EHVa$az_VGO)gY7V&b75Crzp2d^tyI&-A>pInrU+>oT?yeTqeLxWWQzg@| zA-`@kGQQjIkUACFL7W=Jh)FNO<6#{` z9Mcau5w+yl1|t;YMsk0;Xtp`mlm;gC)Fs|uC8$r}Ga9|Wp)FD+Rn3s_7S51DTs8`sv@_jI^o%D-IZfr*?~OWp zZ*fEOJ{r_|88fB0C`9xA-}HgClP3`e2j2jEW~Q`G8G$tYGl`YWo=5E~7k*%{NlEHL zT$YyouhLSVgT~rpGRK~aI*|qL^BBA1*PDHc-}hqsp$wwE=8;BRn2PrI2hqbT?p1^ArWDbQ%Yh(4T88A5MR9^Qv~0MPBP9hbBI4r)a581!>=uamHI2VX3X6wCdNAMvga)hk%xIF-i{Q&SE&6QaBtU)Od@mt-x z5$;5w0ij`x?tUJ7gO#yy{;T8aS-;ng+EDF6g;FC1{w8)f|DGI`qRY}*4lpK@8bCS2 zpsO@R>Y!6hsR?=A0Q_iNhyR?iLn5~ei>JaljT+i^^`E%EK&%l{4U>jRtnnC7AhH4Dw%7}<#upicj{f+GnA5Q18O9$YfVJ6<>URG7xfc>ZI( z=YFXeUwHU-q1S?ONM-0gD4pSl_D?9`5w=%GegoB3TF6Wx@rzzESAoH@!eQ_HP4OZy zc#3J!IN7fPZWvu`*xpg+NOCtZ^>6x?*#SAaTqptRDXFf(#24~%X+0U2sya=b5u{Lh z&9-ay+COCy5nU$x_J%F@1)2$g^j>Nf@BFgPx-+iUeld>-O{Udtf80I*^XZhchIUfE zRk|r25aELw#sPpte%2S_e{A`bBz(K*fU97nG3h^Uq>Q3~IEHfi#zp+j zN1m@s(S)p=wKd*u`S*AJd$<;W<_$v^L|^^8Mv$* zKygNjBN@8^xGh`=6(TI%nTIwvvq&|{B-|q(u0c2R9j%VNZPy+*=bG+s{n^|8-VS)2 zPK`UY8}|9Kl<&WNnT1!zG4!r>A%%0W?A3m*v1qD~Pk&0gTj@nnl~(rUf@FP7e<^3w zZo&63GL0rR%91*McsiOdcIHH<>@M*5y~@n-#*^X49Md~(Tfg9k-3n8Ibrq#iUpNMS zo22Hq)v}hqJ1sg-A(V{WVLlk^Q92Oyx3P?MvOJWRl#*xmFaWoAr2y0f;&;HyW5(+M_X!>dgh7!Ol*#j z&X)=Izr`hIEYx|{U3>v}dsG)zu_j!i>O6igsHz1#%JcGr>1IF=Y6_^oxxE~Z9co@G zxg{bNn}M74FKkFqHOm)F3BCf2w1$BrQYL7e?N0o4W@er^-AUSn(8$$6oR)=YlsknL ze&4JEX3-DOMT(T9#|W^`=lkWMv07!@<=dO(RUc^?99{uiadwww zNOIUYSa(ohsDvKWH8o7HuE<=4 z&Q2ab+>3zDryoTIPoYZo2TBJ?*xO1jH%dB`TVoxPnBO$DcvDWGj!o5$^&9>Xc&vq%}IzG2Cr=A~OsAIc+e}`Y6dQ*504B^nN_=$a+bpD&rcH*Gp!80n$ z`>t83h@$0qX=ZTQn{%l?JY>fy>|}E9&$mMNES=Sfa=!Y$;?u@@P4W5){^^)z#mM?) zSA-au-3w#OJ!c8M=5eo3ga6<^uAPwYwyZeE6}1Nd!%QEAQ*>8s60SEY0@Fd1mqYJm z-bSjP?$3h0c3d7VKMh1Iipk5$|d0psf;PR7Ba8OL+je^#2 zPMVdsK%cuz5#IEMmU*AR!MtY@4$zZ=K(_7CGVapnF+7gR4ZV~pk6jz^49;c86cDev9%I>xUe#c_gMgSbAY@3+iJR}==$YA z!hJgGXX_e5)x;a=es3m90#y;)6Mu73XshMBb@|#2E=Zl=7RtS$Z`->0fj<|1k25Gm z`o7P9qlWSF6ukp8R%K{HiT1jXRjRFJ;%A)l{axQYHK|iy*}lYTng!ZBL3YQC^)O1( z-Oa3xjoYTDX2ydJ@urNeuC+8(SB6IE9RojUUyZ8a^SqJ4zgAiJyZd`G%gV0Uev0iu zOsL3g4YumqnDz0YxR0ct*(gcorpov#Qht<49%c0_ppuYez^dc1f79cBDQP}?Z^R1J zcGHp-t~)+%{LRE8 zW%)2zm}S9{eljrFF9!P764(k`6JqW-K?DH_P7F$9thlqNtlQN?I;dQ91akYVH27eL z3V0peM8rgo>iO=ZZtsAwJG~ZDCXFi&f8{G(n~XxG4<9~j&yBKS2G-${uIHG~IqI6K`ZC_o(Q=rY3(V}u;?`-#m|hBSCWEmL$9`(`ey zZ|&%yvayYHIE?J-qA;XB?(-e-0O5U}|B&?K#A+0(P;)+O2e6gS#XW4G?p@ntokN_bvcXi1K_AZ{?mLNrbFWH@T?-Ev=fa){y=o@&tVl&WkTaH5AeLAVEGWJn= ztlrg1#=-);YNuuhz`MGS?&?qqLbx6v#4Q*D{tAZ%9#yCv@@r6kdk^&+r_?w$(Tgw$ zaE|r#_SWNgYVxcgDh=B^orbh8;OuB@NE&sv&@teW|M3A97s~P>b(o1&^lYgwjRCxf zS`oY*GqnpI!^**ZOZbKI5f_=ki#l1m;NwLs{J_u-&i1=i*l8!}`Wu_06@bTxM8hB> zJ+A~2k&^%@LM_qblKi{&%RT3ci+(&L+nm5rVar9;*Hyt4vh`F`yx59C9Cmv_3z zH8CPo!*HVY+Zxb?ZnPsF+G6LtN zRvsLYauewdQ=d)zOeaDQ;gegLTukLIG`-&2$BVxnS|TNgSn4{F&cTVzzPuHXq&s?% zOK8uRTUB6EdhNGjv70(u@|2&hU|~Z;S{$fjTVFHz&)vG-uLi)hvR{Gh57RHa`7uk* z9zu2Qv!DNl{f|HJpTDG&)}T|M%)$npTDbowj_J#x$ZB{oto`AmU)QQZj4@J0%y8|( zJm5~O!?&f=@j8$qh$~r`g$2KQ{uPTqXOByUwqB&6tHUH*H*8b#F6iOQ3nT|W;CXon z>t}-(-iC&0VAB^38$Tus-fk0u98PiEWIq4x3|~nhk2o@8`k;y9?0fPoPJS7ufi2kmMbGM@I@Td9k{-6`Fm_wY|2@ z6O%a-yvma8oZjqI-0U*v$)M<#j5b+A5JEEWe7tF^x@EVr@M>8mOOgsFyO*iabQUa% z3P`}0yX}f;=a}g3XkINGD9-&6yL{)rjr{;+t2DZzJGj=uPYL0O%*qReb*ajc1zZgU z0*l!C2ux4#3PU*t{E)Dwsw~vW$sXmkrli|j;;`t7vN8iJ^Kl#aF9I2xJl1aSI&a>l zoMm>7q4K(4-=Fh~p^&aB{N>`luG&{MI{TE8vu6U$nFfrvNmS16FfyYPxUuMcXa}2h zrAo2uwPlBH`v4o`IuZ8Tci6o&JdU)TZogGH1G4yXU5o!4OEQR?zfXF7&9L|r6&02K zV^h|YqR|_a$MJzM6-%an`a<69@rL^a4(S4_*^O5+OZW&IL-I2NX=On{PoF1m^c`$8 zRGG~i_hFfd@$2FyJPd{sL1E)=41Egf#MAXyQK$;+=;%JqLj9HYWNPd?rfBenX>T|t zld{b*62Zc^z(;{<(%+^1Xq)}+LT_m2j@{%6q2AZs)weezjHp`=YH^V(NjmCO>@5c9 zy5`5CN#OO%8f&UiB&VwS=-GL4Px0hs-dH_Cf6?FO9U0(dB!l48g1IFPUsh|54=r_Z7d* zH(1l@Xd;zPSb1J|jU=2!{(>nLGGKZlT(DhOHXb)TF&Ct9RpwMt;afZMm_1H`q}k$@ zseSyjW%A825dtwJ;q_MYinS&<-NDuBKwcOwwl&zr`Av?&fU?kP*$oW ztb|!-e|#9w=4qSm>0)dn9K41R40YglobQM_q(MD%p`~X@*Kn2nAZ&EX=jc`Yw=U|Ns0ESp014v*o~DX*N+W{)w)S}I6VVq)Ygq&g!B?e)v1|BL)C}?U z#%*FC2usbJ!zy@`!OIJNwK>WGltipg>{1Myk(Tb@S>YrNRK5_p_M{KF)fC0D2md5^ zn^s{PlxuGW5GmQ%SIdAb<8&4OJ}4uc3?u(iIK|qE(L5z37pAKhko|sd!^^u88~zAD zpAyOao{;57+Qy_06%`x@aX0?rDHV3n*>!EB;8W3tWE;O`|IW30IQNisd*5FYvw`Bb}hxX#A`J5>II!!Fda&liJ` zW3O`rUvE@_oUJM!;coxZzbEHPDb~c%0poo9#4nO#*NIaW#SpMnuB{zLAF?o+CuLw=cT1vY1=U;O5 zkihLl?!9@P_FebCyg1ZmanjN~Tz!F5;jcVl@ZbL9+&Fs^zhp)k20U5RMrjeSM6+Ho#5 zZhn}v2}|w@hpl(JZo@L%Pe;;Tl=z-R2?>FDaCcmGY@kg0GxG6-M_UhWt)fe>E z#Uz+2Q9lzHk)S;oN1R958$2&92I+=2ZeIZuoM=!0O&}RKK#*VO!8N2jFJ9Tx_LNsq zRJ@vS)gbv{{{3@U`G^!g#paO9|Mdd!lj;@@2DM18Nqdu+r4Z5-={AEGmKE+4a4u;Y1_tc$-Mu~Q=faP(8c2ei zi(OqnM}Lj^ow3#b3h-GAeE7)0!%;&jEgF8~Ux_kFZq7uhz0^nw*9Ea_FWlbqcC<&+ zdmZV(r?}gyPEI;gsJJ@@!XFTKE>8fvEdSul|ExfrNb_LbyB%px={v2}W55_+M}g;P z+ry_{+rqlQ6e8HxX#BXQ>~CAM<*@*9m*y-f-nT)je@q<9?q)(k^gyw!r8?!0{O{a*ZTj_dYZBxSnv(3)?!JMtg{ z;w?juIz!yOxh9g+gQ2L?MOyLph!F0_@y%>PMpT;0sfmm{t^K&iQO%IWCCWc(w|{Nt z1{l^U4c01Wu;h{g)=x}ycV@}derB)Q`1?odFcp$Jr`SLT1c6elbs%vz2xVSbJ57T# z^R~A~-Ai5#mvS!8a}|n-_FGMtfq#}V0QlhaOa8?BL{9<%^T1eVu?xxpfEjacrz14`r zOrxNR7Z!OYP|ll$7mSH7=L*A|!U$SYTo$Nqb#|S4m8`4#9LK;lTaSi8K;UPL=0~TV zu%7z%`oS0D{N_%U4N_)}AEx}3gGQr}s@1VH`=^7#&dbS=r`%=6zp2(BF$?O>Y89c$ zQ5tW#So`HLSx4MzvqIN=rNd{dsmS0o^bMp8Bg9s_G<l zhB%7EE5Gk}PPE7BR1vD+Ziz#Bq=yIy-CV_j{O7_yc|OpJ$rAS%cPQvPpDxefpz>phEJ2)Yi-fCfoX0!cJEBROPc|S!m|us-L$x(2J z7oJ#~Kdy4~^mz`cdb`0UkE<-Nt}+L-eiJTen?R*d$kh-M-uObU+5j@2Y@sAgUSGWJS&HGQ=U8 z81?i|N3^>JYm$(FK#YIyj1N&e<+f&)`!Z2+x)WDUj)rX3{L!Hm?|a$IGdeep3Wmz& z+hv<7Z9|60VJtmkpS)pvjoJKAM{fJu6ByHCpHkd@^#av&j%xDX?+YJiVR)_&w- zm2&cUD)awLKkLHYcj0W0Eb)zo&s$yZ(82lHXi`4@9k~!PG}UJ&oml!%`DD;a&IHd| ztb0bds38%F<5T@XqOPXh!SGM9bq>@iA~}fr7f9QOW3-Sshj_5?@Cv@m7(FL4%M7x6 z6GE(u(Ij&8&NS`fvcA!aBX|MDJfzAI2rcvYmbk(e=X0$;Wt`0XGG19m32d%?wcsyV z+$Y`ug&quUrB1ZBMlR2!ms?e0JS&X0wf!EK!p*&H?dqC|9Z#_-jI5m8DKke>D?9K_ z=zmCiU8wInRAk|lTc7TOiRR^w@5n9({XB2yw-lcV)KkM zji1x{#3Hk)?AhO-n>A)^*Q|xXq-lEQkw3wWR7+a*uV23|0?O2mUILjFgvM7W4Ih^p zCqgLPj2!YiVGfnkD+HqIV?d^JsYbfGv$Q&>TjV5E{*U3irdgzevl=s8-+|fwXTDx~ zJ-CZ4+ZJ9@v8~6pUHUAaNshcG4M>+wb3$JW67Z)Q{=1xgYAPqi&N;|LvVU?}KrE9{ zlar&gMktH;W}n!Yb{L9;8K|hd#tx<}X)(%Znw2}*O`T~nUXFpAvvkg@1#sI{62B@Lx9}`gb~~qxOmjzde9KV-^%k}-bfX$ zzZnnG(=EwSpoiI76nUlk3huF6tL`CUEUOx{neepiy^w{rz~uQRr=ExXc(Hrm6W5id z{F)z##D)tKjbV1%>7=CAr$7U3aH!CR^4{^J5ay~c>E*Eq_ivHUosc&9-3+y zm~f0drdwqqrD*@HWtzA_Jn>6sFH-yWKu;h>;Q?3~Oti1=vqzX&yWsr*(EMs>7u?}M zsG&POozrnyu%EU+Y>wIDW99nDJ&}xwvjRhl-0_=@TR{L zhdNu2W~+-ta9p@JBnB$4syI-uyZ%qy@*hCvuZ$z+MT#$6dt>!%T31{~Mm=yn!+NdF;qQ0Q8((${xa*~$>q!b zyHumo7sxh0WKdCT_S+>m!K9-z%7j}b-f(cOgh)Bu4B>+7%AyS?tT|qj;2eJoMfzcA zOY!n=JQPR8HKWa~pA*R*&cRJ3>*A!_Z7G757&Z`gl{P{7OI?Ln8c$38DEkzsbc@yq zDfRV~e+NDmC!Eg7BFP3uE~u4H10e>+*0DMe+sQ}od%pJXg4MB2o|-{>nmn`T_U3=O z*OJR__Dfn!t2k)64o(VMj+hGJN$2%-%if9qirE#`0Xk=m8T$M)ml?%{!xEz&xj z4lsuJWTDR|L;$Zbzl3ve;-qr2xnmfEh-J;K$zND}b?1-6`!!`=Y&*3=`uPzS~OUSL$M^OSg4_#ATVy8Kq@^V!Y z!LQdiJ4^M~xHNj%JFGsf5r&EbmMNG`Mr$@*=@dcVE}#@d;z^<}j5V5xScNY0|1^)h z1NMhAr76U-@k=-Y3{uOW#qqvXcA?#`NyN*|$SR3E3pO^~jGjBZa%ZKDGmj%0G$V}h z%~#ON^v-mPACuL8)%(JATuQ zTqE=oeIlX(^m-ma3!QJW$5$YTcCssJ=-pA29TUXg7#*n;wZ7rse+%-=oCadP!6GM| zQs?9d++s~JN6OKT=} z?6Y(6J}MhdMJcW4Tk6EJzluD_trN9-qsm~TtJ;x2A|oMMnpsj|;xlc@apjY%O{_02rdD;A3= z%8WEmjPj{Y8rlOkzrU6wVX8cBTa_zd+Kdo8hx7=44bDDoQooZoN9#;B)!bW`&!`elC@ojx(xHHMH%{tpGZeVAddiCr>y@vi}nxFH% zo%+RGLoiXPJM3hV;P5qF-@elQ>oPI3sxBia}e`RQxOwUX=2A|~TTer)IGCpqz zyLOdW!S26v#&C17#HNg|mLhr?fB4RSvTJbs(F<`H{kDX22Idv<+Na=knqggIJIC@| z=|s^-h6y;iglQFRbUwnicdR;bv1*GOT2sU|x?|kB7$As6;S(cE%P{%_`}LO66{*s} z2P3QH0=v>3jIP&`DuT_s3s{&74uwNO9gg+~`)o*|x{Dr#d7?7-;i5eeZUNvU_M002+_|f^E_T}ZG%h|m) z{Kf{y%=G@I27GxwIp~q0?0?zzd*q7n1QGY--p$M=Z4l3LD8fcv23HN`dbeA z#SaEqd8;0=_st%%Zp39Du@*&yg-;RsS?RZgtm7JKcJGM_xYK@rRFO(SIqB>yGSY zKUj~uFdg+WWQj&lG~ia0puDZ(MPSb#dzPD{Oo7M;niLA;#?L2_KBC4q}d zdJ=%aSzIk&?z-gWSF(p#AH6hKdh?@vH#??W@-VF9!*qT}V>x350>Od>C%8Kd?g@4tXeLVNOi? zX6TMaFfg9)NtvIn4458O&OsXm=;EH(;9|+_?++pwZCbFf1(x_JgPr&GXm2P@}WZ)w6QSRB|s`23GUi5h!ameA$Vc+GQHwnZ*U;exd0_?>QMLz076@J8sN# zp=CywEeKRyJ9-fNkcG2o?=C^8YU3}g81-0UDfCQDEIFsTQrWb7BJ)Di)64T@f7aYU zTY@79pW!*VluLh!G7Xy<4abyB6SON+kCNX*EX(g7jK|FJraq50c%m!mX;ui_$gH}_ z$x)9DM1T^)JYU~hoZ>Ek4*0Uns_6a^{nERJ&h!F{haH9h7oXhWTTFuia*VD%m~1=Y z%2*Kj%y)&w)NX_IMBkVTNF6iZHyo9QmePwTKa}a`p38LGJ zlNx3VX-EY}AIFgG{@=gaW0wBqIoV))x@6X3R5g~DC(10MEfDJBcB%knV!n^XacdZT z#eNeL52Xvh$

C3dJ0tpm)%-jJHmws3-NXx0tpaY&IH)>KxG89q&7cuL8b82rKk ztm~1QV;8%jVm)Z)=soJ{C0b|=>S=lM`~)40`joIUyrJ5l_BY^}-c6M%%^Ymkq|N%7 z(9IWnOaB+@nApHYnPvl?=a)g*+^f4VA!=#}3FaqTEq_hRrcQ#cjOL^8iQI=gJ8J+_ z4J~W!a|)H_CYo!$0w<$e*Ma8svU!!Tq7&Je(XSAj2~)L}N$pA{R>RChk98Vp@_KPt z_seV_r{BuPqMofo(MZo&FNiY@fUt4l%Stz!~G?)6jCMk1@ z@-IFr<|y8&ze3N;6;sGGdA!l2+48-(6VK{<)60L`915`QN(Vi0F0?Wg(C3NEABn`f z2mozpV*Q+xuF%AtbNm{VjOyi^CO^?76R~Od8b}>_r3eC~9dZ-|2@M5yt)yy{T3*(_ zg6y~rj%#uZdUw-%v3VRi@;X_)I`OoS3%{gs`lsl?8Fp7Rnkymw+x@%BAKTi?w%=H! z!{7h1kuDS#P9DVtYid-MaG>rK4D(#iAdEK$za9vR2-wd$Beq+Z?WdImin{87c5tC- zQB?nVcXtbX%F}g;8f*A5-dvQ?L+3`6SKT%S_|ocA#)$V69vzZH8Ud^isb_}Oi)+k3 zu0F)q9eWB%Rd>J6C1&cb@;Kgky?Pf>`|<#mhNGdA@qU4=NN>2zqcb?)NJrM7(1QkX zkMEwLCK;3{Z90a3tMqX2fU-g|K<}Xs(sc0~sO}g!HeGw}g!_M;y4B!b(8})d6zOuC z4;9G!ScYn%eha~L{E2_um+5G1{-H0%2ie|pDiLpSsae=A=tpr`Qz%}atEFn@jdrXl z+M2a#wK-Q^!L`4;QzJyTN0QJq1Zy%8CS3L$!yZde4&$Om+>3vTj2CIe`YtsFV#deE z2fM2Axsl5|GTr+H2OaA{f6!mQ?yO%uK(6?Y1>M$WovHS`w(O3h>pKp9Lcfc7y2eNM z7UunD!0xz9Z4emkr$$2b@b3#P0A>n}t()rD{`tD{x-V1m{gs0w+5?4hYBF0?I+{## zOUqKHBImnRK|#Tbj=w8C{O>s+tMJ>k%$I+5c8b80#Fe+v_xwBhs(_}1wtS}L>FM38 zkhTe`-oX7C&Rb=h9YqXXl_7fPHeq(^piYHuD#LH@AaKa})fFD|_aA+OmV=sY`~Jk4 zVZNtMGUZ-PU$5nzgT8H!SE`P&vqLBzreC)BZI -g@= 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

NKP)XtZ=ytmuP+7Fw@&NY@&lFaHf>aec^fb670C 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%0DEa*C*WXp@!Uw^FarUsSC86yk0)#c0 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 KCDQL1JpVsP!Le@u literal 130766 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

H&AuE`yD7;#@AK9#IEx4E^Y}dA& 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 diff --git a/docs/reference/index-modules.asciidoc b/docs/reference/index-modules.asciidoc index 1c8f1db216b75..d9b8f8802a04b 100644 --- a/docs/reference/index-modules.asciidoc +++ b/docs/reference/index-modules.asciidoc @@ -113,10 +113,9 @@ Index mode supports the following values: `standard`::: Standard indexing with default settings. -`time_series`::: Index mode optimized for storage of metrics documented in <>. +`tsds`::: _(data streams only)_ Index mode optimized for storage of metrics. For more information, see <>. -`logsdb`::: Index mode optimized for storage of logs. It applies default sort settings on the `hostname` and `timestamp` fields and uses <>. <> on different fields is still allowed. -preview:[] +`logsdb`::: _(data streams only)_ Index mode optimized for <>. [[routing-partition-size]] `index.routing_partition_size`:: diff --git a/docs/reference/indices/index-mgmt.asciidoc b/docs/reference/indices/index-mgmt.asciidoc index 7a78f9452b85e..73643dbfd4b3b 100644 --- a/docs/reference/indices/index-mgmt.asciidoc +++ b/docs/reference/indices/index-mgmt.asciidoc @@ -67,7 +67,7 @@ This value is the time period for which your data is guaranteed to be stored. Da Elasticsearch at a later time. [role="screenshot"] -image::images/index-mgmt/management-data-stream.png[Data stream details] +image::images/index-mgmt/management-data-stream-fields.png[Data stream details] * To view more information about a data stream, such as its generation or its current index lifecycle policy, click the stream's name. From this view, you can navigate to *Discover* to diff --git a/docs/reference/indices/put-index-template.asciidoc b/docs/reference/indices/put-index-template.asciidoc index 36fc66ecb90b8..9a31037546796 100644 --- a/docs/reference/indices/put-index-template.asciidoc +++ b/docs/reference/indices/put-index-template.asciidoc @@ -115,10 +115,10 @@ See <>. `index_mode`:: (Optional, string) Type of data stream to create. Valid values are `null` -(regular data stream) and `time_series` (<>). +(standard data stream), `time_series` (<>) and `logsdb` +(<>). + -If `time_series`, each backing index has an `index.mode` index setting of -`time_series`. +The template's `index_mode` sets the `index.mode` of the backing index. ===== `index_patterns`:: diff --git a/docs/reference/mapping/fields/synthetic-source.asciidoc b/docs/reference/mapping/fields/synthetic-source.asciidoc index f8666e2993d6a..ddbefb73f4522 100644 --- a/docs/reference/mapping/fields/synthetic-source.asciidoc +++ b/docs/reference/mapping/fields/synthetic-source.asciidoc @@ -1,17 +1,10 @@ [[synthetic-source]] ==== Synthetic `_source` -IMPORTANT: Synthetic `_source` is Generally Available only for TSDB indices -(indices that have `index.mode` set to `time_series`). For other indices, -synthetic `_source` is in technical preview. Features in technical preview may -be changed or removed in a future release. Elastic will work to fix -any issues, but features in technical preview are not subject to the support SLA -of official GA features. - Though very handy to have around, the source field takes up a significant amount of space on disk. Instead of storing source documents on disk exactly as you send them, Elasticsearch can reconstruct source content on the fly upon retrieval. -Enable this by using the value `synthetic` for the index setting `index.mapping.source.mode`: +To enable this https://www.elastic.co/subscriptions[subscription] feature, use the value `synthetic` for the index setting `index.mapping.source.mode`: [source,console,id=enable-synthetic-source-example] ---- @@ -30,7 +23,7 @@ PUT idx ---- // TESTSETUP -While this on the fly reconstruction is *generally* slower than saving the source +While this on-the-fly reconstruction is _generally_ slower than saving the source documents verbatim and loading them at query time, it saves a lot of storage space. Additional latency can be avoided by not loading `_source` field in queries when it is not needed. From 50f80127227430826bb4abf4c51fdbb633cd1c30 Mon Sep 17 00:00:00 2001 From: Patrick Doyle <810052+prdoyle@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:47:17 -0500 Subject: [PATCH 20/77] Report GREEN status when FileSettingsService is stopped (#118182) * Fix FileSettingsHealthIndicatorService on stop. When FileSettingsService is stopped, the health should go GREEN. * Update FileSettingsHealthIndicatorServiceTests * Spotless --- muted-tests.yml | 3 - .../service/FileSettingsService.java | 59 +++++++++++++------ ...leSettingsHealthIndicatorServiceTests.java | 16 ++++- .../service/FileSettingsServiceTests.java | 28 ++++----- 4 files changed, 71 insertions(+), 35 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index c07363657b3ec..50ad8c27675b4 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -171,9 +171,6 @@ tests: - class: org.elasticsearch.backwards.MixedClusterClientYamlTestSuiteIT method: test {p0=synonyms/90_synonyms_reloading_for_synset/Reload analyzers for specific synonym set} issue: https://github.com/elastic/elasticsearch/issues/116777 -- class: org.elasticsearch.reservedstate.service.FileSettingsServiceTests - method: testStopWorksInMiddleOfProcessing - issue: https://github.com/elastic/elasticsearch/issues/117591 - class: "org.elasticsearch.xpack.esql.qa.multi_node.EsqlSpecIT" method: "test {scoring.*}" issue: https://github.com/elastic/elasticsearch/issues/117641 diff --git a/server/src/main/java/org/elasticsearch/reservedstate/service/FileSettingsService.java b/server/src/main/java/org/elasticsearch/reservedstate/service/FileSettingsService.java index 5f907572641a6..e36604f9a58c8 100644 --- a/server/src/main/java/org/elasticsearch/reservedstate/service/FileSettingsService.java +++ b/server/src/main/java/org/elasticsearch/reservedstate/service/FileSettingsService.java @@ -37,8 +37,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import static org.elasticsearch.health.HealthStatus.GREEN; import static org.elasticsearch.health.HealthStatus.YELLOW; @@ -122,6 +120,18 @@ public void handleSnapshotRestore(ClusterState clusterState, Metadata.Builder md } } + @Override + protected void doStart() { + healthIndicatorService.startOccurred(); + super.doStart(); + } + + @Override + protected void doStop() { + super.doStop(); + healthIndicatorService.stopOccurred(); + } + /** * If the file settings metadata version is set to zero, then we have restored from * a snapshot and must reprocess the file. @@ -211,6 +221,7 @@ protected void processInitialFileMissing() throws ExecutionException, Interrupte public static class FileSettingsHealthIndicatorService implements HealthIndicatorService { static final String NAME = "file_settings"; + static final String INACTIVE_SYMPTOM = "File-based settings are inactive"; static final String NO_CHANGES_SYMPTOM = "No file-based setting changes have occurred"; static final String SUCCESS_SYMPTOM = "The most recent file-based settings were applied successfully"; static final String FAILURE_SYMPTOM = "The most recent file-based settings encountered an error"; @@ -225,21 +236,33 @@ public static class FileSettingsHealthIndicatorService implements HealthIndicato ) ); - private final AtomicLong changeCount = new AtomicLong(0); - private final AtomicLong failureStreak = new AtomicLong(0); - private final AtomicReference mostRecentFailure = new AtomicReference<>(); + private boolean isActive = false; + private long changeCount = 0; + private long failureStreak = 0; + private String mostRecentFailure = null; - public void changeOccurred() { - changeCount.incrementAndGet(); + public synchronized void startOccurred() { + isActive = true; + failureStreak = 0; } - public void successOccurred() { - failureStreak.set(0); + public synchronized void stopOccurred() { + isActive = false; + mostRecentFailure = null; } - public void failureOccurred(String description) { - failureStreak.incrementAndGet(); - mostRecentFailure.set(description); + public synchronized void changeOccurred() { + ++changeCount; + } + + public synchronized void successOccurred() { + failureStreak = 0; + mostRecentFailure = null; + } + + public synchronized void failureOccurred(String description) { + ++failureStreak; + mostRecentFailure = description; } @Override @@ -248,18 +271,20 @@ public String name() { } @Override - public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { - if (0 == changeCount.get()) { + public synchronized HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { + if (isActive == false) { + return createIndicator(GREEN, INACTIVE_SYMPTOM, HealthIndicatorDetails.EMPTY, List.of(), List.of()); + } + if (0 == changeCount) { return createIndicator(GREEN, NO_CHANGES_SYMPTOM, HealthIndicatorDetails.EMPTY, List.of(), List.of()); } - long numFailures = failureStreak.get(); - if (0 == numFailures) { + if (0 == failureStreak) { return createIndicator(GREEN, SUCCESS_SYMPTOM, HealthIndicatorDetails.EMPTY, List.of(), List.of()); } else { return createIndicator( YELLOW, FAILURE_SYMPTOM, - new SimpleHealthIndicatorDetails(Map.of("failure_streak", numFailures, "most_recent_failure", mostRecentFailure.get())), + new SimpleHealthIndicatorDetails(Map.of("failure_streak", failureStreak, "most_recent_failure", mostRecentFailure)), STALE_SETTINGS_IMPACT, List.of() ); diff --git a/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsHealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsHealthIndicatorServiceTests.java index 03d1adff42c4e..20ea43910e68d 100644 --- a/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsHealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsHealthIndicatorServiceTests.java @@ -22,6 +22,7 @@ import static org.elasticsearch.health.HealthStatus.GREEN; import static org.elasticsearch.health.HealthStatus.YELLOW; import static org.elasticsearch.reservedstate.service.FileSettingsService.FileSettingsHealthIndicatorService.FAILURE_SYMPTOM; +import static org.elasticsearch.reservedstate.service.FileSettingsService.FileSettingsHealthIndicatorService.INACTIVE_SYMPTOM; import static org.elasticsearch.reservedstate.service.FileSettingsService.FileSettingsHealthIndicatorService.NO_CHANGES_SYMPTOM; import static org.elasticsearch.reservedstate.service.FileSettingsService.FileSettingsHealthIndicatorService.STALE_SETTINGS_IMPACT; import static org.elasticsearch.reservedstate.service.FileSettingsService.FileSettingsHealthIndicatorService.SUCCESS_SYMPTOM; @@ -39,14 +40,27 @@ public void initialize() { healthIndicatorService = new FileSettingsHealthIndicatorService(); } - public void testInitiallyGreen() { + public void testInitiallyGreen() {} + + public void testStartAndStop() { + assertEquals( + new HealthIndicatorResult("file_settings", GREEN, INACTIVE_SYMPTOM, HealthIndicatorDetails.EMPTY, List.of(), List.of()), + healthIndicatorService.calculate(false, null) + ); + healthIndicatorService.startOccurred(); assertEquals( new HealthIndicatorResult("file_settings", GREEN, NO_CHANGES_SYMPTOM, HealthIndicatorDetails.EMPTY, List.of(), List.of()), healthIndicatorService.calculate(false, null) ); + healthIndicatorService.stopOccurred(); + assertEquals( + new HealthIndicatorResult("file_settings", GREEN, INACTIVE_SYMPTOM, HealthIndicatorDetails.EMPTY, List.of(), List.of()), + healthIndicatorService.calculate(false, null) + ); } public void testGreenYellowYellowGreen() { + healthIndicatorService.startOccurred(); healthIndicatorService.changeOccurred(); // This is a strange case: a change occurred, but neither success nor failure have been reported yet. // While the change is still in progress, we don't change the status. diff --git a/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java b/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java index 08d83e48b7152..c19cf7c31bc68 100644 --- a/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java @@ -17,7 +17,6 @@ import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.NodeConnectionsService; -import org.elasticsearch.cluster.coordination.FailedToCommitClusterStateException; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.ReservedStateMetadata; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -69,6 +68,8 @@ import static java.nio.file.StandardCopyOption.ATOMIC_MOVE; import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static org.elasticsearch.health.HealthStatus.GREEN; +import static org.elasticsearch.health.HealthStatus.YELLOW; import static org.elasticsearch.node.Node.NODE_NAME_SETTING; import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.hasEntry; @@ -82,8 +83,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; public class FileSettingsServiceTests extends ESTestCase { private static final Logger logger = LogManager.getLogger(FileSettingsServiceTests.class); @@ -138,7 +137,7 @@ public void setUp() throws Exception { List.of(new ReservedClusterSettingsAction(clusterSettings)) ) ); - healthIndicatorService = mock(FileSettingsHealthIndicatorService.class); + healthIndicatorService = spy(new FileSettingsHealthIndicatorService()); fileSettingsService = spy(new FileSettingsService(clusterService, controller, env, healthIndicatorService)); } @@ -170,7 +169,8 @@ public void testStartStop() { assertTrue(fileSettingsService.watching()); fileSettingsService.stop(); assertFalse(fileSettingsService.watching()); - verifyNoInteractions(healthIndicatorService); + verify(healthIndicatorService, times(1)).startOccurred(); + verify(healthIndicatorService, times(1)).stopOccurred(); } public void testOperatorDirName() { @@ -218,9 +218,9 @@ public void testInitialFileError() throws Exception { // assert we never notified any listeners of successful application of file based settings assertFalse(settingsChanged.get()); + assertEquals(YELLOW, healthIndicatorService.calculate(false, null).status()); verify(healthIndicatorService, times(1)).changeOccurred(); verify(healthIndicatorService, times(1)).failureOccurred(argThat(s -> s.startsWith(IllegalStateException.class.getName()))); - verifyNoMoreInteractions(healthIndicatorService); } @SuppressWarnings("unchecked") @@ -246,9 +246,9 @@ public void testInitialFileWorks() throws Exception { verify(fileSettingsService, times(1)).processFileOnServiceStart(); verify(controller, times(1)).process(any(), any(XContentParser.class), eq(ReservedStateVersionCheck.HIGHER_OR_SAME_VERSION), any()); + assertEquals(GREEN, healthIndicatorService.calculate(false, null).status()); verify(healthIndicatorService, times(1)).changeOccurred(); verify(healthIndicatorService, times(1)).successOccurred(); - verifyNoMoreInteractions(healthIndicatorService); } @SuppressWarnings("unchecked") @@ -285,9 +285,9 @@ public void testProcessFileChanges() throws Exception { verify(fileSettingsService, times(1)).processFileChanges(); verify(controller, times(1)).process(any(), any(XContentParser.class), eq(ReservedStateVersionCheck.HIGHER_VERSION_ONLY), any()); + assertEquals(GREEN, healthIndicatorService.calculate(false, null).status()); verify(healthIndicatorService, times(2)).changeOccurred(); verify(healthIndicatorService, times(2)).successOccurred(); - verifyNoMoreInteractions(healthIndicatorService); } public void testInvalidJSON() throws Exception { @@ -323,6 +323,7 @@ public void testInvalidJSON() throws Exception { // referring to fileSettingsService.start(). Rather, it is referring to the initialization // of the watcher thread itself, which occurs asynchronously when clusterChanged is first called. + assertEquals(YELLOW, healthIndicatorService.calculate(false, null).status()); verify(healthIndicatorService).failureOccurred(contains(XContentParseException.class.getName())); } @@ -388,14 +389,13 @@ public void testStopWorksInMiddleOfProcessing() throws Exception { fileSettingsService.stop(); assertFalse(fileSettingsService.watching()); fileSettingsService.close(); + + // When the service is stopped, the health indicator should be green + assertEquals(GREEN, healthIndicatorService.calculate(false, null).status()); + verify(healthIndicatorService).stopOccurred(); + // let the deadlocked thread end, so we can cleanly exit the test deadThreadLatch.countDown(); - - verify(healthIndicatorService, times(1)).changeOccurred(); - verify(healthIndicatorService, times(1)).failureOccurred( - argThat(s -> s.startsWith(FailedToCommitClusterStateException.class.getName())) - ); - verifyNoMoreInteractions(healthIndicatorService); } public void testHandleSnapshotRestoreClearsMetadata() throws Exception { From 42f7816b2abeace5dab691e8c33ee191ca5b6aa9 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Thu, 12 Dec 2024 06:20:49 +1100 Subject: [PATCH 21/77] Mute org.elasticsearch.reservedstate.service.FileSettingsServiceTests testInvalidJSON #116521 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index 50ad8c27675b4..613d3a3655ccf 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -314,6 +314,9 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/118220 - class: org.elasticsearch.xpack.esql.action.EsqlActionBreakerIT issue: https://github.com/elastic/elasticsearch/issues/118238 +- class: org.elasticsearch.reservedstate.service.FileSettingsServiceTests + method: testInvalidJSON + issue: https://github.com/elastic/elasticsearch/issues/116521 # Examples: # From 4f8017583f42610cd42ed1eeb10f3688efb0a1a9 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Wed, 11 Dec 2024 14:31:33 -0500 Subject: [PATCH 22/77] Miscellaneous ILM cleanups (#118488) --- .../core/ilm/OperationModeUpdateTask.java | 6 +- .../xpack/core/ilm/LifecyclePolicyTests.java | 18 ++--- .../xpack/core/ilm/MockAction.java | 2 +- .../xpack/ilm/IndexLifecycle.java | 71 ++++++++----------- .../xpack/ilm/IndexLifecycleRunner.java | 31 ++------ .../xpack/ilm/IndexLifecycleService.java | 2 +- .../xpack/ilm/PolicyStepsRegistry.java | 7 +- .../TransportMigrateToDataTiersAction.java | 2 +- .../xpack/ilm/history/ILMHistoryStore.java | 2 +- .../ilm/IndexLifecycleTransitionTests.java | 10 +-- ...MigrationReindexStatusTransportAction.java | 4 +- 11 files changed, 60 insertions(+), 95 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTask.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTask.java index e3719d57ca25c..aaaaf9943a611 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTask.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTask.java @@ -17,6 +17,7 @@ import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.Priority; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Strings; import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentILMMode; import static org.elasticsearch.xpack.core.ilm.LifecycleOperationMetadata.currentSLMMode; @@ -143,7 +144,10 @@ private ClusterState updateSLMState(final ClusterState currentState) { @Override public void onFailure(Exception e) { - logger.error("unable to update lifecycle metadata with new ilm mode [" + ilmMode + "], slm mode [" + slmMode + "]", e); + logger.error( + () -> Strings.format("unable to update lifecycle metadata with new ilm mode [%s], slm mode [%s]", ilmMode, slmMode), + e + ); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java index 7963d04e0f666..70f75f1cfcdfa 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java @@ -151,17 +151,7 @@ public static LifecyclePolicy randomTimeseriesLifecyclePolicy(@Nullable String l // Remove the frozen phase, we'll randomly re-add it later .filter(pn -> TimeseriesLifecycleType.FROZEN_PHASE.equals(pn) == false) .collect(Collectors.toList()); - Map phases = Maps.newMapWithExpectedSize(phaseNames.size()); - Function> validActions = getPhaseToValidActions(); - Function randomAction = getNameToActionFunction(); - // as what actions end up in the hot phase influence what actions are allowed in the subsequent phases we'll move the hot phase - // at the front of the phases to process (if it exists) - if (phaseNames.contains(TimeseriesLifecycleType.HOT_PHASE)) { - phaseNames.remove(TimeseriesLifecycleType.HOT_PHASE); - phaseNames.add(0, TimeseriesLifecycleType.HOT_PHASE); - } - boolean hotPhaseContainsSearchableSnap = false; - boolean coldPhaseContainsSearchableSnap = false; + // let's order the phases so we can reason about actions in a previous phase in order to generate a random *valid* policy List orderedPhases = new ArrayList<>(phaseNames.size()); for (String validPhase : TimeseriesLifecycleType.ORDERED_VALID_PHASES) { @@ -170,6 +160,12 @@ public static LifecyclePolicy randomTimeseriesLifecyclePolicy(@Nullable String l } } + Map phases = Maps.newMapWithExpectedSize(phaseNames.size()); + Function> validActions = getPhaseToValidActions(); + Function randomAction = getNameToActionFunction(); + boolean hotPhaseContainsSearchableSnap = false; + boolean coldPhaseContainsSearchableSnap = false; + TimeValue prev = null; for (String phase : orderedPhases) { TimeValue after = prev == null diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MockAction.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MockAction.java index e849512aa8f73..0de234615f4c7 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MockAction.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MockAction.java @@ -22,7 +22,7 @@ public class MockAction implements LifecycleAction { public static final String NAME = "TEST_ACTION"; - private List steps; + private final List steps; private static final ObjectParser PARSER = new ObjectParser<>(NAME, MockAction::new); private final boolean safe; diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java index f830a2821d841..5b06ad93a9b07 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java @@ -91,7 +91,6 @@ import java.io.IOException; import java.time.Clock; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.function.LongSupplier; @@ -121,7 +120,7 @@ protected Clock getClock() { @Override public List> getSettings() { - return Arrays.asList( + return List.of( LifecycleSettings.LIFECYCLE_POLL_INTERVAL_SETTING, LifecycleSettings.LIFECYCLE_NAME_SETTING, LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE_SETTING, @@ -203,7 +202,7 @@ public List getNamedXContent() { } private static List xContentEntries() { - return Arrays.asList( + return List.of( // Custom Metadata new NamedXContentRegistry.Entry( Metadata.Custom.class, @@ -259,52 +258,38 @@ public List getRestHandlers( Supplier nodesInCluster, Predicate clusterSupportsFeature ) { - List handlers = new ArrayList<>(); - - handlers.addAll( - Arrays.asList( - // add ILM rest handlers - new RestPutLifecycleAction(), - new RestGetLifecycleAction(), - new RestDeleteLifecycleAction(), - new RestExplainLifecycleAction(), - new RestRemoveIndexLifecyclePolicyAction(), - new RestMoveToStepAction(), - new RestRetryAction(), - new RestStopAction(), - new RestStartILMAction(), - new RestGetStatusAction(), - new RestMigrateToDataTiersAction() - ) + return List.of( + new RestPutLifecycleAction(), + new RestGetLifecycleAction(), + new RestDeleteLifecycleAction(), + new RestExplainLifecycleAction(), + new RestRemoveIndexLifecyclePolicyAction(), + new RestMoveToStepAction(), + new RestRetryAction(), + new RestStopAction(), + new RestStartILMAction(), + new RestGetStatusAction(), + new RestMigrateToDataTiersAction() ); - return handlers; } @Override public List> getActions() { - var ilmUsageAction = new ActionHandler<>(XPackUsageFeatureAction.INDEX_LIFECYCLE, IndexLifecycleUsageTransportAction.class); - var ilmInfoAction = new ActionHandler<>(XPackInfoFeatureAction.INDEX_LIFECYCLE, IndexLifecycleInfoTransportAction.class); - var migrateToDataTiersAction = new ActionHandler<>(MigrateToDataTiersAction.INSTANCE, TransportMigrateToDataTiersAction.class); - List> actions = new ArrayList<>(); - actions.add(ilmUsageAction); - actions.add(ilmInfoAction); - actions.add(migrateToDataTiersAction); - actions.addAll( - Arrays.asList( - // add ILM actions - new ActionHandler<>(ILMActions.PUT, TransportPutLifecycleAction.class), - new ActionHandler<>(GetLifecycleAction.INSTANCE, TransportGetLifecycleAction.class), - new ActionHandler<>(DeleteLifecycleAction.INSTANCE, TransportDeleteLifecycleAction.class), - new ActionHandler<>(ExplainLifecycleAction.INSTANCE, TransportExplainLifecycleAction.class), - new ActionHandler<>(RemoveIndexLifecyclePolicyAction.INSTANCE, TransportRemoveIndexLifecyclePolicyAction.class), - new ActionHandler<>(ILMActions.MOVE_TO_STEP, TransportMoveToStepAction.class), - new ActionHandler<>(ILMActions.RETRY, TransportRetryAction.class), - new ActionHandler<>(ILMActions.START, TransportStartILMAction.class), - new ActionHandler<>(ILMActions.STOP, TransportStopILMAction.class), - new ActionHandler<>(GetStatusAction.INSTANCE, TransportGetStatusAction.class) - ) + return List.of( + new ActionHandler<>(XPackUsageFeatureAction.INDEX_LIFECYCLE, IndexLifecycleUsageTransportAction.class), + new ActionHandler<>(XPackInfoFeatureAction.INDEX_LIFECYCLE, IndexLifecycleInfoTransportAction.class), + new ActionHandler<>(MigrateToDataTiersAction.INSTANCE, TransportMigrateToDataTiersAction.class), + new ActionHandler<>(ILMActions.PUT, TransportPutLifecycleAction.class), + new ActionHandler<>(GetLifecycleAction.INSTANCE, TransportGetLifecycleAction.class), + new ActionHandler<>(DeleteLifecycleAction.INSTANCE, TransportDeleteLifecycleAction.class), + new ActionHandler<>(ExplainLifecycleAction.INSTANCE, TransportExplainLifecycleAction.class), + new ActionHandler<>(RemoveIndexLifecyclePolicyAction.INSTANCE, TransportRemoveIndexLifecyclePolicyAction.class), + new ActionHandler<>(ILMActions.MOVE_TO_STEP, TransportMoveToStepAction.class), + new ActionHandler<>(ILMActions.RETRY, TransportRetryAction.class), + new ActionHandler<>(ILMActions.START, TransportStartILMAction.class), + new ActionHandler<>(ILMActions.STOP, TransportStopILMAction.class), + new ActionHandler<>(GetStatusAction.INSTANCE, TransportGetStatusAction.class) ); - return actions; } List> reservedClusterStateHandlers() { diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java index efa8e67fee3c8..85739dcd0dcfb 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java @@ -18,6 +18,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.cluster.service.MasterServiceTaskQueue; import org.elasticsearch.common.Priority; +import org.elasticsearch.common.Strings; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.TimeValue; @@ -39,7 +40,6 @@ import java.util.Collections; import java.util.HashSet; -import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.function.LongSupplier; @@ -290,13 +290,7 @@ void onErrorMaybeRetryFailedStep(String policy, StepKey currentStep, IndexMetada // IndexLifecycleRunner#runPeriodicStep} run the policy will still be in the ERROR step, as we haven't been able // to move it back into the failed step, so we'll try again submitUnlessAlreadyQueued( - String.format( - Locale.ROOT, - "ilm-retry-failed-step {policy [%s], index [%s], failedStep [%s]}", - policy, - index, - failedStep.getKey() - ), + Strings.format("ilm-retry-failed-step {policy [%s], index [%s], failedStep [%s]}", policy, index, failedStep.getKey()), new MoveToRetryFailedStepUpdateTask(indexMetadata.getIndex(), policy, currentStep, failedStep) ); } else { @@ -444,7 +438,7 @@ void runPolicyAfterStateChange(String policy, IndexMetadata indexMetadata) { } else if (currentStep instanceof ClusterStateActionStep || currentStep instanceof ClusterStateWaitStep) { logger.debug("[{}] running policy with current-step [{}]", indexMetadata.getIndex().getName(), currentStep.getKey()); submitUnlessAlreadyQueued( - String.format(Locale.ROOT, "ilm-execute-cluster-state-steps [%s]", currentStep), + Strings.format("ilm-execute-cluster-state-steps [%s]", currentStep), new ExecuteStepsUpdateTask(policy, indexMetadata.getIndex(), currentStep, stepRegistry, this, nowSupplier) ); } else { @@ -459,8 +453,7 @@ void runPolicyAfterStateChange(String policy, IndexMetadata indexMetadata) { private void moveToStep(Index index, String policy, Step.StepKey currentStepKey, Step.StepKey newStepKey) { logger.debug("[{}] moving to step [{}] {} -> {}", index.getName(), policy, currentStepKey, newStepKey); submitUnlessAlreadyQueued( - String.format( - Locale.ROOT, + Strings.format( "ilm-move-to-step {policy [%s], index [%s], currentStep [%s], nextStep [%s]}", policy, index.getName(), @@ -486,13 +479,7 @@ private void moveToErrorStep(Index index, String policy, Step.StepKey currentSte e ); submitUnlessAlreadyQueued( - String.format( - Locale.ROOT, - "ilm-move-to-error-step {policy [%s], index [%s], currentStep [%s]}", - policy, - index.getName(), - currentStepKey - ), + Strings.format("ilm-move-to-error-step {policy [%s], index [%s], currentStep [%s]}", policy, index.getName(), currentStepKey), new MoveToErrorStepUpdateTask(index, policy, currentStepKey, e, nowSupplier, stepRegistry::getStep, clusterState -> { IndexMetadata indexMetadata = clusterState.metadata().index(index); registerFailedOperation(indexMetadata, e); @@ -506,13 +493,7 @@ private void moveToErrorStep(Index index, String policy, Step.StepKey currentSte */ private void setStepInfo(Index index, String policy, @Nullable Step.StepKey currentStepKey, ToXContentObject stepInfo) { submitUnlessAlreadyQueued( - String.format( - Locale.ROOT, - "ilm-set-step-info {policy [%s], index [%s], currentStep [%s]}", - policy, - index.getName(), - currentStepKey - ), + Strings.format("ilm-set-step-info {policy [%s], index [%s], currentStep [%s]}", policy, index.getName(), currentStepKey), new SetStepInfoUpdateTask(index, policy, currentStepKey, stepInfo) ); } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java index 9c978ffc25cba..e59bde7253051 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java @@ -354,7 +354,7 @@ private void cancelJob() { @Override public void triggered(SchedulerEngine.Event event) { if (event.jobName().equals(XPackField.INDEX_LIFECYCLE)) { - logger.trace("job triggered: " + event.jobName() + ", " + event.scheduledTime() + ", " + event.triggeredTime()); + logger.trace("job triggered: {}, {}, {}", event.jobName(), event.scheduledTime(), event.triggeredTime()); triggerPolicies(clusterService.state(), false); } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/PolicyStepsRegistry.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/PolicyStepsRegistry.java index 4567e291aebed..296623b54509f 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/PolicyStepsRegistry.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/PolicyStepsRegistry.java @@ -14,6 +14,7 @@ import org.elasticsearch.cluster.DiffableUtils; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.core.Nullable; @@ -42,7 +43,6 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -269,9 +269,8 @@ public Set parseStepKeysFromPhase(String policy, String currentPha return parseStepsFromPhase(policy, currentPhase, phaseDefNonNull).stream().map(Step::getKey).collect(Collectors.toSet()); } catch (IOException e) { logger.trace( - () -> String.format( - Locale.ROOT, - "unable to parse steps for policy [{}], phase [{}], and phase definition [{}]", + () -> Strings.format( + "unable to parse steps for policy [%s], phase [%s], and phase definition [%s]", policy, currentPhase, phaseDef diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java index 48cf84ed7a6a4..494f0ee444236 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java @@ -145,7 +145,7 @@ public void onFailure(Exception e) { @Override public void clusterStateProcessed(ClusterState oldState, ClusterState newState) { - rerouteService.reroute("cluster migrated to data tiers routing", Priority.NORMAL, new ActionListener() { + rerouteService.reroute("cluster migrated to data tiers routing", Priority.NORMAL, new ActionListener<>() { @Override public void onResponse(Void ignored) {} diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStore.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStore.java index b8af3e8e0daa2..549b321be8182 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStore.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryStore.java @@ -58,7 +58,7 @@ public class ILMHistoryStore implements Closeable { public static final String ILM_HISTORY_DATA_STREAM = "ilm-history-" + INDEX_TEMPLATE_VERSION; - private static int ILM_HISTORY_BULK_SIZE = StrictMath.toIntExact( + private static final int ILM_HISTORY_BULK_SIZE = StrictMath.toIntExact( ByteSizeValue.parseBytesSizeValue( System.getProperty("es.indices.lifecycle.history.bulk.size", "50MB"), "es.indices.lifecycle.history.bulk.size" diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java index 37d586240eb7a..49aa0a65a5704 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java @@ -72,7 +72,7 @@ public class IndexLifecycleTransitionTests extends ESTestCase { public void testMoveClusterStateToNextStep() { String indexName = "my_index"; LifecyclePolicy policy = randomValueOtherThanMany( - p -> p.getPhases().size() == 0, + p -> p.getPhases().isEmpty(), () -> LifecyclePolicyTests.randomTestLifecyclePolicy("policy") ); Phase nextPhase = policy.getPhases() @@ -125,7 +125,7 @@ public void testMoveClusterStateToNextStep() { public void testMoveClusterStateToNextStepSamePhase() { String indexName = "my_index"; LifecyclePolicy policy = randomValueOtherThanMany( - p -> p.getPhases().size() == 0, + p -> p.getPhases().isEmpty(), () -> LifecyclePolicyTests.randomTestLifecyclePolicy("policy") ); List policyMetadatas = Collections.singletonList( @@ -176,7 +176,7 @@ public void testMoveClusterStateToNextStepSamePhase() { public void testMoveClusterStateToNextStepSameAction() { String indexName = "my_index"; LifecyclePolicy policy = randomValueOtherThanMany( - p -> p.getPhases().size() == 0, + p -> p.getPhases().isEmpty(), () -> LifecyclePolicyTests.randomTestLifecyclePolicy("policy") ); List policyMetadatas = Collections.singletonList( @@ -228,7 +228,7 @@ public void testSuccessfulValidatedMoveClusterStateToNextStep() { String indexName = "my_index"; String policyName = "my_policy"; LifecyclePolicy policy = randomValueOtherThanMany( - p -> p.getPhases().size() == 0, + p -> p.getPhases().isEmpty(), () -> LifecyclePolicyTests.randomTestLifecyclePolicy(policyName) ); Phase nextPhase = policy.getPhases() @@ -1436,6 +1436,6 @@ private void assertClusterStateStepInfo( assertEquals(expectedstepInfoValue, newLifecycleState.stepInfo()); assertEquals(oldLifecycleState.phaseTime(), newLifecycleState.phaseTime()); assertEquals(oldLifecycleState.actionTime(), newLifecycleState.actionTime()); - assertEquals(newLifecycleState.stepTime(), newLifecycleState.stepTime()); + assertEquals(oldLifecycleState.stepTime(), newLifecycleState.stepTime()); } } diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusTransportAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusTransportAction.java index f2a6e33f7cb05..ca81a03fc5630 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusTransportAction.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/GetMigrationReindexStatusTransportAction.java @@ -88,7 +88,7 @@ void getRunningTaskFromNode(String persistentTaskId, ActionListener li listener.onFailure( new ResourceNotFoundException( Strings.format( - "Persistent task [{}] is supposed to be running on node [{}], " + "but the task is not found on that node", + "Persistent task [%s] is supposed to be running on node [%s], but the task is not found on that node", persistentTaskId, clusterService.localNode().getId() ) @@ -106,7 +106,7 @@ private void runOnNodeWithTaskIfPossible(Task thisTask, Request request, String listener.onFailure( new ResourceNotFoundException( Strings.format( - "Persistent task [{}] is supposed to be running on node [{}], but that node is not part of the cluster", + "Persistent task [%s] is supposed to be running on node [%s], but that node is not part of the cluster", request.getIndex(), nodeId ) From 4db3f7b7514e56e44e98e89b83b2cc77a41d19ae Mon Sep 17 00:00:00 2001 From: Sean Story Date: Wed, 11 Dec 2024 13:34:00 -0600 Subject: [PATCH 23/77] Add known issue for salesforce DLS (#118489) --- .../docs/connectors-salesforce.asciidoc | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/reference/connector/docs/connectors-salesforce.asciidoc b/docs/reference/connector/docs/connectors-salesforce.asciidoc index c640751de92c0..f5c5512ad5cc4 100644 --- a/docs/reference/connector/docs/connectors-salesforce.asciidoc +++ b/docs/reference/connector/docs/connectors-salesforce.asciidoc @@ -200,7 +200,7 @@ Once the permissions are set, assign the Profiles, Permission Set or Permission Follow these steps in Salesforce: 1. Navigate to `Administration` under the `Users` section. -2. Select `Users` and choose the user to set the permissions to. +2. Select `Users` and choose the user to set the permissions to. 3. Set the `Profile`, `Permission Set` or `Permission Set Groups` created in the earlier steps. [discrete#es-connectors-salesforce-sync-rules] @@ -249,7 +249,7 @@ Allowed values are *SOQL* and *SOSL*. [ { "query": "FIND {Salesforce} IN ALL FIELDS", - "language": "SOSL" + "language": "SOSL" } ] ---- @@ -381,7 +381,13 @@ See <> for more specifics o [discrete#es-connectors-salesforce-known-issues] ===== Known issues -There are currently no known issues for this connector. +* *DLS feature is "type-level" not "document-level"* ++ +Salesforce DLS, added in 8.13.0, does not accomodate specific access controls to specific Salesforce Objects. +Instead, if a given user/group can have access to _any_ Objects of a given type (`Case`, `Lead`, `Opportunity`, etc), that user/group will appear in the `\_allow_access_control` list for _all_ of the Objects of that type. +See https://github.com/elastic/connectors/issues/3028 for more details. ++ + Refer to <> for a list of known issues for all connectors. [discrete#es-connectors-salesforce-security] @@ -396,7 +402,7 @@ This connector is built with the {connectors-python}[Elastic connector framework View the {connectors-python}/connectors/sources/salesforce.py[source code for this connector^] (branch _{connectors-branch}_, compatible with Elastic _{minor-version}_). -// Closing the collapsible section +// Closing the collapsible section =============== @@ -598,7 +604,7 @@ Once the permissions are set, assign the Profiles, Permission Set or Permission Follow these steps in Salesforce: 1. Navigate to `Administration` under the `Users` section. -2. Select `Users` and choose the user to set the permissions to. +2. Select `Users` and choose the user to set the permissions to. 3. Set the `Profile`, `Permission Set` or `Permission Set Groups` created in the earlier steps. [discrete#es-connectors-salesforce-client-sync-rules] @@ -648,7 +654,7 @@ Allowed values are *SOQL* and *SOSL*. [ { "query": "FIND {Salesforce} IN ALL FIELDS", - "language": "SOSL" + "language": "SOSL" } ] ---- @@ -781,7 +787,13 @@ See <> for more specifics o [discrete#es-connectors-salesforce-client-known-issues] ===== Known issues -There are currently no known issues for this connector. +* *DLS feature is "type-level" not "document-level"* ++ +Salesforce DLS, added in 8.13.0, does not accomodate specific access controls to specific Salesforce Objects. +Instead, if a given user/group can have access to _any_ Objects of a given type (`Case`, `Lead`, `Opportunity`, etc), that user/group will appear in the `\_allow_access_control` list for _all_ of the Objects of that type. +See https://github.com/elastic/connectors/issues/3028 for more details. ++ + Refer to <> for a list of known issues for all connectors. [discrete#es-connectors-salesforce-client-security] @@ -797,5 +809,5 @@ This connector is built with the {connectors-python}[Elastic connector framework View the {connectors-python}/connectors/sources/salesforce.py[source code for this connector^] (branch _{connectors-branch}_, compatible with Elastic _{minor-version}_). -// Closing the collapsible section +// Closing the collapsible section =============== From 2c5efd2e610cfdfe9212e938b8bdd59fc6f3fedc Mon Sep 17 00:00:00 2001 From: Keith Massey Date: Wed, 11 Dec 2024 13:38:04 -0600 Subject: [PATCH 24/77] Correcting the index version filter in migration reindex logic (#118487) --- .../action/ReindexDataStreamAction.java | 18 ++++++++++++++++++ .../ReindexDataStreamTransportAction.java | 6 ++---- ...eindexDataStreamPersistentTaskExecutor.java | 4 +++- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamAction.java index 9e4cbb1082215..39d4170f6e712 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamAction.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamAction.java @@ -13,10 +13,14 @@ import org.elasticsearch.action.ActionType; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.FeatureFlag; import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.IndexVersions; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContent; @@ -39,10 +43,24 @@ public class ReindexDataStreamAction extends ActionType getOldIndexVersionPredicate(Metadata metadata) { + return index -> metadata.index(index).getCreationVersion().onOrBefore(MINIMUM_WRITEABLE_VERSION_AFTER_UPGRADE); + } + public enum Mode { UPGRADE } diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportAction.java index 95a078690a055..f011c429ce79c 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportAction.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportAction.java @@ -27,6 +27,7 @@ import org.elasticsearch.xpack.migrate.task.ReindexDataStreamTaskParams; import static org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction.TASK_ID_PREFIX; +import static org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction.getOldIndexVersionPredicate; /* * This transport action creates a new persistent task for reindexing the source data stream given in the request. On successful creation @@ -67,10 +68,7 @@ protected void doExecute(Task task, ReindexDataStreamRequest request, ActionList return; } int totalIndices = dataStream.getIndices().size(); - int totalIndicesToBeUpgraded = (int) dataStream.getIndices() - .stream() - .filter(index -> metadata.index(index).getCreationVersion().isLegacyIndexVersion()) - .count(); + int totalIndicesToBeUpgraded = (int) dataStream.getIndices().stream().filter(getOldIndexVersionPredicate(metadata)).count(); ReindexDataStreamTaskParams params = new ReindexDataStreamTaskParams( sourceDataStreamName, transportService.getThreadPool().absoluteTimeInMillis(), diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java index fc471cfa89f26..7ec5014b9edff 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java @@ -24,6 +24,8 @@ import java.util.List; import java.util.Map; +import static org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction.getOldIndexVersionPredicate; + public class ReindexDataStreamPersistentTaskExecutor extends PersistentTasksExecutor { private static final TimeValue TASK_KEEP_ALIVE_TIME = TimeValue.timeValueDays(1); private final Client client; @@ -72,7 +74,7 @@ protected void nodeOperation(AllocatedPersistentTask task, ReindexDataStreamTask if (dataStreamInfos.size() == 1) { List indices = dataStreamInfos.getFirst().getDataStream().getIndices(); List indicesToBeReindexed = indices.stream() - .filter(index -> clusterService.state().getMetadata().index(index).getCreationVersion().isLegacyIndexVersion()) + .filter(getOldIndexVersionPredicate(clusterService.state().metadata())) .toList(); reindexDataStreamTask.setPendingIndicesCount(indicesToBeReindexed.size()); for (Index index : indicesToBeReindexed) { From 9056fe430d0679593ce140a27c658215ba84415a Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Wed, 11 Dec 2024 15:54:03 -0500 Subject: [PATCH 25/77] Convert some ILM classes to records (#118466) --- .../xpack/core/ilm/ClusterStateWaitStep.java | 18 +-- .../ClusterStateWaitUntilThresholdStep.java | 4 +- .../xpack/core/ilm/SegmentCountStep.java | 30 +---- .../core/ilm/WaitForFollowShardTasksStep.java | 106 ++++-------------- .../core/ilm/step/info/AllocationInfo.java | 53 +-------- .../ilm/step/info/SingleMessageFieldInfo.java | 31 +---- .../core/ilm/AllocationRoutedStepTests.java | 12 +- .../CheckNoDataStreamWriteIndexStepTests.java | 14 +-- .../core/ilm/CheckShrinkReadyStepTests.java | 14 +-- .../ilm/CheckTargetShardsCountStepTests.java | 8 +- ...usterStateWaitUntilThresholdStepTests.java | 30 ++--- .../ilm/DataTierMigrationRoutedStepTests.java | 32 +++--- .../core/ilm/SegmentCountStepInfoTests.java | 2 +- .../ilm/ShrunkShardsAllocatedStepTests.java | 12 +- .../core/ilm/ShrunkenIndexCheckStepTests.java | 12 +- .../core/ilm/WaitForActiveShardsTests.java | 16 +-- .../core/ilm/WaitForDataTierStepTests.java | 6 +- .../WaitForFollowShardTasksStepInfoTests.java | 2 +- .../ilm/WaitForFollowShardTasksStepTests.java | 8 +- .../core/ilm/WaitForIndexColorStepTests.java | 48 ++++---- .../ilm/WaitForIndexingCompleteStepTests.java | 18 +-- .../info/AllocationRoutedStepInfoTests.java | 12 +- ...adataMigrateToDataTiersRoutingService.java | 62 ++-------- .../xpack/ilm/ExecuteStepsUpdateTask.java | 4 +- .../TransportMigrateToDataTiersAction.java | 24 ++-- ...MigrateToDataTiersRoutingServiceTests.java | 42 +++---- 26 files changed, 199 insertions(+), 421 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitStep.java index d1dbfede63c60..4ed83fa170ead 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitStep.java @@ -33,22 +33,6 @@ public boolean isCompletable() { return true; } - public static class Result { - private final boolean complete; - private final ToXContentObject informationContext; - - public Result(boolean complete, ToXContentObject informationContext) { - this.complete = complete; - this.informationContext = informationContext; - } - - public boolean isComplete() { - return complete; - } - - public ToXContentObject getInformationContext() { - return informationContext; - } - } + public record Result(boolean complete, ToXContentObject informationContext) {} } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStep.java index 5e30baa6b9669..c7fa1ea611a0f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStep.java @@ -62,7 +62,7 @@ public Result isConditionMet(Index index, ClusterState clusterState) { Result stepResult = stepToExecute.isConditionMet(index, clusterState); - if (stepResult.isComplete() == false) { + if (stepResult.complete() == false) { // checking the threshold after we execute the step to make sure we execute the wrapped step at least once (because time is a // wonderful thing) TimeValue retryThreshold = LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD_SETTING.get(idxMeta.getSettings()); @@ -77,7 +77,7 @@ public Result isConditionMet(Index index, ClusterState clusterState) { getKey().name(), getKey().action(), idxMeta.getIndex().getName(), - Strings.toString(stepResult.getInformationContext()), + Strings.toString(stepResult.informationContext()), nextKeyOnThresholdBreach ); logger.debug(message); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SegmentCountStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SegmentCountStep.java index 800ea603ede8c..ad8f450fb0849 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SegmentCountStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SegmentCountStep.java @@ -114,9 +114,7 @@ public boolean equals(Object obj) { return super.equals(obj) && Objects.equals(maxNumSegments, other.maxNumSegments); } - public static class Info implements ToXContentObject { - - private final long numberShardsLeftToMerge; + public record Info(long numberShardsLeftToMerge) implements ToXContentObject { static final ParseField SHARDS_TO_MERGE = new ParseField("shards_left_to_merge"); static final ParseField MESSAGE = new ParseField("message"); @@ -124,19 +122,12 @@ public static class Info implements ToXContentObject { "segment_count_step_info", a -> new Info((long) a[0]) ); + static { PARSER.declareLong(ConstructingObjectParser.constructorArg(), SHARDS_TO_MERGE); PARSER.declareString((i, s) -> {}, MESSAGE); } - public Info(long numberShardsLeftToMerge) { - this.numberShardsLeftToMerge = numberShardsLeftToMerge; - } - - public long getNumberShardsLeftToMerge() { - return numberShardsLeftToMerge; - } - @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -150,23 +141,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - @Override - public int hashCode() { - return Objects.hash(numberShardsLeftToMerge); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - Info other = (Info) obj; - return Objects.equals(numberShardsLeftToMerge, other.numberShardsLeftToMerge); - } - @Override public String toString() { return Strings.toString(this); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStep.java index f1fbdde1e9a5d..224319722297c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStep.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; import static org.elasticsearch.xpack.core.ilm.UnfollowAction.CCR_METADATA_KEY; @@ -70,9 +69,9 @@ static void handleResponse(FollowStatsAction.StatsResponses responses, Listener if (conditionMet) { listener.onResponse(true, null); } else { - List shardFollowTaskInfos = unSyncedShardFollowStatuses.stream() + List shardFollowTaskInfos = unSyncedShardFollowStatuses.stream() .map( - status -> new Info.ShardFollowTaskInfo( + status -> new ShardFollowTaskInfo( status.followerIndex(), status.getShardId(), status.leaderGlobalCheckpoint(), @@ -84,21 +83,11 @@ static void handleResponse(FollowStatsAction.StatsResponses responses, Listener } } - static final class Info implements ToXContentObject { + record Info(List shardFollowTaskInfos) implements ToXContentObject { static final ParseField SHARD_FOLLOW_TASKS = new ParseField("shard_follow_tasks"); static final ParseField MESSAGE = new ParseField("message"); - private final List shardFollowTaskInfos; - - Info(List shardFollowTaskInfos) { - this.shardFollowTaskInfos = shardFollowTaskInfos; - } - - List getShardFollowTaskInfos() { - return shardFollowTaskInfos; - } - @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -114,85 +103,30 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Info info = (Info) o; - return Objects.equals(shardFollowTaskInfos, info.shardFollowTaskInfos); - } - - @Override - public int hashCode() { - return Objects.hash(shardFollowTaskInfos); - } - @Override public String toString() { return Strings.toString(this); } + } - static final class ShardFollowTaskInfo implements ToXContentObject { - - static final ParseField FOLLOWER_INDEX_FIELD = new ParseField("follower_index"); - static final ParseField SHARD_ID_FIELD = new ParseField("shard_id"); - static final ParseField LEADER_GLOBAL_CHECKPOINT_FIELD = new ParseField("leader_global_checkpoint"); - static final ParseField FOLLOWER_GLOBAL_CHECKPOINT_FIELD = new ParseField("follower_global_checkpoint"); - - private final String followerIndex; - private final int shardId; - private final long leaderGlobalCheckpoint; - private final long followerGlobalCheckpoint; - - ShardFollowTaskInfo(String followerIndex, int shardId, long leaderGlobalCheckpoint, long followerGlobalCheckpoint) { - this.followerIndex = followerIndex; - this.shardId = shardId; - this.leaderGlobalCheckpoint = leaderGlobalCheckpoint; - this.followerGlobalCheckpoint = followerGlobalCheckpoint; - } - - String getFollowerIndex() { - return followerIndex; - } - - int getShardId() { - return shardId; - } - - long getLeaderGlobalCheckpoint() { - return leaderGlobalCheckpoint; - } - - long getFollowerGlobalCheckpoint() { - return followerGlobalCheckpoint; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field(FOLLOWER_INDEX_FIELD.getPreferredName(), followerIndex); - builder.field(SHARD_ID_FIELD.getPreferredName(), shardId); - builder.field(LEADER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), leaderGlobalCheckpoint); - builder.field(FOLLOWER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), followerGlobalCheckpoint); - builder.endObject(); - return builder; - } + record ShardFollowTaskInfo(String followerIndex, int shardId, long leaderGlobalCheckpoint, long followerGlobalCheckpoint) + implements + ToXContentObject { - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ShardFollowTaskInfo that = (ShardFollowTaskInfo) o; - return shardId == that.shardId - && leaderGlobalCheckpoint == that.leaderGlobalCheckpoint - && followerGlobalCheckpoint == that.followerGlobalCheckpoint - && Objects.equals(followerIndex, that.followerIndex); - } + static final ParseField FOLLOWER_INDEX_FIELD = new ParseField("follower_index"); + static final ParseField SHARD_ID_FIELD = new ParseField("shard_id"); + static final ParseField LEADER_GLOBAL_CHECKPOINT_FIELD = new ParseField("leader_global_checkpoint"); + static final ParseField FOLLOWER_GLOBAL_CHECKPOINT_FIELD = new ParseField("follower_global_checkpoint"); - @Override - public int hashCode() { - return Objects.hash(followerIndex, shardId, leaderGlobalCheckpoint, followerGlobalCheckpoint); - } + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(FOLLOWER_INDEX_FIELD.getPreferredName(), followerIndex); + builder.field(SHARD_ID_FIELD.getPreferredName(), shardId); + builder.field(LEADER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), leaderGlobalCheckpoint); + builder.field(FOLLOWER_GLOBAL_CHECKPOINT_FIELD.getPreferredName(), followerGlobalCheckpoint); + builder.endObject(); + return builder; } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/step/info/AllocationInfo.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/step/info/AllocationInfo.java index 5732f5e72a42f..9f280bd344083 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/step/info/AllocationInfo.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/step/info/AllocationInfo.java @@ -14,19 +14,15 @@ import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; -import java.util.Objects; /** * Represents the state of an index's shards allocation, including a user friendly message describing the current state. * It allows to transfer the allocation information to {@link org.elasticsearch.xcontent.XContent} using * {@link #toXContent(XContentBuilder, Params)} */ -public class AllocationInfo implements ToXContentObject { - - private final long numberOfReplicas; - private final long numberShardsLeftToAllocate; - private final boolean allShardsActive; - private final String message; +public record AllocationInfo(long numberOfReplicas, long numberShardsLeftToAllocate, boolean allShardsActive, String message) + implements + ToXContentObject { static final ParseField NUMBER_OF_REPLICAS = new ParseField("number_of_replicas"); static final ParseField SHARDS_TO_ALLOCATE = new ParseField("shards_left_to_allocate"); @@ -44,13 +40,6 @@ public class AllocationInfo implements ToXContentObject { PARSER.declareString(ConstructingObjectParser.constructorArg(), MESSAGE); } - public AllocationInfo(long numberOfReplicas, long numberShardsLeftToAllocate, boolean allShardsActive, String message) { - this.numberOfReplicas = numberOfReplicas; - this.numberShardsLeftToAllocate = numberShardsLeftToAllocate; - this.allShardsActive = allShardsActive; - this.message = message; - } - /** * Builds the AllocationInfo representing a cluster state with a routing table that does not have enough shards active for a * particular index. @@ -72,22 +61,6 @@ public static AllocationInfo allShardsActiveAllocationInfo(long numReplicas, lon ); } - public long getNumberOfReplicas() { - return numberOfReplicas; - } - - public long getNumberShardsLeftToAllocate() { - return numberShardsLeftToAllocate; - } - - public boolean allShardsActive() { - return allShardsActive; - } - - public String getMessage() { - return message; - } - @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -99,26 +72,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - @Override - public int hashCode() { - return Objects.hash(numberOfReplicas, numberShardsLeftToAllocate, allShardsActive); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - AllocationInfo other = (AllocationInfo) obj; - return Objects.equals(numberOfReplicas, other.numberOfReplicas) - && Objects.equals(numberShardsLeftToAllocate, other.numberShardsLeftToAllocate) - && Objects.equals(message, other.message) - && Objects.equals(allShardsActive, other.allShardsActive); - } - @Override public String toString() { return Strings.toString(this); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/step/info/SingleMessageFieldInfo.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/step/info/SingleMessageFieldInfo.java index 8d7eb8c3d303b..bd23e21d46489 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/step/info/SingleMessageFieldInfo.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/step/info/SingleMessageFieldInfo.java @@ -12,20 +12,13 @@ import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; -import java.util.Objects; /** * A simple object that allows a `message` field to be transferred to `XContent`. */ -public class SingleMessageFieldInfo implements ToXContentObject { +public record SingleMessageFieldInfo(String message) implements ToXContentObject { - private final String message; - - static final ParseField MESSAGE = new ParseField("message"); - - public SingleMessageFieldInfo(String message) { - this.message = message; - } + private static final ParseField MESSAGE = new ParseField("message"); @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { @@ -35,24 +28,4 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } - public String getMessage() { - return message; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SingleMessageFieldInfo that = (SingleMessageFieldInfo) o; - return Objects.equals(message, that.message); - } - - @Override - public int hashCode() { - return Objects.hash(message); - } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AllocationRoutedStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AllocationRoutedStepTests.java index 415014623f340..afad708ddbe2c 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AllocationRoutedStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AllocationRoutedStepTests.java @@ -181,8 +181,8 @@ public void testClusterExcludeFiltersConditionMetOnlyOneCopyAllocated() { Result actualResult = step.isConditionMet(index, clusterState); Result expectedResult = new ClusterStateWaitStep.Result(false, allShardsActiveAllocationInfo(1, 1)); - assertEquals(expectedResult.isComplete(), actualResult.isComplete()); - assertEquals(expectedResult.getInformationContext(), actualResult.getInformationContext()); + assertEquals(expectedResult.complete(), actualResult.complete()); + assertEquals(expectedResult.informationContext(), actualResult.informationContext()); } public void testExcludeConditionMetOnlyOneCopyAllocated() { @@ -495,8 +495,8 @@ public void testExecuteIndexMissing() throws Exception { AllocationRoutedStep step = createRandomInstance(); Result actualResult = step.isConditionMet(index, clusterState); - assertFalse(actualResult.isComplete()); - assertNull(actualResult.getInformationContext()); + assertFalse(actualResult.complete()); + assertNull(actualResult.informationContext()); } private void assertAllocateStatus( @@ -537,7 +537,7 @@ private void assertAllocateStatus( .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) .build(); Result actualResult = step.isConditionMet(index, clusterState); - assertEquals(expectedResult.isComplete(), actualResult.isComplete()); - assertEquals(expectedResult.getInformationContext(), actualResult.getInformationContext()); + assertEquals(expectedResult.complete(), actualResult.complete()); + assertEquals(expectedResult.informationContext(), actualResult.informationContext()); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckNoDataStreamWriteIndexStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckNoDataStreamWriteIndexStepTests.java index af9aa0982d61d..54c6ceb814af8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckNoDataStreamWriteIndexStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckNoDataStreamWriteIndexStepTests.java @@ -59,8 +59,8 @@ public void testStepCompleteIfIndexIsNotPartOfDataStream() { .build(); ClusterStateWaitStep.Result result = createRandomInstance().isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), is(nullValue())); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), is(nullValue())); } public void testStepIncompleteIfIndexIsTheDataStreamWriteIndex() { @@ -94,10 +94,10 @@ public void testStepIncompleteIfIndexIsTheDataStreamWriteIndex() { IndexMetadata indexToOperateOn = useFailureStore ? failureIndexMetadata : indexMetadata; String expectedIndexName = indexToOperateOn.getIndex().getName(); ClusterStateWaitStep.Result result = createRandomInstance().isConditionMet(indexToOperateOn.getIndex(), clusterState); - assertThat(result.isComplete(), is(false)); - SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.getInformationContext(); + assertThat(result.complete(), is(false)); + SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.informationContext(); assertThat( - info.getMessage(), + info.message(), is( "index [" + expectedIndexName @@ -161,7 +161,7 @@ public void testStepCompleteIfPartOfDataStreamButNotWriteIndex() { boolean useFailureStore = randomBoolean(); IndexMetadata indexToOperateOn = useFailureStore ? failureIndexMetadata : indexMetadata; ClusterStateWaitStep.Result result = createRandomInstance().isConditionMet(indexToOperateOn.getIndex(), clusterState); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), is(nullValue())); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), is(nullValue())); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStepTests.java index 371f7def67c52..8dcd8fc7ddd55 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStepTests.java @@ -417,8 +417,8 @@ public void testExecuteIndexMissing() throws Exception { CheckShrinkReadyStep step = createRandomInstance(); ClusterStateWaitStep.Result actualResult = step.isConditionMet(index, clusterState); - assertFalse(actualResult.isComplete()); - assertNull(actualResult.getInformationContext()); + assertFalse(actualResult.complete()); + assertNull(actualResult.informationContext()); } public void testStepCompletableIfAllShardsActive() { @@ -495,7 +495,7 @@ public void testStepCompletableIfAllShardsActive() { .build(); assertTrue(step.isCompletable()); ClusterStateWaitStep.Result actualResult = step.isConditionMet(index, clusterState); - assertTrue(actualResult.isComplete()); + assertTrue(actualResult.complete()); assertTrue(step.isCompletable()); } } @@ -574,9 +574,9 @@ public void testStepBecomesUncompletable() { .build(); assertTrue(step.isCompletable()); ClusterStateWaitStep.Result actualResult = step.isConditionMet(index, clusterState); - assertFalse(actualResult.isComplete()); + assertFalse(actualResult.complete()); assertThat( - Strings.toString(actualResult.getInformationContext()), + Strings.toString(actualResult.informationContext()), containsString("node with id [node1] is currently marked as shutting down") ); assertFalse(step.isCompletable()); @@ -625,8 +625,8 @@ private void assertAllocateStatus( .routingTable(RoutingTable.builder().add(indexRoutingTable).build()) .build(); ClusterStateWaitStep.Result actualResult = step.isConditionMet(index, clusterState); - assertEquals(expectedResult.isComplete(), actualResult.isComplete()); - assertEquals(expectedResult.getInformationContext(), actualResult.getInformationContext()); + assertEquals(expectedResult.complete(), actualResult.complete()); + assertEquals(expectedResult.informationContext(), actualResult.informationContext()); } public static UnassignedInfo randomUnassignedInfo(String message) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckTargetShardsCountStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckTargetShardsCountStepTests.java index 8eb8d0f395aba..23d24fbd28730 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckTargetShardsCountStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckTargetShardsCountStepTests.java @@ -56,7 +56,7 @@ public void testStepCompleteIfTargetShardsCountIsValid() { CheckTargetShardsCountStep checkTargetShardsCountStep = new CheckTargetShardsCountStep(randomStepKey(), randomStepKey(), 2); ClusterStateWaitStep.Result result = checkTargetShardsCountStep.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(true)); + assertThat(result.complete(), is(true)); } public void testStepIncompleteIfTargetShardsCountNotValid() { @@ -75,10 +75,10 @@ public void testStepIncompleteIfTargetShardsCountNotValid() { CheckTargetShardsCountStep checkTargetShardsCountStep = new CheckTargetShardsCountStep(randomStepKey(), randomStepKey(), 3); ClusterStateWaitStep.Result result = checkTargetShardsCountStep.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(false)); - SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.getInformationContext(); + assertThat(result.complete(), is(false)); + SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.informationContext(); assertThat( - info.getMessage(), + info.message(), is( "lifecycle action of policy [" + policyName diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStepTests.java index ea583b51c4c28..f24f0f86de7db 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStepTests.java @@ -70,8 +70,8 @@ public void testIndexIsMissingReturnsIncompleteResult() { new Index("testName", UUID.randomUUID().toString()), ClusterState.EMPTY_STATE ); - assertThat(result.isComplete(), is(false)); - assertThat(result.getInformationContext(), nullValue()); + assertThat(result.complete(), is(false)); + assertThat(result.informationContext(), nullValue()); } public void testIsConditionMetForUnderlyingStep() { @@ -95,8 +95,8 @@ public void testIsConditionMetForUnderlyingStep() { ClusterStateWaitUntilThresholdStep underTest = new ClusterStateWaitUntilThresholdStep(stepToExecute, randomStepKey()); ClusterStateWaitStep.Result result = underTest.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), nullValue()); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), nullValue()); } { @@ -120,10 +120,10 @@ public void testIsConditionMetForUnderlyingStep() { ClusterStateWaitUntilThresholdStep underTest = new ClusterStateWaitUntilThresholdStep(stepToExecute, randomStepKey()); ClusterStateWaitStep.Result result = underTest.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(false)); - assertThat(result.getInformationContext(), notNullValue()); + assertThat(result.complete(), is(false)); + assertThat(result.informationContext(), notNullValue()); WaitForIndexingCompleteStep.IndexingNotCompleteInfo info = (WaitForIndexingCompleteStep.IndexingNotCompleteInfo) result - .getInformationContext(); + .informationContext(); assertThat( info.getMessage(), equalTo( @@ -154,8 +154,8 @@ public void testIsConditionMetForUnderlyingStep() { ClusterStateWaitUntilThresholdStep underTest = new ClusterStateWaitUntilThresholdStep(stepToExecute, nextKeyOnThresholdBreach); ClusterStateWaitStep.Result result = underTest.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), nullValue()); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), nullValue()); assertThat(underTest.getNextStepKey(), is(not(nextKeyOnThresholdBreach))); assertThat(underTest.getNextStepKey(), is(stepToExecute.getNextStepKey())); } @@ -184,11 +184,11 @@ public void testIsConditionMetForUnderlyingStep() { ClusterStateWaitUntilThresholdStep underTest = new ClusterStateWaitUntilThresholdStep(stepToExecute, nextKeyOnThresholdBreach); ClusterStateWaitStep.Result result = underTest.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), notNullValue()); - SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.getInformationContext(); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), notNullValue()); + SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.informationContext(); assertThat( - info.getMessage(), + info.message(), equalTo( "[" + currentStepKey.name() @@ -267,7 +267,7 @@ public boolean isRetryable() { new StepKey("phase", "action", "breached") ); - assertFalse(step.isConditionMet(indexMetadata.getIndex(), clusterState).isComplete()); + assertFalse(step.isConditionMet(indexMetadata.getIndex(), clusterState).complete()); assertThat(step.getNextStepKey().name(), equalTo("next-key")); @@ -290,7 +290,7 @@ public boolean isRetryable() { }, new StepKey("phase", "action", "breached") ); - assertTrue(step.isConditionMet(indexMetadata.getIndex(), clusterState).isComplete()); + assertTrue(step.isConditionMet(indexMetadata.getIndex(), clusterState).complete()); assertThat(step.getNextStepKey().name(), equalTo("breached")); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/DataTierMigrationRoutedStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/DataTierMigrationRoutedStepTests.java index 95c1f5c4aa96b..51d1464ed5525 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/DataTierMigrationRoutedStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/DataTierMigrationRoutedStepTests.java @@ -89,8 +89,8 @@ public void testExecuteWithUnassignedShard() { Result expectedResult = new Result(false, waitingForActiveShardsAllocationInfo(1)); Result actualResult = step.isConditionMet(index, clusterState); - assertThat(actualResult.isComplete(), is(false)); - assertThat(actualResult.getInformationContext(), is(expectedResult.getInformationContext())); + assertThat(actualResult.complete(), is(false)); + assertThat(actualResult.informationContext(), is(expectedResult.informationContext())); } public void testExecuteWithPendingShards() { @@ -129,8 +129,8 @@ public void testExecuteWithPendingShards() { ); Result actualResult = step.isConditionMet(index, clusterState); - assertThat(actualResult.isComplete(), is(false)); - assertThat(actualResult.getInformationContext(), is(expectedResult.getInformationContext())); + assertThat(actualResult.complete(), is(false)); + assertThat(actualResult.informationContext(), is(expectedResult.informationContext())); } public void testExecuteWithPendingShardsAndTargetRoleNotPresentInCluster() { @@ -163,8 +163,8 @@ public void testExecuteWithPendingShardsAndTargetRoleNotPresentInCluster() { ); Result actualResult = step.isConditionMet(index, clusterState); - assertThat(actualResult.isComplete(), is(false)); - assertThat(actualResult.getInformationContext(), is(expectedResult.getInformationContext())); + assertThat(actualResult.complete(), is(false)); + assertThat(actualResult.informationContext(), is(expectedResult.informationContext())); } public void testExecuteIndexMissing() { @@ -174,8 +174,8 @@ public void testExecuteIndexMissing() { DataTierMigrationRoutedStep step = createRandomInstance(); Result actualResult = step.isConditionMet(index, clusterState); - assertThat(actualResult.isComplete(), is(false)); - assertThat(actualResult.getInformationContext(), is(nullValue())); + assertThat(actualResult.complete(), is(false)); + assertThat(actualResult.informationContext(), is(nullValue())); } public void testExecuteIsComplete() { @@ -199,8 +199,8 @@ public void testExecuteIsComplete() { .build(); DataTierMigrationRoutedStep step = createRandomInstance(); Result result = step.isConditionMet(index, clusterState); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), is(nullValue())); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), is(nullValue())); } public void testExecuteWithGenericDataNodes() { @@ -220,8 +220,8 @@ public void testExecuteWithGenericDataNodes() { .build(); DataTierMigrationRoutedStep step = createRandomInstance(); Result result = step.isConditionMet(index, clusterState); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), is(nullValue())); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), is(nullValue())); } public void testExecuteForIndexWithoutTierRoutingInformationWaitsForReplicasToBeActive() { @@ -245,8 +245,8 @@ public void testExecuteForIndexWithoutTierRoutingInformationWaitsForReplicasToBe Result expectedResult = new Result(false, waitingForActiveShardsAllocationInfo(1)); Result result = step.isConditionMet(index, clusterState); - assertThat(result.isComplete(), is(false)); - assertThat(result.getInformationContext(), is(expectedResult.getInformationContext())); + assertThat(result.complete(), is(false)); + assertThat(result.informationContext(), is(expectedResult.informationContext())); } { @@ -266,8 +266,8 @@ public void testExecuteForIndexWithoutTierRoutingInformationWaitsForReplicasToBe DataTierMigrationRoutedStep step = createRandomInstance(); Result result = step.isConditionMet(index, clusterState); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), is(nullValue())); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), is(nullValue())); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SegmentCountStepInfoTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SegmentCountStepInfoTests.java index 7aeeba557ee54..9e0c7c7c6b167 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SegmentCountStepInfoTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SegmentCountStepInfoTests.java @@ -38,7 +38,7 @@ public final void testEqualsAndHashcode() { } protected final Info copyInstance(Info instance) throws IOException { - return new Info(instance.getNumberShardsLeftToMerge()); + return new Info(instance.numberShardsLeftToMerge()); } protected Info mutateInstance(Info instance) throws IOException { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkShardsAllocatedStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkShardsAllocatedStepTests.java index 59eff971c1643..592d259f07069 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkShardsAllocatedStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkShardsAllocatedStepTests.java @@ -94,8 +94,8 @@ public void testConditionMet() { .build(); Result result = step.isConditionMet(originalIndexMetadata.getIndex(), clusterState); - assertTrue(result.isComplete()); - assertNull(result.getInformationContext()); + assertTrue(result.complete()); + assertNull(result.informationContext()); } public void testConditionNotMetBecauseOfActive() { @@ -137,8 +137,8 @@ public void testConditionNotMetBecauseOfActive() { .build(); Result result = step.isConditionMet(originalIndexMetadata.getIndex(), clusterState); - assertFalse(result.isComplete()); - assertEquals(new ShrunkShardsAllocatedStep.Info(true, shrinkNumberOfShards, false), result.getInformationContext()); + assertFalse(result.complete()); + assertEquals(new ShrunkShardsAllocatedStep.Info(true, shrinkNumberOfShards, false), result.informationContext()); } public void testConditionNotMetBecauseOfShrunkIndexDoesntExistYet() { @@ -166,7 +166,7 @@ public void testConditionNotMetBecauseOfShrunkIndexDoesntExistYet() { .build(); Result result = step.isConditionMet(originalIndexMetadata.getIndex(), clusterState); - assertFalse(result.isComplete()); - assertEquals(new ShrunkShardsAllocatedStep.Info(false, -1, false), result.getInformationContext()); + assertFalse(result.complete()); + assertEquals(new ShrunkShardsAllocatedStep.Info(false, -1, false), result.informationContext()); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkenIndexCheckStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkenIndexCheckStepTests.java index 523404a00a0c5..4eb49df7f89c5 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkenIndexCheckStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrunkenIndexCheckStepTests.java @@ -59,8 +59,8 @@ public void testConditionMet() { ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT).metadata(metadata).build(); Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); - assertTrue(result.isComplete()); - assertNull(result.getInformationContext()); + assertTrue(result.complete()); + assertNull(result.informationContext()); } public void testConditionNotMetBecauseNotSameShrunkenIndex() { @@ -77,8 +77,8 @@ public void testConditionNotMetBecauseNotSameShrunkenIndex() { .build(); ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT).metadata(metadata).build(); Result result = step.isConditionMet(shrinkIndexMetadata.getIndex(), clusterState); - assertFalse(result.isComplete()); - assertEquals(new ShrunkenIndexCheckStep.Info(sourceIndex), result.getInformationContext()); + assertFalse(result.complete()); + assertEquals(new ShrunkenIndexCheckStep.Info(sourceIndex), result.informationContext()); } public void testConditionNotMetBecauseSourceIndexExists() { @@ -101,8 +101,8 @@ public void testConditionNotMetBecauseSourceIndexExists() { .build(); ClusterState clusterState = ClusterState.builder(ClusterName.DEFAULT).metadata(metadata).build(); Result result = step.isConditionMet(shrinkIndexMetadata.getIndex(), clusterState); - assertFalse(result.isComplete()); - assertEquals(new ShrunkenIndexCheckStep.Info(sourceIndex), result.getInformationContext()); + assertFalse(result.complete()); + assertEquals(new ShrunkenIndexCheckStep.Info(sourceIndex), result.informationContext()); } public void testIllegalState() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForActiveShardsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForActiveShardsTests.java index e12bae3b92f80..328698254dc76 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForActiveShardsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForActiveShardsTests.java @@ -125,7 +125,7 @@ public void testResultEvaluatedOnWriteIndexAliasWhenExists() { assertThat( "the rolled index has both the primary and the replica shards started so the condition should be met", - createRandomInstance().isConditionMet(originalIndex.getIndex(), clusterState).isComplete(), + createRandomInstance().isConditionMet(originalIndex.getIndex(), clusterState).complete(), is(true) ); } @@ -163,7 +163,7 @@ public void testResultEvaluatedOnOnlyIndexTheAliasPointsToIfWriteIndexIsNull() { assertThat( "the index the alias is pointing to has both the primary and the replica shards started so the condition should be" + " met", - createRandomInstance().isConditionMet(originalIndex.getIndex(), clusterState).isComplete(), + createRandomInstance().isConditionMet(originalIndex.getIndex(), clusterState).complete(), is(true) ); } @@ -244,13 +244,13 @@ public void testResultEvaluatedOnDataStream() throws IOException { boolean useFailureStore = randomBoolean(); IndexMetadata indexToOperateOn = useFailureStore ? failureOriginalIndexMeta : originalIndexMeta; ClusterStateWaitStep.Result result = waitForActiveShardsStep.isConditionMet(indexToOperateOn.getIndex(), clusterState); - assertThat(result.isComplete(), is(false)); + assertThat(result.complete(), is(false)); XContentBuilder expected = new WaitForActiveShardsStep.ActiveShardsInfo(2, "3", false).toXContent( JsonXContent.contentBuilder(), ToXContent.EMPTY_PARAMS ); - String actualResultAsString = Strings.toString(result.getInformationContext()); + String actualResultAsString = Strings.toString(result.informationContext()); assertThat(actualResultAsString, is(Strings.toString(expected))); assertThat(actualResultAsString, containsString("waiting for [3] shards to become active, but only [2] are active")); } @@ -288,13 +288,13 @@ public void testResultReportsMeaningfulMessage() throws IOException { .build(); ClusterStateWaitStep.Result result = createRandomInstance().isConditionMet(originalIndex.getIndex(), clusterState); - assertThat(result.isComplete(), is(false)); + assertThat(result.complete(), is(false)); XContentBuilder expected = new WaitForActiveShardsStep.ActiveShardsInfo(2, "3", false).toXContent( JsonXContent.contentBuilder(), ToXContent.EMPTY_PARAMS ); - String actualResultAsString = Strings.toString(result.getInformationContext()); + String actualResultAsString = Strings.toString(result.informationContext()); assertThat(actualResultAsString, is(Strings.toString(expected))); assertThat(actualResultAsString, containsString("waiting for [3] shards to become active, but only [2] are active")); } @@ -316,9 +316,9 @@ public void testResultReportsErrorMessage() { WaitForActiveShardsStep step = createRandomInstance(); ClusterStateWaitStep.Result result = step.isConditionMet(new Index("index-000000", UUID.randomUUID().toString()), clusterState); - assertThat(result.isComplete(), is(false)); + assertThat(result.complete(), is(false)); - String actualResultAsString = Strings.toString(result.getInformationContext()); + String actualResultAsString = Strings.toString(result.informationContext()); assertThat( actualResultAsString, containsString( diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForDataTierStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForDataTierStepTests.java index 3247c02cd9bac..00012575ea5de 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForDataTierStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForDataTierStepTests.java @@ -79,11 +79,11 @@ public void testConditionMet() { private void verify(WaitForDataTierStep step, ClusterState state, boolean complete, String message) { ClusterStateWaitStep.Result result = step.isConditionMet(null, state); - assertThat(result.isComplete(), is(complete)); + assertThat(result.complete(), is(complete)); if (message != null) { - assertThat(Strings.toString(result.getInformationContext()), containsString(message)); + assertThat(Strings.toString(result.informationContext()), containsString(message)); } else { - assertThat(result.getInformationContext(), is(nullValue())); + assertThat(result.informationContext(), is(nullValue())); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStepInfoTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStepInfoTests.java index 62c12e272ef59..0e5323d51f155 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStepInfoTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStepInfoTests.java @@ -10,7 +10,7 @@ import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ilm.WaitForFollowShardTasksStep.Info; -import org.elasticsearch.xpack.core.ilm.WaitForFollowShardTasksStep.Info.ShardFollowTaskInfo; +import org.elasticsearch.xpack.core.ilm.WaitForFollowShardTasksStep.ShardFollowTaskInfo; import java.io.IOException; import java.util.ArrayList; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStepTests.java index 162f0ec3361b4..4ac5511a247c9 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStepTests.java @@ -131,10 +131,10 @@ public void onFailure(Exception e) { assertThat(informationContextHolder[0], notNullValue()); assertThat(exceptionHolder[0], nullValue()); WaitForFollowShardTasksStep.Info info = (WaitForFollowShardTasksStep.Info) informationContextHolder[0]; - assertThat(info.getShardFollowTaskInfos().size(), equalTo(1)); - assertThat(info.getShardFollowTaskInfos().get(0).getShardId(), equalTo(1)); - assertThat(info.getShardFollowTaskInfos().get(0).getLeaderGlobalCheckpoint(), equalTo(8L)); - assertThat(info.getShardFollowTaskInfos().get(0).getFollowerGlobalCheckpoint(), equalTo(3L)); + assertThat(info.shardFollowTaskInfos().size(), equalTo(1)); + assertThat(info.shardFollowTaskInfos().get(0).shardId(), equalTo(1)); + assertThat(info.shardFollowTaskInfos().get(0).leaderGlobalCheckpoint(), equalTo(8L)); + assertThat(info.shardFollowTaskInfos().get(0).followerGlobalCheckpoint(), equalTo(3L)); } public void testConditionNotMetNotAFollowerIndex() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java index 0ae7b02c7400a..1414788f3ff98 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexColorStepTests.java @@ -93,8 +93,8 @@ public void testConditionMetForGreen() { WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.GREEN); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), nullValue()); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), nullValue()); } public void testConditionNotMetForGreen() { @@ -119,10 +119,10 @@ public void testConditionNotMetForGreen() { WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.GREEN); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(false)); - SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.getInformationContext(); + assertThat(result.complete(), is(false)); + SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.informationContext(); assertThat(info, notNullValue()); - assertThat(info.getMessage(), equalTo("index is not green; not all shards are active")); + assertThat(info.message(), equalTo("index is not green; not all shards are active")); } public void testConditionNotMetNoIndexRoutingTable() { @@ -139,10 +139,10 @@ public void testConditionNotMetNoIndexRoutingTable() { WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(false)); - SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.getInformationContext(); + assertThat(result.complete(), is(false)); + SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.informationContext(); assertThat(info, notNullValue()); - assertThat(info.getMessage(), equalTo("index is red; no indexRoutingTable")); + assertThat(info.message(), equalTo("index is red; no indexRoutingTable")); } public void testConditionMetForYellow() { @@ -167,8 +167,8 @@ public void testConditionMetForYellow() { WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), nullValue()); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), nullValue()); } public void testConditionNotMetForYellow() { @@ -193,10 +193,10 @@ public void testConditionNotMetForYellow() { WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(false)); - SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.getInformationContext(); + assertThat(result.complete(), is(false)); + SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.informationContext(); assertThat(info, notNullValue()); - assertThat(info.getMessage(), equalTo("index is red; not all primary shards are active")); + assertThat(info.message(), equalTo("index is red; not all primary shards are active")); } public void testConditionNotMetNoIndexRoutingTableForYellow() { @@ -213,10 +213,10 @@ public void testConditionNotMetNoIndexRoutingTableForYellow() { WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.YELLOW); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(false)); - SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.getInformationContext(); + assertThat(result.complete(), is(false)); + SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.informationContext(); assertThat(info, notNullValue()); - assertThat(info.getMessage(), equalTo("index is red; no indexRoutingTable")); + assertThat(info.message(), equalTo("index is red; no indexRoutingTable")); } public void testStepReturnsFalseIfTargetIndexIsMissing() { @@ -243,11 +243,11 @@ public void testStepReturnsFalseIfTargetIndexIsMissing() { WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.GREEN, indexPrefix); ClusterStateWaitStep.Result result = step.isConditionMet(originalIndex.getIndex(), clusterState); - assertThat(result.isComplete(), is(false)); - SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.getInformationContext(); + assertThat(result.complete(), is(false)); + SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.informationContext(); String targetIndex = indexPrefix + originalIndex.getIndex().getName(); assertThat( - info.getMessage(), + info.message(), is( "[" + step.getKey().action() @@ -303,9 +303,9 @@ public void testStepWaitsForTargetIndexHealthWhenPrefixConfigured() { WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.GREEN); ClusterStateWaitStep.Result result = step.isConditionMet(originalIndex.getIndex(), clusterTargetInitializing); - assertThat(result.isComplete(), is(false)); - SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.getInformationContext(); - assertThat(info.getMessage(), is("index is not green; not all shards are active")); + assertThat(result.complete(), is(false)); + SingleMessageFieldInfo info = (SingleMessageFieldInfo) result.informationContext(); + assertThat(info.message(), is("index is not green; not all shards are active")); } { @@ -326,8 +326,8 @@ public void testStepWaitsForTargetIndexHealthWhenPrefixConfigured() { WaitForIndexColorStep step = new WaitForIndexColorStep(randomStepKey(), randomStepKey(), ClusterHealthStatus.GREEN); ClusterStateWaitStep.Result result = step.isConditionMet(originalIndex.getIndex(), clusterTargetInitializing); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), nullValue()); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), nullValue()); } } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexingCompleteStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexingCompleteStepTests.java index ad5e4c9533c99..2f91393b451d7 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexingCompleteStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexingCompleteStepTests.java @@ -65,8 +65,8 @@ public void testConditionMet() { WaitForIndexingCompleteStep step = createRandomInstance(); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), nullValue()); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), nullValue()); } public void testConditionMetNotAFollowerIndex() { @@ -82,8 +82,8 @@ public void testConditionMetNotAFollowerIndex() { WaitForIndexingCompleteStep step = createRandomInstance(); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(true)); - assertThat(result.getInformationContext(), nullValue()); + assertThat(result.complete(), is(true)); + assertThat(result.informationContext(), nullValue()); } public void testConditionNotMet() { @@ -104,10 +104,10 @@ public void testConditionNotMet() { WaitForIndexingCompleteStep step = createRandomInstance(); ClusterStateWaitStep.Result result = step.isConditionMet(indexMetadata.getIndex(), clusterState); - assertThat(result.isComplete(), is(false)); - assertThat(result.getInformationContext(), notNullValue()); + assertThat(result.complete(), is(false)); + assertThat(result.informationContext(), notNullValue()); WaitForIndexingCompleteStep.IndexingNotCompleteInfo info = (WaitForIndexingCompleteStep.IndexingNotCompleteInfo) result - .getInformationContext(); + .informationContext(); assertThat( info.getMessage(), equalTo( @@ -122,7 +122,7 @@ public void testIndexDeleted() { WaitForIndexingCompleteStep step = createRandomInstance(); ClusterStateWaitStep.Result result = step.isConditionMet(new Index("this-index-doesnt-exist", "uuid"), clusterState); - assertThat(result.isComplete(), is(false)); - assertThat(result.getInformationContext(), nullValue()); + assertThat(result.complete(), is(false)); + assertThat(result.informationContext(), nullValue()); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/step/info/AllocationRoutedStepInfoTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/step/info/AllocationRoutedStepInfoTests.java index 67214868293ea..0e6903ba6cf44 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/step/info/AllocationRoutedStepInfoTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/step/info/AllocationRoutedStepInfoTests.java @@ -38,18 +38,18 @@ public final void testEqualsAndHashcode() { protected final AllocationInfo copyInstance(AllocationInfo instance) { return new AllocationInfo( - instance.getNumberOfReplicas(), - instance.getNumberShardsLeftToAllocate(), + instance.numberOfReplicas(), + instance.numberShardsLeftToAllocate(), instance.allShardsActive(), - instance.getMessage() + instance.message() ); } protected AllocationInfo mutateInstance(AllocationInfo instance) throws IOException { - long actualReplicas = instance.getNumberOfReplicas(); - long shardsToAllocate = instance.getNumberShardsLeftToAllocate(); + long actualReplicas = instance.numberOfReplicas(); + long shardsToAllocate = instance.numberShardsLeftToAllocate(); boolean allShardsActive = instance.allShardsActive(); - var message = instance.getMessage(); + var message = instance.message(); switch (between(0, 2)) { case 0 -> shardsToAllocate += between(1, 20); case 1 -> allShardsActive = allShardsActive == false; diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index e06c7bc2708ca..9efe46402428c 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -43,7 +43,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.SortedMap; import java.util.TreeMap; @@ -811,13 +810,12 @@ static String convertAttributeValueToTierPreference(String nodeAttributeValue) { * Represents the elasticsearch abstractions that were, in some way, migrated such that the system is managing indices lifecycles and * allocations using data tiers. */ - public static final class MigratedEntities { - @Nullable - public final String removedIndexTemplateName; - public final List migratedIndices; - public final List migratedPolicies; - public final MigratedTemplates migratedTemplates; - + public record MigratedEntities( + @Nullable String removedIndexTemplateName, + List migratedIndices, + List migratedPolicies, + MigratedTemplates migratedTemplates + ) { public MigratedEntities( @Nullable String removedIndexTemplateName, List migratedIndices, @@ -830,36 +828,17 @@ public MigratedEntities( this.migratedTemplates = migratedTemplates; } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - MigratedEntities that = (MigratedEntities) o; - return Objects.equals(removedIndexTemplateName, that.removedIndexTemplateName) - && Objects.equals(migratedIndices, that.migratedIndices) - && Objects.equals(migratedPolicies, that.migratedPolicies) - && Objects.equals(migratedTemplates, that.migratedTemplates); - } - - @Override - public int hashCode() { - return Objects.hash(removedIndexTemplateName, migratedIndices, migratedPolicies, migratedTemplates); - } } /** * Represents the legacy, composable, and component templates that were migrated away from shard allocation settings based on custom * node attributes. */ - public static final class MigratedTemplates { - public final List migratedLegacyTemplates; - public final List migratedComposableTemplates; - public final List migratedComponentTemplates; - + public record MigratedTemplates( + List migratedLegacyTemplates, + List migratedComposableTemplates, + List migratedComponentTemplates + ) { public MigratedTemplates( List migratedLegacyTemplates, List migratedComposableTemplates, @@ -869,24 +848,5 @@ public MigratedTemplates( this.migratedComposableTemplates = Collections.unmodifiableList(migratedComposableTemplates); this.migratedComponentTemplates = Collections.unmodifiableList(migratedComponentTemplates); } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - MigratedTemplates that = (MigratedTemplates) o; - return Objects.equals(migratedLegacyTemplates, that.migratedLegacyTemplates) - && Objects.equals(migratedComposableTemplates, that.migratedComposableTemplates) - && Objects.equals(migratedComponentTemplates, that.migratedComponentTemplates); - } - - @Override - public int hashCode() { - return Objects.hash(migratedLegacyTemplates, migratedComposableTemplates, migratedComponentTemplates); - } } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java index 77b143f93576b..8c08194b11e05 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTask.java @@ -159,7 +159,7 @@ public ClusterState doExecute(final ClusterState currentState) throws IOExceptio // to be met (eg. {@link LifecycleSettings#LIFECYCLE_STEP_WAIT_TIME_THRESHOLD_SETTING}, so it's important we // re-evaluate what the next step is after we evaluate the condition nextStepKey = currentStep.getNextStepKey(); - if (result.isComplete()) { + if (result.complete()) { logger.trace( "[{}] cluster state step condition met successfully ({}) [{}], moving to next step {}", index.getName(), @@ -180,7 +180,7 @@ public ClusterState doExecute(final ClusterState currentState) throws IOExceptio ); } } else { - final ToXContentObject stepInfo = result.getInformationContext(); + final ToXContentObject stepInfo = result.informationContext(); if (logger.isTraceEnabled()) { logger.trace( "[{}] condition not met ({}) [{}], returning existing state (info: {})", diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java index 494f0ee444236..ef7554beed9e9 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportMigrateToDataTiersAction.java @@ -100,12 +100,12 @@ protected void masterOperation( ).v2(); listener.onResponse( new MigrateToDataTiersResponse( - entities.removedIndexTemplateName, - entities.migratedPolicies, - entities.migratedIndices, - entities.migratedTemplates.migratedLegacyTemplates, - entities.migratedTemplates.migratedComposableTemplates, - entities.migratedTemplates.migratedComponentTemplates, + entities.removedIndexTemplateName(), + entities.migratedPolicies(), + entities.migratedIndices(), + entities.migratedTemplates().migratedLegacyTemplates(), + entities.migratedTemplates().migratedComposableTemplates(), + entities.migratedTemplates().migratedComponentTemplates(), true ) ); @@ -161,12 +161,12 @@ public void onFailure(Exception e) { MigratedEntities entities = migratedEntities.get(); listener.onResponse( new MigrateToDataTiersResponse( - entities.removedIndexTemplateName, - entities.migratedPolicies, - entities.migratedIndices, - entities.migratedTemplates.migratedLegacyTemplates, - entities.migratedTemplates.migratedComposableTemplates, - entities.migratedTemplates.migratedComponentTemplates, + entities.removedIndexTemplateName(), + entities.migratedPolicies(), + entities.migratedIndices(), + entities.migratedTemplates().migratedLegacyTemplates(), + entities.migratedTemplates().migratedComposableTemplates(), + entities.migratedTemplates().migratedComponentTemplates(), false ) ); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java index 51df651ea4a4c..570c2f5231acf 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java @@ -1080,11 +1080,11 @@ public void testMigrateToDataTiersRouting() { ); MigratedEntities migratedEntities = migratedEntitiesTuple.v2(); - assertThat(migratedEntities.removedIndexTemplateName, is("catch-all")); - assertThat(migratedEntities.migratedPolicies.size(), is(1)); - assertThat(migratedEntities.migratedPolicies.get(0), is(lifecycleName)); - assertThat(migratedEntities.migratedIndices.size(), is(2)); - assertThat(migratedEntities.migratedIndices, hasItems("indexWithWarmDataAttribute", "indexWithUnknownDataAttribute")); + assertThat(migratedEntities.removedIndexTemplateName(), is("catch-all")); + assertThat(migratedEntities.migratedPolicies().size(), is(1)); + assertThat(migratedEntities.migratedPolicies().get(0), is(lifecycleName)); + assertThat(migratedEntities.migratedIndices().size(), is(2)); + assertThat(migratedEntities.migratedIndices(), hasItems("indexWithWarmDataAttribute", "indexWithUnknownDataAttribute")); ClusterState newState = migratedEntitiesTuple.v1(); assertThat(newState.metadata().getTemplates().size(), is(1)); @@ -1105,11 +1105,11 @@ public void testMigrateToDataTiersRouting() { ); MigratedEntities migratedEntities = migratedEntitiesTuple.v2(); - assertThat(migratedEntities.removedIndexTemplateName, nullValue()); - assertThat(migratedEntities.migratedPolicies.size(), is(1)); - assertThat(migratedEntities.migratedPolicies.get(0), is(lifecycleName)); - assertThat(migratedEntities.migratedIndices.size(), is(2)); - assertThat(migratedEntities.migratedIndices, hasItems("indexWithWarmDataAttribute", "indexWithUnknownDataAttribute")); + assertThat(migratedEntities.removedIndexTemplateName(), nullValue()); + assertThat(migratedEntities.migratedPolicies().size(), is(1)); + assertThat(migratedEntities.migratedPolicies().get(0), is(lifecycleName)); + assertThat(migratedEntities.migratedIndices().size(), is(2)); + assertThat(migratedEntities.migratedIndices(), hasItems("indexWithWarmDataAttribute", "indexWithUnknownDataAttribute")); ClusterState newState = migratedEntitiesTuple.v1(); assertThat(newState.metadata().getTemplates().size(), is(2)); @@ -1130,10 +1130,10 @@ public void testMigrateToDataTiersRouting() { ); MigratedEntities migratedEntities = migratedEntitiesTuple.v2(); - assertThat(migratedEntities.migratedPolicies.size(), is(1)); - assertThat(migratedEntities.migratedPolicies.get(0), is(lifecycleName)); - assertThat(migratedEntities.migratedIndices.size(), is(2)); - assertThat(migratedEntities.migratedIndices, hasItems("indexWithWarmDataAttribute", "indexWithUnknownDataAttribute")); + assertThat(migratedEntities.migratedPolicies().size(), is(1)); + assertThat(migratedEntities.migratedPolicies().get(0), is(lifecycleName)); + assertThat(migratedEntities.migratedIndices().size(), is(2)); + assertThat(migratedEntities.migratedIndices(), hasItems("indexWithWarmDataAttribute", "indexWithUnknownDataAttribute")); IndexMetadata migratedIndex; migratedIndex = migratedEntitiesTuple.v1().metadata().index("indexWithWarmDataAttribute"); @@ -1185,9 +1185,9 @@ public void testMigrateToDataTiersRoutingRequiresILMStopped() { null, false ); - assertThat(migratedState.v2().migratedIndices, empty()); - assertThat(migratedState.v2().migratedPolicies, empty()); - assertThat(migratedState.v2().removedIndexTemplateName, nullValue()); + assertThat(migratedState.v2().migratedIndices(), empty()); + assertThat(migratedState.v2().migratedPolicies(), empty()); + assertThat(migratedState.v2().removedIndexTemplateName(), nullValue()); } } @@ -1232,7 +1232,7 @@ public void testMigrationDoesNotRemoveComposableTemplates() { null, false ); - assertThat(migratedEntitiesTuple.v2().removedIndexTemplateName, nullValue()); + assertThat(migratedEntitiesTuple.v2().removedIndexTemplateName(), nullValue()); // the composable template still exists, however it was migrated to not use the custom require.data routing setting assertThat(migratedEntitiesTuple.v1().metadata().templatesV2().get(composableTemplateName), is(notNullValue())); } @@ -1676,9 +1676,9 @@ public void testMigrateIndexAndComponentTemplates() { Metadata.Builder mb = Metadata.builder(clusterState.metadata()); MetadataMigrateToDataTiersRoutingService.MigratedTemplates migratedTemplates = MetadataMigrateToDataTiersRoutingService .migrateIndexAndComponentTemplates(mb, clusterState, nodeAttrName); - assertThat(migratedTemplates.migratedLegacyTemplates, is(List.of("template-with-require-routing"))); - assertThat(migratedTemplates.migratedComposableTemplates, is(List.of("composable-template-with-require-routing"))); - assertThat(migratedTemplates.migratedComponentTemplates, is(List.of("component-with-require-and-include-routing"))); + assertThat(migratedTemplates.migratedLegacyTemplates(), is(List.of("template-with-require-routing"))); + assertThat(migratedTemplates.migratedComposableTemplates(), is(List.of("composable-template-with-require-routing"))); + assertThat(migratedTemplates.migratedComponentTemplates(), is(List.of("component-with-require-and-include-routing"))); } private String getWarmPhaseDef() { From f4dc7168eb790e4c4b0540b2ea5b7b9072840ffe Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:03:36 -0500 Subject: [PATCH 26/77] [ML] Checking for presence of error object when validating response (#118375) * Refactoring error handling logic * Refactoring base response handler to remove duplication * Update docs/changelog/118375.yaml * Addressing feedback --------- Co-authored-by: Max Hniebergall <137079448+maxhniebergall@users.noreply.github.com> --- docs/changelog/118375.yaml | 5 + .../AlibabaCloudSearchResponseHandler.java | 14 +-- .../anthropic/AnthropicResponseHandler.java | 13 +- .../cohere/CohereResponseHandler.java | 14 +-- ...lasticInferenceServiceResponseHandler.java | 12 +- .../GoogleAiStudioResponseHandler.java | 13 +- .../GoogleVertexAiResponseHandler.java | 11 +- .../http/retry/BaseResponseHandler.java | 44 ++++++- .../external/http/retry/ErrorMessage.java | 12 -- .../external/http/retry/ErrorResponse.java | 37 ++++++ .../HuggingFaceResponseHandler.java | 14 +-- .../ibmwatsonx/IbmWatsonxResponseHandler.java | 13 +- .../openai/OpenAiResponseHandler.java | 19 +-- ...eMistralOpenAiExternalResponseHandler.java | 4 +- .../response/ErrorMessageResponseEntity.java | 39 +++--- ...AlibabaCloudSearchErrorResponseEntity.java | 19 +-- .../cohere/CohereErrorResponseEntity.java | 19 +-- ...icInferenceServiceErrorResponseEntity.java | 20 +-- .../GoogleAiStudioErrorResponseEntity.java | 24 ++-- .../GoogleVertexAiErrorResponseEntity.java | 24 ++-- .../HuggingFaceErrorResponseEntity.java | 20 +-- .../IbmWatsonxErrorResponseEntity.java | 22 ++-- .../openai/OpenAiErrorResponseEntity.java | 69 ---------- .../http/retry/BaseResponseHandlerTests.java | 118 ++++++++++++++++++ .../ErrorMessageResponseEntityTests.java | 33 ++++- ...baCloudSearchErrorResponseEntityTests.java | 2 +- .../CohereErrorResponseEntityTests.java | 8 +- ...erenceServiceErrorResponseEntityTests.java | 12 +- ...oogleAiStudioErrorResponseEntityTests.java | 9 +- ...oogleVertexAiErrorResponseEntityTests.java | 10 +- .../HuggingFaceErrorResponseEntityTests.java | 12 +- .../OpenAiErrorResponseEntityTests.java | 66 ---------- 32 files changed, 348 insertions(+), 403 deletions(-) create mode 100644 docs/changelog/118375.yaml delete mode 100644 x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/retry/ErrorMessage.java create mode 100644 x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/retry/ErrorResponse.java delete mode 100644 x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/openai/OpenAiErrorResponseEntity.java delete mode 100644 x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/openai/OpenAiErrorResponseEntityTests.java diff --git a/docs/changelog/118375.yaml b/docs/changelog/118375.yaml new file mode 100644 index 0000000000000..bad2751aeaa50 --- /dev/null +++ b/docs/changelog/118375.yaml @@ -0,0 +1,5 @@ +pr: 118375 +summary: Check for presence of error object when validating streaming responses from integrations in the inference API +area: Machine Learning +type: enhancement +issues: [] diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/alibabacloudsearch/AlibabaCloudSearchResponseHandler.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/alibabacloudsearch/AlibabaCloudSearchResponseHandler.java index ecfa988b5035e..30b371f3172ea 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/alibabacloudsearch/AlibabaCloudSearchResponseHandler.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/alibabacloudsearch/AlibabaCloudSearchResponseHandler.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.external.alibabacloudsearch; -import org.apache.logging.log4j.Logger; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.inference.external.http.HttpResult; import org.elasticsearch.xpack.inference.external.http.retry.BaseResponseHandler; @@ -15,9 +14,6 @@ import org.elasticsearch.xpack.inference.external.http.retry.RetryException; import org.elasticsearch.xpack.inference.external.request.Request; import org.elasticsearch.xpack.inference.external.response.alibabacloudsearch.AlibabaCloudSearchErrorResponseEntity; -import org.elasticsearch.xpack.inference.logging.ThrottlerManager; - -import static org.elasticsearch.xpack.inference.external.http.HttpUtils.checkForEmptyBody; /** * Defines how to handle various errors returned from the AlibabaCloudSearch integration. @@ -28,13 +24,6 @@ public AlibabaCloudSearchResponseHandler(String requestType, ResponseParser pars super(requestType, parseFunction, AlibabaCloudSearchErrorResponseEntity::fromResponse); } - @Override - public void validateResponse(ThrottlerManager throttlerManager, Logger logger, Request request, HttpResult result) - throws RetryException { - checkForFailureStatusCode(request, result); - checkForEmptyBody(throttlerManager, logger, request, result); - } - /** * Validates the status code throws an RetryException if not in the range [200, 300). * @@ -42,7 +31,8 @@ public void validateResponse(ThrottlerManager throttlerManager, Logger logger, R * @param result The http response and body * @throws RetryException Throws if status code is {@code >= 300 or < 200 } */ - void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { + @Override + protected void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { int statusCode = result.response().getStatusLine().getStatusCode(); if (RestStatus.isSuccessful(statusCode)) { return; diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/anthropic/AnthropicResponseHandler.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/anthropic/AnthropicResponseHandler.java index 373045f879afa..d9a78a56af0d6 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/anthropic/AnthropicResponseHandler.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/anthropic/AnthropicResponseHandler.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.external.anthropic; -import org.apache.logging.log4j.Logger; import org.elasticsearch.common.Strings; import org.elasticsearch.inference.InferenceServiceResults; import org.elasticsearch.xpack.core.inference.results.StreamingChatCompletionResults; @@ -19,11 +18,9 @@ import org.elasticsearch.xpack.inference.external.response.ErrorMessageResponseEntity; import org.elasticsearch.xpack.inference.external.response.streaming.ServerSentEventParser; import org.elasticsearch.xpack.inference.external.response.streaming.ServerSentEventProcessor; -import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import java.util.concurrent.Flow; -import static org.elasticsearch.xpack.inference.external.http.HttpUtils.checkForEmptyBody; import static org.elasticsearch.xpack.inference.external.http.retry.ResponseHandlerUtils.getFirstHeaderOrUnknown; public class AnthropicResponseHandler extends BaseResponseHandler { @@ -54,13 +51,6 @@ public AnthropicResponseHandler(String requestType, ResponseParser parseFunction this.canHandleStreamingResponses = canHandleStreamingResponses; } - @Override - public void validateResponse(ThrottlerManager throttlerManager, Logger logger, Request request, HttpResult result) - throws RetryException { - checkForFailureStatusCode(request, result); - checkForEmptyBody(throttlerManager, logger, request, result); - } - @Override public boolean canHandleStreamingResponses() { return canHandleStreamingResponses; @@ -83,7 +73,8 @@ public InferenceServiceResults parseResult(Request request, Flow.Publisher= 300 or < 200 } */ - void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { + @Override + protected void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { if (result.isSuccessfulResponse()) { return; } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/cohere/CohereResponseHandler.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/cohere/CohereResponseHandler.java index ac2e1747f8057..e3a74785caa4b 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/cohere/CohereResponseHandler.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/cohere/CohereResponseHandler.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.external.cohere; -import org.apache.logging.log4j.Logger; import org.elasticsearch.inference.InferenceServiceResults; import org.elasticsearch.xpack.core.inference.results.StreamingChatCompletionResults; import org.elasticsearch.xpack.inference.external.http.HttpResult; @@ -17,12 +16,9 @@ import org.elasticsearch.xpack.inference.external.request.Request; import org.elasticsearch.xpack.inference.external.response.cohere.CohereErrorResponseEntity; import org.elasticsearch.xpack.inference.external.response.streaming.NewlineDelimitedByteProcessor; -import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import java.util.concurrent.Flow; -import static org.elasticsearch.xpack.inference.external.http.HttpUtils.checkForEmptyBody; - /** * Defines how to handle various errors returned from the Cohere integration. * @@ -45,13 +41,6 @@ public CohereResponseHandler(String requestType, ResponseParser parseFunction, b this.canHandleStreamingResponse = canHandleStreamingResponse; } - @Override - public void validateResponse(ThrottlerManager throttlerManager, Logger logger, Request request, HttpResult result) - throws RetryException { - checkForFailureStatusCode(request, result); - checkForEmptyBody(throttlerManager, logger, request, result); - } - @Override public boolean canHandleStreamingResponses() { return canHandleStreamingResponse; @@ -73,7 +62,8 @@ public InferenceServiceResults parseResult(Request request, Flow.Publisher= 300 or < 200 } */ - void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { + @Override + protected void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { if (result.isSuccessfulResponse()) { return; } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/elastic/ElasticInferenceServiceResponseHandler.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/elastic/ElasticInferenceServiceResponseHandler.java index 2b79afb3b56fd..b11b4a743fb27 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/elastic/ElasticInferenceServiceResponseHandler.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/elastic/ElasticInferenceServiceResponseHandler.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.external.elastic; -import org.apache.logging.log4j.Logger; import org.elasticsearch.xpack.inference.external.http.HttpResult; import org.elasticsearch.xpack.inference.external.http.retry.BaseResponseHandler; import org.elasticsearch.xpack.inference.external.http.retry.ContentTooLargeException; @@ -15,9 +14,6 @@ import org.elasticsearch.xpack.inference.external.http.retry.RetryException; import org.elasticsearch.xpack.inference.external.request.Request; import org.elasticsearch.xpack.inference.external.response.elastic.ElasticInferenceServiceErrorResponseEntity; -import org.elasticsearch.xpack.inference.logging.ThrottlerManager; - -import static org.elasticsearch.xpack.inference.external.http.HttpUtils.checkForEmptyBody; public class ElasticInferenceServiceResponseHandler extends BaseResponseHandler { @@ -26,13 +22,7 @@ public ElasticInferenceServiceResponseHandler(String requestType, ResponseParser } @Override - public void validateResponse(ThrottlerManager throttlerManager, Logger logger, Request request, HttpResult result) - throws RetryException { - checkForFailureStatusCode(request, result); - checkForEmptyBody(throttlerManager, logger, request, result); - } - - void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { + protected void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { if (result.isSuccessfulResponse()) { return; } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/googleaistudio/GoogleAiStudioResponseHandler.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/googleaistudio/GoogleAiStudioResponseHandler.java index 0241dcd6142a6..d61e82cb83b45 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/googleaistudio/GoogleAiStudioResponseHandler.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/googleaistudio/GoogleAiStudioResponseHandler.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.external.googleaistudio; -import org.apache.logging.log4j.Logger; import org.elasticsearch.core.CheckedFunction; import org.elasticsearch.inference.InferenceServiceResults; import org.elasticsearch.xcontent.XContentParser; @@ -20,13 +19,11 @@ import org.elasticsearch.xpack.inference.external.response.googleaistudio.GoogleAiStudioErrorResponseEntity; import org.elasticsearch.xpack.inference.external.response.streaming.ServerSentEventParser; import org.elasticsearch.xpack.inference.external.response.streaming.ServerSentEventProcessor; -import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import java.io.IOException; import java.util.concurrent.Flow; import static org.elasticsearch.core.Strings.format; -import static org.elasticsearch.xpack.inference.external.http.HttpUtils.checkForEmptyBody; public class GoogleAiStudioResponseHandler extends BaseResponseHandler { @@ -52,13 +49,6 @@ public GoogleAiStudioResponseHandler( this.content = content; } - @Override - public void validateResponse(ThrottlerManager throttlerManager, Logger logger, Request request, HttpResult result) - throws RetryException { - checkForFailureStatusCode(request, result); - checkForEmptyBody(throttlerManager, logger, request, result); - } - /** * Validates the status code and throws a RetryException if not in the range [200, 300). * @@ -67,7 +57,8 @@ public void validateResponse(ThrottlerManager throttlerManager, Logger logger, R * @param result The http response and body * @throws RetryException Throws if status code is {@code >= 300 or < 200 } */ - void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { + @Override + protected void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { if (result.isSuccessfulResponse()) { return; } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/googlevertexai/GoogleVertexAiResponseHandler.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/googlevertexai/GoogleVertexAiResponseHandler.java index 6b1aef9856d33..6924a5e4cb336 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/googlevertexai/GoogleVertexAiResponseHandler.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/googlevertexai/GoogleVertexAiResponseHandler.java @@ -7,17 +7,14 @@ package org.elasticsearch.xpack.inference.external.googlevertexai; -import org.apache.logging.log4j.Logger; import org.elasticsearch.xpack.inference.external.http.HttpResult; import org.elasticsearch.xpack.inference.external.http.retry.BaseResponseHandler; import org.elasticsearch.xpack.inference.external.http.retry.ResponseParser; import org.elasticsearch.xpack.inference.external.http.retry.RetryException; import org.elasticsearch.xpack.inference.external.request.Request; import org.elasticsearch.xpack.inference.external.response.googlevertexai.GoogleVertexAiErrorResponseEntity; -import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import static org.elasticsearch.core.Strings.format; -import static org.elasticsearch.xpack.inference.external.http.HttpUtils.checkForEmptyBody; public class GoogleVertexAiResponseHandler extends BaseResponseHandler { @@ -28,13 +25,7 @@ public GoogleVertexAiResponseHandler(String requestType, ResponseParser parseFun } @Override - public void validateResponse(ThrottlerManager throttlerManager, Logger logger, Request request, HttpResult result) - throws RetryException { - checkForFailureStatusCode(request, result); - checkForEmptyBody(throttlerManager, logger, request, result); - } - - void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { + protected void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { if (result.isSuccessfulResponse()) { return; } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/retry/BaseResponseHandler.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/retry/BaseResponseHandler.java index c9cbe169ec03d..1b0dd893ada6f 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/retry/BaseResponseHandler.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/retry/BaseResponseHandler.java @@ -7,16 +7,20 @@ package org.elasticsearch.xpack.inference.external.http.retry; +import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.common.Strings; import org.elasticsearch.inference.InferenceServiceResults; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.inference.external.http.HttpResult; import org.elasticsearch.xpack.inference.external.request.Request; +import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import java.util.Objects; import java.util.function.Function; import static org.elasticsearch.core.Strings.format; +import static org.elasticsearch.xpack.inference.external.http.HttpUtils.checkForEmptyBody; public abstract class BaseResponseHandler implements ResponseHandler { @@ -27,14 +31,15 @@ public abstract class BaseResponseHandler implements ResponseHandler { public static final String REDIRECTION = "Unhandled redirection"; public static final String CONTENT_TOO_LARGE = "Received a content too large status code"; public static final String UNSUCCESSFUL = "Received an unsuccessful status code"; + public static final String SERVER_ERROR_OBJECT = "Received an error response"; public static final String BAD_REQUEST = "Received a bad request status code"; public static final String METHOD_NOT_ALLOWED = "Received a method not allowed status code"; protected final String requestType; private final ResponseParser parseFunction; - private final Function errorParseFunction; + private final Function errorParseFunction; - public BaseResponseHandler(String requestType, ResponseParser parseFunction, Function errorParseFunction) { + public BaseResponseHandler(String requestType, ResponseParser parseFunction, Function errorParseFunction) { this.requestType = Objects.requireNonNull(requestType); this.parseFunction = Objects.requireNonNull(parseFunction); this.errorParseFunction = Objects.requireNonNull(errorParseFunction); @@ -54,11 +59,42 @@ public String getRequestType() { return requestType; } + @Override + public void validateResponse(ThrottlerManager throttlerManager, Logger logger, Request request, HttpResult result) { + checkForFailureStatusCode(request, result); + checkForEmptyBody(throttlerManager, logger, request, result); + + // When the response is streamed the status code could be 200 but the error object will be set + // so we need to check for that specifically + checkForErrorObject(request, result); + } + + protected abstract void checkForFailureStatusCode(Request request, HttpResult result); + + private void checkForErrorObject(Request request, HttpResult result) { + var errorEntity = errorParseFunction.apply(result); + + if (errorEntity.errorStructureFound()) { + // We don't really know what happened because the status code was 200 so we'll return a failure and let the + // client retry if necessary + // If we did want to retry here, we'll need to determine if this was a streaming request, if it was + // we shouldn't retry because that would replay the entire streaming request and the client would get + // duplicate chunks back + throw new RetryException(false, buildError(SERVER_ERROR_OBJECT, request, result, errorEntity)); + } + } + protected Exception buildError(String message, Request request, HttpResult result) { var errorEntityMsg = errorParseFunction.apply(result); + return buildError(message, request, result, errorEntityMsg); + } + + protected Exception buildError(String message, Request request, HttpResult result, ErrorResponse errorResponse) { var responseStatusCode = result.response().getStatusLine().getStatusCode(); - if (errorEntityMsg == null) { + if (errorResponse == null + || errorResponse.errorStructureFound() == false + || Strings.isNullOrEmpty(errorResponse.getErrorMessage())) { return new ElasticsearchStatusException( format( "%s for request from inference entity id [%s] status [%s]", @@ -76,7 +112,7 @@ protected Exception buildError(String message, Request request, HttpResult resul message, request.getInferenceEntityId(), responseStatusCode, - errorEntityMsg.getErrorMessage() + errorResponse.getErrorMessage() ), toRestStatus(responseStatusCode) ); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/retry/ErrorMessage.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/retry/ErrorMessage.java deleted file mode 100644 index a4be7f15827fb..0000000000000 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/retry/ErrorMessage.java +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.inference.external.http.retry; - -public interface ErrorMessage { - String getErrorMessage(); -} diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/retry/ErrorResponse.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/retry/ErrorResponse.java new file mode 100644 index 0000000000000..be9669c331371 --- /dev/null +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/retry/ErrorResponse.java @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.inference.external.http.retry; + +import java.util.Objects; + +public class ErrorResponse { + + // Denotes an error object that was not found + public static final ErrorResponse UNDEFINED_ERROR = new ErrorResponse(false); + + private final String errorMessage; + private final boolean errorStructureFound; + + public ErrorResponse(String errorMessage) { + this.errorMessage = Objects.requireNonNull(errorMessage); + this.errorStructureFound = true; + } + + private ErrorResponse(boolean errorStructureFound) { + this.errorMessage = ""; + this.errorStructureFound = errorStructureFound; + } + + public String getErrorMessage() { + return errorMessage; + } + + public boolean errorStructureFound() { + return errorStructureFound; + } +} diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/huggingface/HuggingFaceResponseHandler.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/huggingface/HuggingFaceResponseHandler.java index f6fd9afabe28d..5b6b0de4f2428 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/huggingface/HuggingFaceResponseHandler.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/huggingface/HuggingFaceResponseHandler.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.external.huggingface; -import org.apache.logging.log4j.Logger; import org.elasticsearch.xpack.inference.external.http.HttpResult; import org.elasticsearch.xpack.inference.external.http.retry.BaseResponseHandler; import org.elasticsearch.xpack.inference.external.http.retry.ContentTooLargeException; @@ -15,9 +14,6 @@ import org.elasticsearch.xpack.inference.external.http.retry.RetryException; import org.elasticsearch.xpack.inference.external.request.Request; import org.elasticsearch.xpack.inference.external.response.huggingface.HuggingFaceErrorResponseEntity; -import org.elasticsearch.xpack.inference.logging.ThrottlerManager; - -import static org.elasticsearch.xpack.inference.external.http.HttpUtils.checkForEmptyBody; public class HuggingFaceResponseHandler extends BaseResponseHandler { @@ -25,13 +21,6 @@ public HuggingFaceResponseHandler(String requestType, ResponseParser parseFuncti super(requestType, parseFunction, HuggingFaceErrorResponseEntity::fromResponse); } - @Override - public void validateResponse(ThrottlerManager throttlerManager, Logger logger, Request request, HttpResult result) - throws RetryException { - checkForFailureStatusCode(request, result); - checkForEmptyBody(throttlerManager, logger, request, result); - } - /** * Validates the status code and throws a RetryException if it is not in the range [200, 300). * @@ -40,7 +29,8 @@ public void validateResponse(ThrottlerManager throttlerManager, Logger logger, R * @param result the http response and body * @throws RetryException thrown if status code is {@code >= 300 or < 200} */ - void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { + @Override + protected void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { if (result.isSuccessfulResponse()) { return; } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/ibmwatsonx/IbmWatsonxResponseHandler.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/ibmwatsonx/IbmWatsonxResponseHandler.java index cb686ddb654db..6d1d3fb2a4f91 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/ibmwatsonx/IbmWatsonxResponseHandler.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/ibmwatsonx/IbmWatsonxResponseHandler.java @@ -7,17 +7,14 @@ package org.elasticsearch.xpack.inference.external.ibmwatsonx; -import org.apache.logging.log4j.Logger; import org.elasticsearch.xpack.inference.external.http.HttpResult; import org.elasticsearch.xpack.inference.external.http.retry.BaseResponseHandler; import org.elasticsearch.xpack.inference.external.http.retry.ResponseParser; import org.elasticsearch.xpack.inference.external.http.retry.RetryException; import org.elasticsearch.xpack.inference.external.request.Request; import org.elasticsearch.xpack.inference.external.response.ibmwatsonx.IbmWatsonxErrorResponseEntity; -import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import static org.elasticsearch.core.Strings.format; -import static org.elasticsearch.xpack.inference.external.http.HttpUtils.checkForEmptyBody; public class IbmWatsonxResponseHandler extends BaseResponseHandler { @@ -25,13 +22,6 @@ public IbmWatsonxResponseHandler(String requestType, ResponseParser parseFunctio super(requestType, parseFunction, IbmWatsonxErrorResponseEntity::fromResponse); } - @Override - public void validateResponse(ThrottlerManager throttlerManager, Logger logger, Request request, HttpResult result) - throws RetryException { - checkForFailureStatusCode(request, result); - checkForEmptyBody(throttlerManager, logger, request, result); - } - /** * Validates the status code and throws a RetryException if it is not in the range [200, 300). * @@ -41,7 +31,8 @@ public void validateResponse(ThrottlerManager throttlerManager, Logger logger, R * @param result the http response and body * @throws RetryException thrown if status code is {@code >= 300 or < 200} */ - void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { + @Override + protected void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { if (result.isSuccessfulResponse()) { return; } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/openai/OpenAiResponseHandler.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/openai/OpenAiResponseHandler.java index 6404236d51184..cf867fb1a0ab0 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/openai/OpenAiResponseHandler.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/openai/OpenAiResponseHandler.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.external.openai; -import org.apache.logging.log4j.Logger; import org.elasticsearch.common.Strings; import org.elasticsearch.inference.InferenceServiceResults; import org.elasticsearch.xpack.core.inference.results.StreamingChatCompletionResults; @@ -17,14 +16,12 @@ import org.elasticsearch.xpack.inference.external.http.retry.ResponseParser; import org.elasticsearch.xpack.inference.external.http.retry.RetryException; import org.elasticsearch.xpack.inference.external.request.Request; -import org.elasticsearch.xpack.inference.external.response.openai.OpenAiErrorResponseEntity; +import org.elasticsearch.xpack.inference.external.response.ErrorMessageResponseEntity; import org.elasticsearch.xpack.inference.external.response.streaming.ServerSentEventParser; import org.elasticsearch.xpack.inference.external.response.streaming.ServerSentEventProcessor; -import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import java.util.concurrent.Flow; -import static org.elasticsearch.xpack.inference.external.http.HttpUtils.checkForEmptyBody; import static org.elasticsearch.xpack.inference.external.http.retry.ResponseHandlerUtils.getFirstHeaderOrUnknown; public class OpenAiResponseHandler extends BaseResponseHandler { @@ -47,17 +44,10 @@ public class OpenAiResponseHandler extends BaseResponseHandler { private final boolean canHandleStreamingResponses; public OpenAiResponseHandler(String requestType, ResponseParser parseFunction, boolean canHandleStreamingResponses) { - super(requestType, parseFunction, OpenAiErrorResponseEntity::fromResponse); + super(requestType, parseFunction, ErrorMessageResponseEntity::fromResponse); this.canHandleStreamingResponses = canHandleStreamingResponses; } - @Override - public void validateResponse(ThrottlerManager throttlerManager, Logger logger, Request request, HttpResult result) - throws RetryException { - checkForFailureStatusCode(request, result); - checkForEmptyBody(throttlerManager, logger, request, result); - } - /** * Validates the status code throws an RetryException if not in the range [200, 300). * @@ -66,7 +56,8 @@ public void validateResponse(ThrottlerManager throttlerManager, Logger logger, R * @param result The http response and body * @throws RetryException Throws if status code is {@code >= 300 or < 200 } */ - void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { + @Override + protected void checkForFailureStatusCode(Request request, HttpResult result) throws RetryException { if (result.isSuccessfulResponse()) { return; } @@ -104,7 +95,7 @@ private static boolean isContentTooLarge(HttpResult result) { } if (statusCode == 400) { - var errorEntity = OpenAiErrorResponseEntity.fromResponse(result); + var errorEntity = ErrorMessageResponseEntity.fromResponse(result); return errorEntity != null && errorEntity.getErrorMessage().contains(CONTENT_TOO_LARGE_MESSAGE); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/AzureMistralOpenAiExternalResponseHandler.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/AzureMistralOpenAiExternalResponseHandler.java index 7764a5b6586e2..de627a758c7c3 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/AzureMistralOpenAiExternalResponseHandler.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/AzureMistralOpenAiExternalResponseHandler.java @@ -14,7 +14,7 @@ import org.elasticsearch.xpack.inference.external.http.HttpResult; import org.elasticsearch.xpack.inference.external.http.retry.BaseResponseHandler; import org.elasticsearch.xpack.inference.external.http.retry.ContentTooLargeException; -import org.elasticsearch.xpack.inference.external.http.retry.ErrorMessage; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; import org.elasticsearch.xpack.inference.external.http.retry.ResponseParser; import org.elasticsearch.xpack.inference.external.http.retry.RetryException; import org.elasticsearch.xpack.inference.external.openai.OpenAiStreamingProcessor; @@ -54,7 +54,7 @@ public class AzureMistralOpenAiExternalResponseHandler extends BaseResponseHandl public AzureMistralOpenAiExternalResponseHandler( String requestType, ResponseParser parseFunction, - Function errorParseFunction, + Function errorParseFunction, boolean canHandleStreamingResponses ) { super(requestType, parseFunction, errorParseFunction); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/ErrorMessageResponseEntity.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/ErrorMessageResponseEntity.java index dbf2b37955b22..489844f4d14de 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/ErrorMessageResponseEntity.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/ErrorMessageResponseEntity.java @@ -12,9 +12,10 @@ import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.inference.external.http.HttpResult; -import org.elasticsearch.xpack.inference.external.http.retry.ErrorMessage; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; import java.util.Map; +import java.util.Objects; /** * A pattern is emerging in how external providers provide error responses. @@ -31,27 +32,14 @@ * This currently covers error handling for Azure AI Studio, however this pattern * can be used to simplify and refactor handling for Azure OpenAI and OpenAI responses. */ -public class ErrorMessageResponseEntity implements ErrorMessage { - protected String errorMessage; +public class ErrorMessageResponseEntity extends ErrorResponse { public ErrorMessageResponseEntity(String errorMessage) { - this.errorMessage = errorMessage; + super(errorMessage); } - @Override - public String getErrorMessage() { - return errorMessage; - } - - /** - * Standard error response parser. This can be overridden for those subclasses that - * might have a different format - * - * @param response the HttpResult - * @return the error response - */ @SuppressWarnings("unchecked") - public static ErrorMessage fromResponse(HttpResult response) { + public static ErrorResponse fromResponse(HttpResult response, String defaultMessage) { try ( XContentParser jsonParser = XContentFactory.xContent(XContentType.JSON) .createParser(XContentParserConfiguration.EMPTY, response.body()) @@ -61,14 +49,23 @@ public static ErrorMessage fromResponse(HttpResult response) { var error = (Map) responseMap.get("error"); if (error != null) { var message = (String) error.get("message"); - if (message != null) { - return new ErrorMessageResponseEntity(message); - } + return new ErrorMessageResponseEntity(Objects.requireNonNullElse(message, defaultMessage)); } } catch (Exception e) { // swallow the error } - return null; + return ErrorResponse.UNDEFINED_ERROR; + } + + /** + * Standard error response parser. This can be overridden for those subclasses that + * might have a different format + * + * @param response the HttpResult + * @return the error response + */ + public static ErrorResponse fromResponse(HttpResult response) { + return fromResponse(response, ""); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/alibabacloudsearch/AlibabaCloudSearchErrorResponseEntity.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/alibabacloudsearch/AlibabaCloudSearchErrorResponseEntity.java index 77a0c6ecc7cc8..fe69176db2a1d 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/alibabacloudsearch/AlibabaCloudSearchErrorResponseEntity.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/alibabacloudsearch/AlibabaCloudSearchErrorResponseEntity.java @@ -14,20 +14,13 @@ import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.inference.external.http.HttpResult; -import org.elasticsearch.xpack.inference.external.http.retry.ErrorMessage; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; -public class AlibabaCloudSearchErrorResponseEntity implements ErrorMessage { +public class AlibabaCloudSearchErrorResponseEntity extends ErrorResponse { private static final Logger logger = LogManager.getLogger(AlibabaCloudSearchErrorResponseEntity.class); - private final String errorMessage; - private AlibabaCloudSearchErrorResponseEntity(String errorMessage) { - this.errorMessage = errorMessage; - } - - @Override - public String getErrorMessage() { - return errorMessage; + super(errorMessage); } /** @@ -44,9 +37,9 @@ public String getErrorMessage() { * * @param response The error response * @return An error entity if the response is JSON with the above structure - * or null if the response does not contain the message field + * or {@link ErrorResponse#UNDEFINED_ERROR} if the message field wasn't found */ - public static AlibabaCloudSearchErrorResponseEntity fromResponse(HttpResult response) { + public static ErrorResponse fromResponse(HttpResult response) { try ( XContentParser jsonParser = XContentFactory.xContent(XContentType.JSON) .createParser(XContentParserConfiguration.EMPTY, response.body()) @@ -64,6 +57,6 @@ public static AlibabaCloudSearchErrorResponseEntity fromResponse(HttpResult resp // swallow the error } - return null; + return ErrorResponse.UNDEFINED_ERROR; } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/cohere/CohereErrorResponseEntity.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/cohere/CohereErrorResponseEntity.java index 7d1731105e2f5..28f361b12fa2b 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/cohere/CohereErrorResponseEntity.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/cohere/CohereErrorResponseEntity.java @@ -12,19 +12,12 @@ import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.inference.external.http.HttpResult; -import org.elasticsearch.xpack.inference.external.http.retry.ErrorMessage; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; -public class CohereErrorResponseEntity implements ErrorMessage { - - private final String errorMessage; +public class CohereErrorResponseEntity extends ErrorResponse { private CohereErrorResponseEntity(String errorMessage) { - this.errorMessage = errorMessage; - } - - @Override - public String getErrorMessage() { - return errorMessage; + super(errorMessage); } /** @@ -38,9 +31,9 @@ public String getErrorMessage() { * * @param response The error response * @return An error entity if the response is JSON with the above structure - * or null if the response does not contain the message field + * or {@link ErrorResponse#UNDEFINED_ERROR} if the message field wasn't found */ - public static CohereErrorResponseEntity fromResponse(HttpResult response) { + public static ErrorResponse fromResponse(HttpResult response) { try ( XContentParser jsonParser = XContentFactory.xContent(XContentType.JSON) .createParser(XContentParserConfiguration.EMPTY, response.body()) @@ -54,6 +47,6 @@ public static CohereErrorResponseEntity fromResponse(HttpResult response) { // swallow the error } - return null; + return ErrorResponse.UNDEFINED_ERROR; } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/elastic/ElasticInferenceServiceErrorResponseEntity.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/elastic/ElasticInferenceServiceErrorResponseEntity.java index c860821c81bbf..696be7b2acdd2 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/elastic/ElasticInferenceServiceErrorResponseEntity.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/elastic/ElasticInferenceServiceErrorResponseEntity.java @@ -9,27 +9,19 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.inference.external.http.HttpResult; -import org.elasticsearch.xpack.inference.external.http.retry.ErrorMessage; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; -public class ElasticInferenceServiceErrorResponseEntity implements ErrorMessage { - - private final String errorMessage; +public class ElasticInferenceServiceErrorResponseEntity extends ErrorResponse { private static final Logger logger = LogManager.getLogger(ElasticInferenceServiceErrorResponseEntity.class); private ElasticInferenceServiceErrorResponseEntity(String errorMessage) { - this.errorMessage = errorMessage; - } - - @Override - public String getErrorMessage() { - return errorMessage; + super(errorMessage); } /** @@ -43,9 +35,9 @@ public String getErrorMessage() { * * @param response The error response * @return An error entity if the response is JSON with the above structure - * or null if the response does not contain the error field + * or {@link ErrorResponse#UNDEFINED_ERROR} if the error field wasn't found */ - public static @Nullable ElasticInferenceServiceErrorResponseEntity fromResponse(HttpResult response) { + public static ErrorResponse fromResponse(HttpResult response) { try ( XContentParser jsonParser = XContentFactory.xContent(XContentType.JSON) .createParser(XContentParserConfiguration.EMPTY, response.body()) @@ -59,6 +51,6 @@ public String getErrorMessage() { logger.debug("Failed to parse error response", e); } - return null; + return ErrorResponse.UNDEFINED_ERROR; } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/googleaistudio/GoogleAiStudioErrorResponseEntity.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/googleaistudio/GoogleAiStudioErrorResponseEntity.java index f57f672e10b16..fa8751a2fb99b 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/googleaistudio/GoogleAiStudioErrorResponseEntity.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/googleaistudio/GoogleAiStudioErrorResponseEntity.java @@ -12,21 +12,15 @@ import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.inference.external.http.HttpResult; -import org.elasticsearch.xpack.inference.external.http.retry.ErrorMessage; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; import java.util.Map; +import java.util.Objects; -public class GoogleAiStudioErrorResponseEntity implements ErrorMessage { - - private final String errorMessage; +public class GoogleAiStudioErrorResponseEntity extends ErrorResponse { private GoogleAiStudioErrorResponseEntity(String errorMessage) { - this.errorMessage = errorMessage; - } - - @Override - public String getErrorMessage() { - return errorMessage; + super(errorMessage); } /** @@ -52,11 +46,11 @@ public String getErrorMessage() { * * @param response The error response * @return An error entity if the response is JSON with the above structure - * or null if the response does not contain the `error.message` field + * or {@link ErrorResponse#UNDEFINED_ERROR} if the error field wasn't found */ @SuppressWarnings("unchecked") - public static GoogleAiStudioErrorResponseEntity fromResponse(HttpResult response) { + public static ErrorResponse fromResponse(HttpResult response) { try ( XContentParser jsonParser = XContentFactory.xContent(XContentType.JSON) .createParser(XContentParserConfiguration.EMPTY, response.body()) @@ -65,14 +59,12 @@ public static GoogleAiStudioErrorResponseEntity fromResponse(HttpResult response var error = (Map) responseMap.get("error"); if (error != null) { var message = (String) error.get("message"); - if (message != null) { - return new GoogleAiStudioErrorResponseEntity(message); - } + return new GoogleAiStudioErrorResponseEntity(Objects.requireNonNullElse(message, "")); } } catch (Exception e) { // swallow the error } - return null; + return ErrorResponse.UNDEFINED_ERROR; } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/googlevertexai/GoogleVertexAiErrorResponseEntity.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/googlevertexai/GoogleVertexAiErrorResponseEntity.java index bf14d751db868..99e6f45f7e988 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/googlevertexai/GoogleVertexAiErrorResponseEntity.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/googlevertexai/GoogleVertexAiErrorResponseEntity.java @@ -12,21 +12,15 @@ import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.inference.external.http.HttpResult; -import org.elasticsearch.xpack.inference.external.http.retry.ErrorMessage; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; import java.util.Map; +import java.util.Objects; -public class GoogleVertexAiErrorResponseEntity implements ErrorMessage { - - private final String errorMessage; +public class GoogleVertexAiErrorResponseEntity extends ErrorResponse { private GoogleVertexAiErrorResponseEntity(String errorMessage) { - this.errorMessage = errorMessage; - } - - @Override - public String getErrorMessage() { - return errorMessage; + super(errorMessage); } /** @@ -54,10 +48,10 @@ public String getErrorMessage() { * * @param response The error response * @return An error entity if the response is JSON with the above structure - * or null if the response does not contain the `error.message` field + * or {@link ErrorResponse#UNDEFINED_ERROR} if the error field wasn't found */ @SuppressWarnings("unchecked") - public static GoogleVertexAiErrorResponseEntity fromResponse(HttpResult response) { + public static ErrorResponse fromResponse(HttpResult response) { try ( XContentParser jsonParser = XContentFactory.xContent(XContentType.JSON) .createParser(XContentParserConfiguration.EMPTY, response.body()) @@ -66,14 +60,12 @@ public static GoogleVertexAiErrorResponseEntity fromResponse(HttpResult response var error = (Map) responseMap.get("error"); if (error != null) { var message = (String) error.get("message"); - if (message != null) { - return new GoogleVertexAiErrorResponseEntity(message); - } + return new GoogleVertexAiErrorResponseEntity(Objects.requireNonNullElse(message, "")); } } catch (Exception e) { // swallow the error } - return null; + return ErrorResponse.UNDEFINED_ERROR; } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/huggingface/HuggingFaceErrorResponseEntity.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/huggingface/HuggingFaceErrorResponseEntity.java index faeb7c6ac4fa9..af9057c334b12 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/huggingface/HuggingFaceErrorResponseEntity.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/huggingface/HuggingFaceErrorResponseEntity.java @@ -12,9 +12,14 @@ import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.inference.external.http.HttpResult; -import org.elasticsearch.xpack.inference.external.http.retry.ErrorMessage; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; + +public class HuggingFaceErrorResponseEntity extends ErrorResponse { + + public HuggingFaceErrorResponseEntity(String message) { + super(message); + } -public record HuggingFaceErrorResponseEntity(String message) implements ErrorMessage { /** * An example error response for invalid auth would look like * @@ -26,9 +31,9 @@ public record HuggingFaceErrorResponseEntity(String message) implements ErrorMes * * @param response The error response * @return An error entity if the response is JSON with the above structure - * or null if the response does not contain the error field + * or {@link ErrorResponse#UNDEFINED_ERROR} if the error field wasn't found */ - public static HuggingFaceErrorResponseEntity fromResponse(HttpResult response) { + public static ErrorResponse fromResponse(HttpResult response) { try ( XContentParser jsonParser = XContentFactory.xContent(XContentType.JSON) .createParser(XContentParserConfiguration.EMPTY, response.body()) @@ -42,11 +47,6 @@ public static HuggingFaceErrorResponseEntity fromResponse(HttpResult response) { // swallow the error } - return null; - } - - @Override - public String getErrorMessage() { - return message; + return ErrorResponse.UNDEFINED_ERROR; } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/ibmwatsonx/IbmWatsonxErrorResponseEntity.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/ibmwatsonx/IbmWatsonxErrorResponseEntity.java index 582e3d009be50..9c805b22ff018 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/ibmwatsonx/IbmWatsonxErrorResponseEntity.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/ibmwatsonx/IbmWatsonxErrorResponseEntity.java @@ -12,25 +12,19 @@ import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.inference.external.http.HttpResult; -import org.elasticsearch.xpack.inference.external.http.retry.ErrorMessage; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; import java.util.Map; +import java.util.Objects; -public class IbmWatsonxErrorResponseEntity implements ErrorMessage { - - private final String errorMessage; +public class IbmWatsonxErrorResponseEntity extends ErrorResponse { private IbmWatsonxErrorResponseEntity(String errorMessage) { - this.errorMessage = errorMessage; - } - - @Override - public String getErrorMessage() { - return errorMessage; + super(errorMessage); } @SuppressWarnings("unchecked") - public static IbmWatsonxErrorResponseEntity fromResponse(HttpResult response) { + public static ErrorResponse fromResponse(HttpResult response) { try ( XContentParser jsonParser = XContentFactory.xContent(XContentType.JSON) .createParser(XContentParserConfiguration.EMPTY, response.body()) @@ -39,14 +33,12 @@ public static IbmWatsonxErrorResponseEntity fromResponse(HttpResult response) { var error = (Map) responseMap.get("error"); if (error != null) { var message = (String) error.get("message"); - if (message != null) { - return new IbmWatsonxErrorResponseEntity(message); - } + return new IbmWatsonxErrorResponseEntity(Objects.requireNonNullElse(message, "")); } } catch (Exception e) { // swallow the error } - return null; + return ErrorResponse.UNDEFINED_ERROR; } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/openai/OpenAiErrorResponseEntity.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/openai/OpenAiErrorResponseEntity.java deleted file mode 100644 index a364be29ada33..0000000000000 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/response/openai/OpenAiErrorResponseEntity.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.inference.external.response.openai; - -import org.elasticsearch.xcontent.XContentFactory; -import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xcontent.XContentParserConfiguration; -import org.elasticsearch.xcontent.XContentType; -import org.elasticsearch.xpack.inference.external.http.HttpResult; -import org.elasticsearch.xpack.inference.external.http.retry.ErrorMessage; - -import java.util.Map; - -public class OpenAiErrorResponseEntity implements ErrorMessage { - - private final String errorMessage; - - private OpenAiErrorResponseEntity(String errorMessage) { - this.errorMessage = errorMessage; - } - - public String getErrorMessage() { - return errorMessage; - } - - /** - * An example error response for invalid auth would look like - * - * { - * "error": { - * "message": "You didn't provide an API key...", - * "type": "invalid_request_error", - * "param": null, - * "code": null - * } - * } - * - * - * - * @param response The error response - * @return An error entity if the response is JSON with the above structure - * or null if the response does not contain the error.message field - */ - @SuppressWarnings("unchecked") - public static OpenAiErrorResponseEntity fromResponse(HttpResult response) { - try ( - XContentParser jsonParser = XContentFactory.xContent(XContentType.JSON) - .createParser(XContentParserConfiguration.EMPTY, response.body()) - ) { - var responseMap = jsonParser.map(); - var error = (Map) responseMap.get("error"); - if (error != null) { - var message = (String) error.get("message"); - if (message != null) { - return new OpenAiErrorResponseEntity(message); - } - } - } catch (Exception e) { - // swallow the error - } - - return null; - } -} diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/http/retry/BaseResponseHandlerTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/http/retry/BaseResponseHandlerTests.java index b7095979b0fa5..444a187261fff 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/http/retry/BaseResponseHandlerTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/http/retry/BaseResponseHandlerTests.java @@ -7,11 +7,22 @@ package org.elasticsearch.xpack.inference.external.http.retry; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.logging.log4j.Logger; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.inference.external.http.HttpResult; +import org.elasticsearch.xpack.inference.external.request.Request; +import org.elasticsearch.xpack.inference.external.response.ErrorMessageResponseEntity; +import org.elasticsearch.xpack.inference.logging.ThrottlerManager; + +import java.nio.charset.StandardCharsets; import static org.elasticsearch.xpack.inference.external.http.retry.BaseResponseHandler.toRestStatus; import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class BaseResponseHandlerTests extends ESTestCase { public void testToRestStatus_ReturnsBadRequest_WhenStatusIs500() { @@ -29,4 +40,111 @@ public void testToRestStatus_ReturnsStatusCodeValue_WhenStatusIs200() { public void testToRestStatus_ReturnsBadRequest_WhenStatusIsUnknown() { assertThat(toRestStatus(1000), is(RestStatus.BAD_REQUEST)); } + + public void testValidateResponse_DoesNotThrowAnExceptionWhenStatus200_AndNoErrorObject() { + var handler = getBaseResponseHandler(); + + String responseJson = """ + { + "field": "hello" + } + """; + + var response = mock200Response(); + + var request = mock(Request.class); + when(request.getInferenceEntityId()).thenReturn("abc"); + + handler.validateResponse( + mock(ThrottlerManager.class), + mock(Logger.class), + request, + new HttpResult(response, responseJson.getBytes(StandardCharsets.UTF_8)) + ); + } + + public void testValidateResponse_ThrowsErrorWhenMalformedErrorObjectExists() { + var handler = getBaseResponseHandler(); + + String responseJson = """ + { + "error": { + "type": "not_found_error" + } + } + """; + + var response = mock200Response(); + + var request = mock(Request.class); + when(request.getInferenceEntityId()).thenReturn("abc"); + + var exception = expectThrows( + RetryException.class, + () -> handler.validateResponse( + mock(ThrottlerManager.class), + mock(Logger.class), + request, + new HttpResult(response, responseJson.getBytes(StandardCharsets.UTF_8)) + ) + ); + + assertFalse(exception.shouldRetry()); + assertThat( + exception.getCause().getMessage(), + is("Received an error response for request from inference entity id [abc] status [200]") + ); + } + + public void testValidateResponse_ThrowsErrorWhenWellFormedErrorObjectExists() { + var handler = getBaseResponseHandler(); + + String responseJson = """ + { + "error": { + "type": "not_found_error", + "message": "a message" + } + } + """; + + var response = mock200Response(); + + var request = mock(Request.class); + when(request.getInferenceEntityId()).thenReturn("abc"); + + var exception = expectThrows( + RetryException.class, + () -> handler.validateResponse( + mock(ThrottlerManager.class), + mock(Logger.class), + request, + new HttpResult(response, responseJson.getBytes(StandardCharsets.UTF_8)) + ) + ); + + assertFalse(exception.shouldRetry()); + assertThat( + exception.getCause().getMessage(), + is("Received an error response for request from inference entity id [abc] status [200]. Error message: [a message]") + ); + } + + private static HttpResponse mock200Response() { + int statusCode = 200; + var statusLine = mock(StatusLine.class); + when(statusLine.getStatusCode()).thenReturn(statusCode); + + var response = mock(HttpResponse.class); + when(response.getStatusLine()).thenReturn(statusLine); + + return response; + } + + private static BaseResponseHandler getBaseResponseHandler() { + return new BaseResponseHandler("abc", (Request request, HttpResult result) -> null, ErrorMessageResponseEntity::fromResponse) { + @Override + protected void checkForFailureStatusCode(Request request, HttpResult result) {} + }; + } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/ErrorMessageResponseEntityTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/ErrorMessageResponseEntityTests.java index d57d1537f6c30..9ad9b9f3ca0a5 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/ErrorMessageResponseEntityTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/ErrorMessageResponseEntityTests.java @@ -11,10 +11,12 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.inference.external.http.HttpResult; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; import java.nio.charset.StandardCharsets; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Mockito.mock; public class ErrorMessageResponseEntityTests extends ESTestCase { @@ -33,11 +35,29 @@ public void testErrorResponse_ExtractsError() { assertThat(error.getErrorMessage(), is("test_error_message")); } + public void testFromResponse_WithOtherFieldsPresent() { + String responseJson = """ + { + "error": { + "message": "You didn't provide an API key", + "type": "invalid_request_error", + "param": null, + "code": null + } + } + """; + + ErrorResponse errorMessage = ErrorMessageResponseEntity.fromResponse( + new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) + ); + assertEquals("You didn't provide an API key", errorMessage.getErrorMessage()); + } + public void testFromResponse_noMessage() { String responseJson = """ { "error": { - "type": "not_found_error", + "type": "not_found_error" } } """; @@ -45,21 +65,22 @@ public void testFromResponse_noMessage() { var errorMessage = ErrorMessageResponseEntity.fromResponse( new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) ); - assertNull(errorMessage); + assertThat(errorMessage.getErrorMessage(), is("")); + assertTrue(errorMessage.errorStructureFound()); } - public void testErrorResponse_ReturnsNullIfNoError() { + public void testErrorResponse_ReturnsUndefinedObjectIfNoError() { var result = getMockResult(""" {"noerror":true}"""); var error = ErrorMessageResponseEntity.fromResponse(result); - assertNull(error); + assertThat(error, sameInstance(ErrorResponse.UNDEFINED_ERROR)); } - public void testErrorResponse_ReturnsNullIfNotJson() { + public void testErrorResponse_ReturnsUndefinedObjectIfNotJson() { var result = getMockResult("not a json string"); var error = ErrorMessageResponseEntity.fromResponse(result); - assertNull(error); + assertThat(error, sameInstance(ErrorResponse.UNDEFINED_ERROR)); } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/alibabacloudsearch/AlibabaCloudSearchErrorResponseEntityTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/alibabacloudsearch/AlibabaCloudSearchErrorResponseEntityTests.java index a03349c66b6d5..1dd570537f12e 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/alibabacloudsearch/AlibabaCloudSearchErrorResponseEntityTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/alibabacloudsearch/AlibabaCloudSearchErrorResponseEntityTests.java @@ -26,7 +26,7 @@ public void testFromResponse() { } """; - AlibabaCloudSearchErrorResponseEntity errorMessage = AlibabaCloudSearchErrorResponseEntity.fromResponse( + var errorMessage = AlibabaCloudSearchErrorResponseEntity.fromResponse( new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) ); assertNotNull(errorMessage); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/cohere/CohereErrorResponseEntityTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/cohere/CohereErrorResponseEntityTests.java index a2b1c26b2b3d5..d770adb0b5a65 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/cohere/CohereErrorResponseEntityTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/cohere/CohereErrorResponseEntityTests.java @@ -10,7 +10,9 @@ import org.apache.http.HttpResponse; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.inference.external.http.HttpResult; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; import java.nio.charset.StandardCharsets; @@ -25,7 +27,7 @@ public void testFromResponse() { } """; - CohereErrorResponseEntity errorMessage = CohereErrorResponseEntity.fromResponse( + var errorMessage = CohereErrorResponseEntity.fromResponse( new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) ); assertNotNull(errorMessage); @@ -42,9 +44,9 @@ public void testFromResponse_noMessage() { } """; - CohereErrorResponseEntity errorMessage = CohereErrorResponseEntity.fromResponse( + var errorMessage = CohereErrorResponseEntity.fromResponse( new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) ); - assertNull(errorMessage); + assertThat(errorMessage, Matchers.sameInstance(ErrorResponse.UNDEFINED_ERROR)); } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/elastic/ElasticInferenceServiceErrorResponseEntityTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/elastic/ElasticInferenceServiceErrorResponseEntityTests.java index 4da0518084828..d99e1c1ce5afc 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/elastic/ElasticInferenceServiceErrorResponseEntityTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/elastic/ElasticInferenceServiceErrorResponseEntityTests.java @@ -10,6 +10,8 @@ import org.apache.http.HttpResponse; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.inference.external.http.HttpResult; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; +import org.hamcrest.Matchers; import java.nio.charset.StandardCharsets; @@ -25,7 +27,7 @@ public void testFromResponse() { } """; - ElasticInferenceServiceErrorResponseEntity errorResponseEntity = ElasticInferenceServiceErrorResponseEntity.fromResponse( + var errorResponseEntity = ElasticInferenceServiceErrorResponseEntity.fromResponse( new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) ); @@ -40,11 +42,11 @@ public void testFromResponse_NoErrorMessagePresent() { } """; - ElasticInferenceServiceErrorResponseEntity errorResponseEntity = ElasticInferenceServiceErrorResponseEntity.fromResponse( + var errorResponseEntity = ElasticInferenceServiceErrorResponseEntity.fromResponse( new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) ); - assertNull(errorResponseEntity); + assertThat(errorResponseEntity, Matchers.sameInstance(ErrorResponse.UNDEFINED_ERROR)); } public void testFromResponse_InvalidJson() { @@ -52,10 +54,10 @@ public void testFromResponse_InvalidJson() { { """; - ElasticInferenceServiceErrorResponseEntity errorResponseEntity = ElasticInferenceServiceErrorResponseEntity.fromResponse( + var errorResponseEntity = ElasticInferenceServiceErrorResponseEntity.fromResponse( new HttpResult(mock(HttpResponse.class), invalidResponseJson.getBytes(StandardCharsets.UTF_8)) ); - assertNull(errorResponseEntity); + assertThat(errorResponseEntity, Matchers.sameInstance(ErrorResponse.UNDEFINED_ERROR)); } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/googleaistudio/GoogleAiStudioErrorResponseEntityTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/googleaistudio/GoogleAiStudioErrorResponseEntityTests.java index 61448f2e35bdf..e2ccbb4167ef6 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/googleaistudio/GoogleAiStudioErrorResponseEntityTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/googleaistudio/GoogleAiStudioErrorResponseEntityTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.xpack.inference.external.http.HttpResult; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Mockito.mock; public class GoogleAiStudioErrorResponseEntityTests extends ESTestCase { @@ -48,7 +49,7 @@ public void testErrorResponse_ExtractsError() { assertThat(error.getErrorMessage(), is("error message")); } - public void testErrorResponse_ReturnsNullIfNoError() { + public void testErrorResponse_ReturnsUndefinedObjectIfNoError() { var result = getMockResult(""" { "foo": "bar" @@ -56,13 +57,13 @@ public void testErrorResponse_ReturnsNullIfNoError() { """); var error = GoogleAiStudioErrorResponseEntity.fromResponse(result); - assertNull(error); + assertThat(error, sameInstance(GoogleAiStudioErrorResponseEntity.UNDEFINED_ERROR)); } - public void testErrorResponse_ReturnsNullIfNotJson() { + public void testErrorResponse_ReturnsUndefinedIfNotJson() { var result = getMockResult("error message"); var error = GoogleAiStudioErrorResponseEntity.fromResponse(result); - assertNull(error); + assertThat(error, sameInstance(GoogleAiStudioErrorResponseEntity.UNDEFINED_ERROR)); } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/googlevertexai/GoogleVertexAiErrorResponseEntityTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/googlevertexai/GoogleVertexAiErrorResponseEntityTests.java index e2c9ebed2c164..23bb2f9829e62 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/googlevertexai/GoogleVertexAiErrorResponseEntityTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/googlevertexai/GoogleVertexAiErrorResponseEntityTests.java @@ -11,8 +11,10 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.inference.external.http.HttpResult; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Mockito.mock; public class GoogleVertexAiErrorResponseEntityTests extends ESTestCase { @@ -48,7 +50,7 @@ public void testErrorResponse_ExtractsError() { assertThat(error.getErrorMessage(), is("error message")); } - public void testErrorResponse_ReturnsNullIfNoError() { + public void testErrorResponse_ReturnsUndefinedObjectIfNoError() { var result = getMockResult(""" { "foo": "bar" @@ -56,14 +58,14 @@ public void testErrorResponse_ReturnsNullIfNoError() { """); var error = GoogleVertexAiErrorResponseEntity.fromResponse(result); - assertNull(error); + assertThat(error, sameInstance(ErrorResponse.UNDEFINED_ERROR)); } - public void testErrorResponse_ReturnsNullIfNotJson() { + public void testErrorResponse_ReturnsUndefinedObjectIfNotJson() { var result = getMockResult("error message"); var error = GoogleVertexAiErrorResponseEntity.fromResponse(result); - assertNull(error); + assertThat(error, sameInstance(ErrorResponse.UNDEFINED_ERROR)); } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/huggingface/HuggingFaceErrorResponseEntityTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/huggingface/HuggingFaceErrorResponseEntityTests.java index ed381de844731..06b15d57f5a89 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/huggingface/HuggingFaceErrorResponseEntityTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/huggingface/HuggingFaceErrorResponseEntityTests.java @@ -10,6 +10,8 @@ import org.apache.http.HttpResponse; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.inference.external.http.HttpResult; +import org.elasticsearch.xpack.inference.external.http.retry.ErrorResponse; +import org.hamcrest.Matchers; import java.nio.charset.StandardCharsets; @@ -23,7 +25,7 @@ public void testFromResponse() { } """; - HuggingFaceErrorResponseEntity errorMessage = HuggingFaceErrorResponseEntity.fromResponse( + var errorMessage = HuggingFaceErrorResponseEntity.fromResponse( new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) ); assertNotNull(errorMessage); @@ -39,10 +41,10 @@ public void testFromResponse_noMessage() { } """; - HuggingFaceErrorResponseEntity errorMessage = HuggingFaceErrorResponseEntity.fromResponse( + var errorMessage = HuggingFaceErrorResponseEntity.fromResponse( new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) ); - assertNull(errorMessage); + assertThat(errorMessage, Matchers.sameInstance(ErrorResponse.UNDEFINED_ERROR)); } public void testFromResponse_noError() { @@ -54,9 +56,9 @@ public void testFromResponse_noError() { } """; - HuggingFaceErrorResponseEntity errorMessage = HuggingFaceErrorResponseEntity.fromResponse( + var errorMessage = HuggingFaceErrorResponseEntity.fromResponse( new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) ); - assertNull(errorMessage); + assertThat(errorMessage, Matchers.sameInstance(ErrorResponse.UNDEFINED_ERROR)); } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/openai/OpenAiErrorResponseEntityTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/openai/OpenAiErrorResponseEntityTests.java deleted file mode 100644 index 4dc6c4190f92c..0000000000000 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/response/openai/OpenAiErrorResponseEntityTests.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.inference.external.response.openai; - -import org.apache.http.HttpResponse; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.inference.external.http.HttpResult; - -import java.nio.charset.StandardCharsets; - -import static org.mockito.Mockito.mock; - -public class OpenAiErrorResponseEntityTests extends ESTestCase { - public void testFromResponse() { - String responseJson = """ - { - "error": { - "message": "You didn't provide an API key", - "type": "invalid_request_error", - "param": null, - "code": null - } - } - """; - - OpenAiErrorResponseEntity errorMessage = OpenAiErrorResponseEntity.fromResponse( - new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) - ); - assertEquals("You didn't provide an API key", errorMessage.getErrorMessage()); - } - - public void testFromResponse_noMessage() { - String responseJson = """ - { - "error": { - "type": "invalid_request_error" - } - } - """; - - OpenAiErrorResponseEntity errorMessage = OpenAiErrorResponseEntity.fromResponse( - new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) - ); - assertNull(errorMessage); - } - - public void testFromResponse_noError() { - String responseJson = """ - { - "something": { - "not": "relevant" - } - } - """; - - OpenAiErrorResponseEntity errorMessage = OpenAiErrorResponseEntity.fromResponse( - new HttpResult(mock(HttpResponse.class), responseJson.getBytes(StandardCharsets.UTF_8)) - ); - assertNull(errorMessage); - } -} From b85e6491a91d11548b39540e87a3dcf536186d60 Mon Sep 17 00:00:00 2001 From: Patrick Doyle <810052+prdoyle@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:29:54 -0500 Subject: [PATCH 27/77] Relax assertion - remove unnecessary times(1) (#118497) --- muted-tests.yml | 3 --- .../service/FileSettingsServiceTests.java | 11 +++++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index 613d3a3655ccf..50ad8c27675b4 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -314,9 +314,6 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/118220 - class: org.elasticsearch.xpack.esql.action.EsqlActionBreakerIT issue: https://github.com/elastic/elasticsearch/issues/118238 -- class: org.elasticsearch.reservedstate.service.FileSettingsServiceTests - method: testInvalidJSON - issue: https://github.com/elastic/elasticsearch/issues/116521 # Examples: # diff --git a/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java b/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java index c19cf7c31bc68..b1cf40d7f22ec 100644 --- a/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/reservedstate/service/FileSettingsServiceTests.java @@ -43,6 +43,7 @@ import org.elasticsearch.xcontent.XContentParser; import org.junit.After; import org.junit.Before; +import org.mockito.Mockito; import org.mockito.stubbing.Answer; import java.io.IOException; @@ -315,7 +316,13 @@ public void testInvalidJSON() throws Exception { writeTestFile(fileSettingsService.watchedFile(), "test_invalid_JSON"); awaitOrBust(fileChangeBarrier); - verify(fileSettingsService, times(1)).onProcessFileChangesException( + // These checks use atLeast(1) because the initial JSON is also invalid, + // and so we sometimes get two calls to these error-reporting methods + // depending on timing. Rather than trace down the root cause and fix + // it, we tolerate this for now because, hey, invalid JSON is invalid JSON + // and this is still testing what we want to test. + + verify(fileSettingsService, Mockito.atLeast(1)).onProcessFileChangesException( argThat(e -> unwrapException(e) instanceof XContentParseException) ); @@ -324,7 +331,7 @@ public void testInvalidJSON() throws Exception { // of the watcher thread itself, which occurs asynchronously when clusterChanged is first called. assertEquals(YELLOW, healthIndicatorService.calculate(false, null).status()); - verify(healthIndicatorService).failureOccurred(contains(XContentParseException.class.getName())); + verify(healthIndicatorService, Mockito.atLeast(1)).failureOccurred(contains(XContentParseException.class.getName())); } /** From c6b42bb5026d1641703a3eb7f34fae331f663785 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:46:39 +1100 Subject: [PATCH 28/77] Mute org.elasticsearch.packaging.test.DockerTests test011SecurityEnabledStatus #118517 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index 50ad8c27675b4..573bd035b872b 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -314,6 +314,9 @@ tests: issue: https://github.com/elastic/elasticsearch/issues/118220 - class: org.elasticsearch.xpack.esql.action.EsqlActionBreakerIT issue: https://github.com/elastic/elasticsearch/issues/118238 +- class: org.elasticsearch.packaging.test.DockerTests + method: test011SecurityEnabledStatus + issue: https://github.com/elastic/elasticsearch/issues/118517 # Examples: # From a5607829958f15d92eddbfab59c2309ca0b0f5ea Mon Sep 17 00:00:00 2001 From: John Verwolf Date: Wed, 11 Dec 2024 17:43:17 -0800 Subject: [PATCH 29/77] Remove any references to V_7 (#118103) This PR removes any references to org.elasticsearch.core.RestApiVersion#V_7. --- docs/changelog/118103.yaml | 11 +++++++++++ .../org/elasticsearch/core/RestApiVersion.java | 10 +--------- .../action/ingest/SimulatePipelineRequest.java | 2 +- .../common/xcontent/ChunkedToXContent.java | 1 - .../org/elasticsearch/ingest/IngestDocument.java | 3 +++ .../action/bulk/BulkRequestParserTests.java | 2 +- .../SimulatePipelineRequestParsingTests.java | 2 +- .../java/org/elasticsearch/license/License.java | 3 +-- .../xpack/core/ml/MachineLearningField.java | 3 --- .../xpack/core/ml/action/CloseJobAction.java | 13 +------------ .../core/ml/action/GetDatafeedsStatsAction.java | 2 ++ .../core/ml/action/GetOverallBucketsAction.java | 13 +------------ .../xpack/core/ml/action/StopDatafeedAction.java | 13 +------------ .../xpack/ml/rest/cat/RestCatDatafeedsAction.java | 14 +------------- .../xpack/ml/rest/cat/RestCatJobsAction.java | 14 +------------- .../rest/datafeeds/RestGetDatafeedStatsAction.java | 14 +------------- .../ml/rest/datafeeds/RestGetDatafeedsAction.java | 14 +------------- .../ml/rest/datafeeds/RestStopDatafeedAction.java | 14 +------------- .../xpack/ml/rest/job/RestCloseJobAction.java | 14 +------------- .../xpack/ml/rest/job/RestGetJobStatsAction.java | 14 +------------- .../xpack/ml/rest/job/RestGetJobsAction.java | 14 +------------- .../rest/results/RestGetOverallBucketsAction.java | 14 +------------- 22 files changed, 33 insertions(+), 171 deletions(-) create mode 100644 docs/changelog/118103.yaml diff --git a/docs/changelog/118103.yaml b/docs/changelog/118103.yaml new file mode 100644 index 0000000000000..c0bb8f56d6931 --- /dev/null +++ b/docs/changelog/118103.yaml @@ -0,0 +1,11 @@ +pr: 118103 +summary: "Remove any references to org.elasticsearch.core.RestApiVersion#V_7" +area: Infra/Core +type: breaking +issues: [] +breaking: + title: "Remove any references to org.elasticsearch.core.RestApiVersion#V_7" + area: REST API + details: "This PR removes all references to V_7 in the Rest API. V7 features marked for deprecation have been removed." + impact: "This change is breaking for any external plugins/clients that rely on the V_7 enum or deprecated version 7 functionality" + notable: false diff --git a/libs/core/src/main/java/org/elasticsearch/core/RestApiVersion.java b/libs/core/src/main/java/org/elasticsearch/core/RestApiVersion.java index 672090d195c5a..085887fdcdb0c 100644 --- a/libs/core/src/main/java/org/elasticsearch/core/RestApiVersion.java +++ b/libs/core/src/main/java/org/elasticsearch/core/RestApiVersion.java @@ -20,10 +20,7 @@ public enum RestApiVersion { V_9(9), - V_8(8), - - @UpdateForV9(owner = UpdateForV9.Owner.CORE_INFRA) // remove all references to V_7 then delete this annotation - V_7(7); + V_8(8); public final byte major; @@ -54,7 +51,6 @@ public static Predicate equalTo(RestApiVersion restApiVersion) { return switch (restApiVersion) { case V_9 -> r -> r.major == V_9.major; case V_8 -> r -> r.major == V_8.major; - case V_7 -> r -> r.major == V_7.major; }; } @@ -62,15 +58,11 @@ public static Predicate onOrAfter(RestApiVersion restApiVersion) return switch (restApiVersion) { case V_9 -> r -> r.major >= V_9.major; case V_8 -> r -> r.major >= V_8.major; - case V_7 -> r -> r.major >= V_7.major; }; } public static RestApiVersion forMajor(int major) { switch (major) { - case 7 -> { - return V_7; - } case 8 -> { return V_8; } diff --git a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java index d6a2d81fdb7d3..02027b1f633d2 100644 --- a/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java +++ b/server/src/main/java/org/elasticsearch/action/ingest/SimulatePipelineRequest.java @@ -177,7 +177,7 @@ private static List parseDocs(Map config, RestAp String index = ConfigurationUtils.readStringOrIntProperty(null, null, dataMap, Metadata.INDEX.getFieldName(), "_index"); String id = ConfigurationUtils.readStringOrIntProperty(null, null, dataMap, Metadata.ID.getFieldName(), "_id"); String routing = ConfigurationUtils.readOptionalStringOrIntProperty(null, null, dataMap, Metadata.ROUTING.getFieldName()); - if (restApiVersion != RestApiVersion.V_8 && dataMap.containsKey(Metadata.TYPE.getFieldName())) { + if (dataMap.containsKey(Metadata.TYPE.getFieldName())) { deprecationLogger.compatibleCritical( "simulate_pipeline_with_types", "[types removal] specifying _type in pipeline simulation requests is deprecated" diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContent.java b/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContent.java index db0b5b4357c7d..1970983654b88 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContent.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/ChunkedToXContent.java @@ -49,7 +49,6 @@ static ChunkedToXContentBuilder builder(ToXContent.Params params) { */ default Iterator toXContentChunked(RestApiVersion restApiVersion, ToXContent.Params params) { return switch (restApiVersion) { - case V_7 -> throw new AssertionError("v7 API not supported"); case V_8 -> toXContentChunkedV8(params); case V_9 -> toXContentChunked(params); }; diff --git a/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java b/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java index 280c7684a8553..0614e9e92edf2 100644 --- a/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java +++ b/server/src/main/java/org/elasticsearch/ingest/IngestDocument.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.core.UpdateForV10; import org.elasticsearch.index.VersionType; import org.elasticsearch.index.mapper.IdFieldMapper; import org.elasticsearch.index.mapper.IndexFieldMapper; @@ -957,6 +958,8 @@ void resetTerminate() { terminate = false; } + // Unconditionally deprecate the _type field once V7 BWC support is removed + @UpdateForV10(owner = UpdateForV10.Owner.DATA_MANAGEMENT) public enum Metadata { INDEX(IndexFieldMapper.NAME), TYPE("_type"), diff --git a/server/src/test/java/org/elasticsearch/action/bulk/BulkRequestParserTests.java b/server/src/test/java/org/elasticsearch/action/bulk/BulkRequestParserTests.java index 5785d076693e7..9d944d43f4c36 100644 --- a/server/src/test/java/org/elasticsearch/action/bulk/BulkRequestParserTests.java +++ b/server/src/test/java/org/elasticsearch/action/bulk/BulkRequestParserTests.java @@ -30,7 +30,7 @@ public class BulkRequestParserTests extends ESTestCase { @UpdateForV10(owner = UpdateForV10.Owner.DATA_MANAGEMENT) // Replace with just RestApiVersion.values() when V8 no longer exists public static final List REST_API_VERSIONS_POST_V8 = Stream.of(RestApiVersion.values()) - .filter(v -> v.compareTo(RestApiVersion.V_8) > 0) + .filter(v -> v.matches(RestApiVersion.onOrAfter(RestApiVersion.V_9))) .toList(); public void testParserCannotBeReusedAfterFailure() { diff --git a/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestParsingTests.java b/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestParsingTests.java index b8be05fbfb72d..391c258b6f098 100644 --- a/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestParsingTests.java +++ b/server/src/test/java/org/elasticsearch/action/ingest/SimulatePipelineRequestParsingTests.java @@ -342,7 +342,7 @@ public void testIngestPipelineWithDocumentsWithType() throws Exception { requestContent, false, ingestService, - RestApiVersion.V_7 + RestApiVersion.V_8 ); assertThat(actualRequest.verbose(), equalTo(false)); assertThat(actualRequest.documents().size(), equalTo(numDocs)); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java index f280dcf9b3edf..19790e61b6102 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java @@ -15,7 +15,6 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.XContentHelper; -import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.features.NodeFeature; import org.elasticsearch.xcontent.ToXContentObject; @@ -142,7 +141,7 @@ static boolean isEnterprise(String typeName) { */ public static final String LICENSE_VERSION_MODE = "license_version"; /** - * Set for {@link RestApiVersion#V_7} requests only + * Set for RestApiVersion#V_7 requests only * XContent param name to map the "enterprise" license type to "platinum" * for backwards compatibility with older clients */ diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MachineLearningField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MachineLearningField.java index 6c49cadb8d189..3a37f94e6b2d4 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MachineLearningField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MachineLearningField.java @@ -22,9 +22,6 @@ public final class MachineLearningField { - public static final String DEPRECATED_ALLOW_NO_JOBS_PARAM = "allow_no_jobs"; - public static final String DEPRECATED_ALLOW_NO_DATAFEEDS_PARAM = "allow_no_datafeeds"; - public static final Setting AUTODETECT_PROCESS = Setting.boolSetting( "xpack.ml.autodetect_process", true, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CloseJobAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CloseJobAction.java index bddae0417e467..5963cff9746b6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CloseJobAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/CloseJobAction.java @@ -12,9 +12,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.tasks.Task; import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; @@ -27,10 +25,6 @@ import java.io.IOException; import java.util.Objects; -import static org.elasticsearch.core.RestApiVersion.equalTo; -import static org.elasticsearch.core.RestApiVersion.onOrAfter; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.DEPRECATED_ALLOW_NO_JOBS_PARAM; - public class CloseJobAction extends ActionType { public static final CloseJobAction INSTANCE = new CloseJobAction(); @@ -45,11 +39,7 @@ public static class Request extends BaseTasksRequest implements ToXCont public static final ParseField TIMEOUT = new ParseField("timeout"); public static final ParseField FORCE = new ParseField("force"); - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate forRestApiVersion - public static final ParseField ALLOW_NO_MATCH = new ParseField("allow_no_match").forRestApiVersion(onOrAfter(RestApiVersion.V_8)); - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate ref to RestApiVersion.V_7 - public static final ParseField ALLOW_NO_MATCH_V7 = new ParseField("allow_no_match", DEPRECATED_ALLOW_NO_JOBS_PARAM) - .forRestApiVersion(equalTo(RestApiVersion.V_7)); + public static final ParseField ALLOW_NO_MATCH = new ParseField("allow_no_match"); public static final ObjectParser PARSER = new ObjectParser<>(NAME, Request::new); static { @@ -60,7 +50,6 @@ public static class Request extends BaseTasksRequest implements ToXCont ); PARSER.declareBoolean(Request::setForce, FORCE); PARSER.declareBoolean(Request::setAllowNoMatch, ALLOW_NO_MATCH); - PARSER.declareBoolean(Request::setAllowNoMatch, ALLOW_NO_MATCH_V7); } public static Request parseRequest(String jobId, XContentParser parser) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetDatafeedsStatsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetDatafeedsStatsAction.java index fafb9afa99f85..eabd6ef5939e3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetDatafeedsStatsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetDatafeedsStatsAction.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.persistent.PersistentTasksCustomMetadata; import org.elasticsearch.tasks.CancellableTask; import org.elasticsearch.tasks.Task; @@ -62,6 +63,7 @@ private GetDatafeedsStatsAction() { // serialized to older nodes where the transport action was a MasterNodeReadAction. // TODO: Make this a simple request in a future version where there is no possibility // of this request being serialized to another node. + @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) public static class Request extends MasterNodeReadRequest { public static final String ALLOW_NO_MATCH = "allow_no_match"; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetOverallBucketsAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetOverallBucketsAction.java index 47bc6df5f6536..f9e91194fe31b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetOverallBucketsAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetOverallBucketsAction.java @@ -13,9 +13,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.time.DateMathParser; -import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; @@ -33,10 +31,6 @@ import java.util.Objects; import java.util.function.LongSupplier; -import static org.elasticsearch.core.RestApiVersion.equalTo; -import static org.elasticsearch.core.RestApiVersion.onOrAfter; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.DEPRECATED_ALLOW_NO_JOBS_PARAM; - /** *

* This action returns summarized bucket results over multiple jobs. @@ -68,11 +62,7 @@ public static class Request extends ActionRequest implements ToXContentObject { public static final ParseField EXCLUDE_INTERIM = new ParseField("exclude_interim"); public static final ParseField START = new ParseField("start"); public static final ParseField END = new ParseField("end"); - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate forRestApiVersion - public static final ParseField ALLOW_NO_MATCH = new ParseField("allow_no_match").forRestApiVersion(onOrAfter(RestApiVersion.V_8)); - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate ref to RestApiVersion.V_7 - public static final ParseField ALLOW_NO_MATCH_V7 = new ParseField("allow_no_match", DEPRECATED_ALLOW_NO_JOBS_PARAM) - .forRestApiVersion(equalTo(RestApiVersion.V_7)); + public static final ParseField ALLOW_NO_MATCH = new ParseField("allow_no_match"); private static final ObjectParser PARSER = new ObjectParser<>(NAME, Request::new); @@ -88,7 +78,6 @@ public static class Request extends ActionRequest implements ToXContentObject { ); PARSER.declareString((request, endTime) -> request.setEnd(parseDateOrThrow(endTime, END, System::currentTimeMillis)), END); PARSER.declareBoolean(Request::setAllowNoMatch, ALLOW_NO_MATCH); - PARSER.declareBoolean(Request::setAllowNoMatch, ALLOW_NO_MATCH_V7); } static long parseDateOrThrow(String date, ParseField paramName, LongSupplier now) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedAction.java index bd4aac7ccad89..1278d1a57ec55 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/StopDatafeedAction.java @@ -13,9 +13,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.tasks.Task; import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; @@ -29,10 +27,6 @@ import java.io.IOException; import java.util.Objects; -import static org.elasticsearch.core.RestApiVersion.equalTo; -import static org.elasticsearch.core.RestApiVersion.onOrAfter; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.DEPRECATED_ALLOW_NO_DATAFEEDS_PARAM; - public class StopDatafeedAction extends ActionType { public static final StopDatafeedAction INSTANCE = new StopDatafeedAction(); @@ -47,11 +41,7 @@ public static class Request extends BaseTasksRequest implements ToXCont public static final ParseField TIMEOUT = new ParseField("timeout"); public static final ParseField FORCE = new ParseField("force"); - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate forRestApiVersion - public static final ParseField ALLOW_NO_MATCH = new ParseField("allow_no_match").forRestApiVersion(onOrAfter(RestApiVersion.V_8)); - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate ref to RestApiVersion.V_7 - public static final ParseField ALLOW_NO_MATCH_V7 = new ParseField("allow_no_match", DEPRECATED_ALLOW_NO_DATAFEEDS_PARAM) - .forRestApiVersion(equalTo(RestApiVersion.V_7)); + public static final ParseField ALLOW_NO_MATCH = new ParseField("allow_no_match"); public static final ObjectParser PARSER = new ObjectParser<>(NAME, Request::new); static { @@ -62,7 +52,6 @@ public static class Request extends BaseTasksRequest implements ToXCont ); PARSER.declareBoolean(Request::setForce, FORCE); PARSER.declareBoolean(Request::setAllowNoMatch, ALLOW_NO_MATCH); - PARSER.declareBoolean(Request::setAllowNoMatch, ALLOW_NO_MATCH_V7); } public static Request parseRequest(String datafeedId, XContentParser parser) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/cat/RestCatDatafeedsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/cat/RestCatDatafeedsAction.java index 205bb4f68a62c..417e9bca0a497 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/cat/RestCatDatafeedsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/cat/RestCatDatafeedsAction.java @@ -10,9 +10,7 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.Strings; import org.elasticsearch.common.Table; -import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.Scope; @@ -30,8 +28,6 @@ import java.util.List; import static org.elasticsearch.rest.RestRequest.Method.GET; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.DEPRECATED_ALLOW_NO_DATAFEEDS_PARAM; -import static org.elasticsearch.xpack.ml.rest.RestCompatibilityChecker.checkAndSetDeprecatedParam; @ServerlessScope(Scope.PUBLIC) public class RestCatDatafeedsAction extends AbstractCatAction { @@ -52,16 +48,8 @@ protected RestChannelConsumer doCatRequest(RestRequest restRequest, NodeClient c if (Strings.isNullOrEmpty(datafeedId)) { datafeedId = GetDatafeedsStatsAction.ALL; } - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate ref to RestApiVersion.V_7 Request request = new Request(datafeedId); - checkAndSetDeprecatedParam( - DEPRECATED_ALLOW_NO_DATAFEEDS_PARAM, - Request.ALLOW_NO_MATCH, - RestApiVersion.V_7, - restRequest, - (r, s) -> r.paramAsBoolean(s, request.allowNoMatch()), - request::setAllowNoMatch - ); + request.setAllowNoMatch(restRequest.paramAsBoolean(Request.ALLOW_NO_MATCH, request.allowNoMatch())); return channel -> client.execute(GetDatafeedsStatsAction.INSTANCE, request, new RestResponseListener<>(channel) { @Override public RestResponse buildResponse(Response getDatafeedsStatsRespons) throws Exception { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/cat/RestCatJobsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/cat/RestCatJobsAction.java index b27819bceee44..d8088c9510b6a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/cat/RestCatJobsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/cat/RestCatJobsAction.java @@ -12,9 +12,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.Table; import org.elasticsearch.common.unit.ByteSizeValue; -import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.Scope; @@ -35,8 +33,6 @@ import java.util.List; import static org.elasticsearch.rest.RestRequest.Method.GET; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.DEPRECATED_ALLOW_NO_JOBS_PARAM; -import static org.elasticsearch.xpack.ml.rest.RestCompatibilityChecker.checkAndSetDeprecatedParam; @ServerlessScope(Scope.PUBLIC) public class RestCatJobsAction extends AbstractCatAction { @@ -57,16 +53,8 @@ protected RestChannelConsumer doCatRequest(RestRequest restRequest, NodeClient c if (Strings.isNullOrEmpty(jobId)) { jobId = Metadata.ALL; } - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate ref to RestApiVersion.V_7 Request request = new Request(jobId); - checkAndSetDeprecatedParam( - DEPRECATED_ALLOW_NO_JOBS_PARAM, - Request.ALLOW_NO_MATCH, - RestApiVersion.V_7, - restRequest, - (r, s) -> r.paramAsBoolean(s, request.allowNoMatch()), - request::setAllowNoMatch - ); + request.setAllowNoMatch(restRequest.paramAsBoolean(Request.ALLOW_NO_MATCH, request.allowNoMatch())); return channel -> client.execute(GetJobsStatsAction.INSTANCE, request, new RestResponseListener<>(channel) { @Override public RestResponse buildResponse(Response getJobStatsResponse) throws Exception { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/datafeeds/RestGetDatafeedStatsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/datafeeds/RestGetDatafeedStatsAction.java index 8c85c055fca3b..a4879eca46d39 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/datafeeds/RestGetDatafeedStatsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/datafeeds/RestGetDatafeedStatsAction.java @@ -8,8 +8,6 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Strings; -import org.elasticsearch.core.RestApiVersion; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.Scope; @@ -24,10 +22,8 @@ import java.util.List; import static org.elasticsearch.rest.RestRequest.Method.GET; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.DEPRECATED_ALLOW_NO_DATAFEEDS_PARAM; import static org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig.ID; import static org.elasticsearch.xpack.ml.MachineLearning.BASE_PATH; -import static org.elasticsearch.xpack.ml.rest.RestCompatibilityChecker.checkAndSetDeprecatedParam; @ServerlessScope(Scope.PUBLIC) public class RestGetDatafeedStatsAction extends BaseRestHandler { @@ -48,16 +44,8 @@ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient if (Strings.isNullOrEmpty(datafeedId)) { datafeedId = GetDatafeedsStatsAction.ALL; } - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate ref to RestApiVersion.V_7 Request request = new Request(datafeedId); - checkAndSetDeprecatedParam( - DEPRECATED_ALLOW_NO_DATAFEEDS_PARAM, - Request.ALLOW_NO_MATCH, - RestApiVersion.V_7, - restRequest, - (r, s) -> r.paramAsBoolean(s, request.allowNoMatch()), - request::setAllowNoMatch - ); + request.setAllowNoMatch(restRequest.paramAsBoolean(Request.ALLOW_NO_MATCH, request.allowNoMatch())); return channel -> new RestCancellableNodeClient(client, restRequest.getHttpChannel()).execute( GetDatafeedsStatsAction.INSTANCE, request, diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/datafeeds/RestGetDatafeedsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/datafeeds/RestGetDatafeedsAction.java index fd0681f68a3a5..6955b81fdbb4c 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/datafeeds/RestGetDatafeedsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/datafeeds/RestGetDatafeedsAction.java @@ -7,8 +7,6 @@ package org.elasticsearch.xpack.ml.rest.datafeeds; import org.elasticsearch.client.internal.node.NodeClient; -import org.elasticsearch.core.RestApiVersion; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.Scope; @@ -26,11 +24,9 @@ import java.util.Set; import static org.elasticsearch.rest.RestRequest.Method.GET; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.DEPRECATED_ALLOW_NO_DATAFEEDS_PARAM; import static org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig.ID; import static org.elasticsearch.xpack.core.ml.utils.ToXContentParams.EXCLUDE_GENERATED; import static org.elasticsearch.xpack.ml.MachineLearning.BASE_PATH; -import static org.elasticsearch.xpack.ml.rest.RestCompatibilityChecker.checkAndSetDeprecatedParam; @ServerlessScope(Scope.PUBLIC) public class RestGetDatafeedsAction extends BaseRestHandler { @@ -51,16 +47,8 @@ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient if (datafeedId == null) { datafeedId = GetDatafeedsAction.ALL; } - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate ref to RestApiVersion.V_7 Request request = new Request(datafeedId); - checkAndSetDeprecatedParam( - DEPRECATED_ALLOW_NO_DATAFEEDS_PARAM, - GetDatafeedsStatsAction.Request.ALLOW_NO_MATCH, - RestApiVersion.V_7, - restRequest, - (r, s) -> r.paramAsBoolean(s, request.allowNoMatch()), - request::setAllowNoMatch - ); + request.setAllowNoMatch(restRequest.paramAsBoolean(GetDatafeedsStatsAction.Request.ALLOW_NO_MATCH, request.allowNoMatch())); return channel -> new RestCancellableNodeClient(client, restRequest.getHttpChannel()).execute( GetDatafeedsAction.INSTANCE, request, diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/datafeeds/RestStopDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/datafeeds/RestStopDatafeedAction.java index 8235e2785cc37..dcc213a571469 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/datafeeds/RestStopDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/datafeeds/RestStopDatafeedAction.java @@ -7,9 +7,7 @@ package org.elasticsearch.xpack.ml.rest.datafeeds; import org.elasticsearch.client.internal.node.NodeClient; -import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; @@ -28,10 +26,8 @@ import java.util.List; import static org.elasticsearch.rest.RestRequest.Method.POST; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.DEPRECATED_ALLOW_NO_DATAFEEDS_PARAM; import static org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfig.ID; import static org.elasticsearch.xpack.ml.MachineLearning.BASE_PATH; -import static org.elasticsearch.xpack.ml.rest.RestCompatibilityChecker.checkAndSetDeprecatedParam; @ServerlessScope(Scope.PUBLIC) public class RestStopDatafeedAction extends BaseRestHandler { @@ -49,7 +45,6 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException { String datafeedId = restRequest.param(DatafeedConfig.ID.getPreferredName()); - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate ref to RestApiVersion.V_7 Request request; if (restRequest.hasContentOrSourceParam()) { XContentParser parser = restRequest.contentOrSourceParamParser(); @@ -63,14 +58,7 @@ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient if (restRequest.hasParam(Request.FORCE.getPreferredName())) { request.setForce(restRequest.paramAsBoolean(Request.FORCE.getPreferredName(), request.isForce())); } - checkAndSetDeprecatedParam( - DEPRECATED_ALLOW_NO_DATAFEEDS_PARAM, - Request.ALLOW_NO_MATCH.getPreferredName(), - RestApiVersion.V_7, - restRequest, - (r, s) -> r.paramAsBoolean(s, request.allowNoMatch()), - request::setAllowNoMatch - ); + request.setAllowNoMatch(restRequest.paramAsBoolean(Request.ALLOW_NO_MATCH.getPreferredName(), request.allowNoMatch())); } return channel -> client.execute(StopDatafeedAction.INSTANCE, request, new RestBuilderListener(channel) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestCloseJobAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestCloseJobAction.java index f98a2f5a933ae..56afb7d65dfe3 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestCloseJobAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestCloseJobAction.java @@ -7,9 +7,7 @@ package org.elasticsearch.xpack.ml.rest.job; import org.elasticsearch.client.internal.node.NodeClient; -import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.Scope; @@ -23,10 +21,8 @@ import java.util.List; import static org.elasticsearch.rest.RestRequest.Method.POST; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.DEPRECATED_ALLOW_NO_JOBS_PARAM; import static org.elasticsearch.xpack.core.ml.job.config.Job.ID; import static org.elasticsearch.xpack.ml.MachineLearning.BASE_PATH; -import static org.elasticsearch.xpack.ml.rest.RestCompatibilityChecker.checkAndSetDeprecatedParam; @ServerlessScope(Scope.PUBLIC) public class RestCloseJobAction extends BaseRestHandler { @@ -43,7 +39,6 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException { - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate ref to RestApiVersion.V_7 Request request; if (restRequest.hasContentOrSourceParam()) { request = Request.parseRequest(restRequest.param(Job.ID.getPreferredName()), restRequest.contentParser()); @@ -57,14 +52,7 @@ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient if (restRequest.hasParam(Request.FORCE.getPreferredName())) { request.setForce(restRequest.paramAsBoolean(Request.FORCE.getPreferredName(), request.isForce())); } - checkAndSetDeprecatedParam( - DEPRECATED_ALLOW_NO_JOBS_PARAM, - Request.ALLOW_NO_MATCH.getPreferredName(), - RestApiVersion.V_7, - restRequest, - (r, s) -> r.paramAsBoolean(s, request.allowNoMatch()), - request::setAllowNoMatch - ); + request.setAllowNoMatch(restRequest.paramAsBoolean(Request.ALLOW_NO_MATCH.getPreferredName(), request.allowNoMatch())); } return channel -> client.execute(CloseJobAction.INSTANCE, request, new RestToXContentListener<>(channel)); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestGetJobStatsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestGetJobStatsAction.java index 2899faabdc40f..79951d7fad621 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestGetJobStatsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestGetJobStatsAction.java @@ -9,8 +9,6 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.Strings; -import org.elasticsearch.core.RestApiVersion; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.Scope; @@ -25,10 +23,8 @@ import java.util.List; import static org.elasticsearch.rest.RestRequest.Method.GET; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.DEPRECATED_ALLOW_NO_JOBS_PARAM; import static org.elasticsearch.xpack.core.ml.job.config.Job.ID; import static org.elasticsearch.xpack.ml.MachineLearning.BASE_PATH; -import static org.elasticsearch.xpack.ml.rest.RestCompatibilityChecker.checkAndSetDeprecatedParam; @ServerlessScope(Scope.PUBLIC) public class RestGetJobStatsAction extends BaseRestHandler { @@ -52,16 +48,8 @@ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient if (Strings.isNullOrEmpty(jobId)) { jobId = Metadata.ALL; } - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate ref to RestApiVersion.V_7 Request request = new Request(jobId); - checkAndSetDeprecatedParam( - DEPRECATED_ALLOW_NO_JOBS_PARAM, - Request.ALLOW_NO_MATCH, - RestApiVersion.V_7, - restRequest, - (r, s) -> r.paramAsBoolean(s, request.allowNoMatch()), - request::setAllowNoMatch - ); + request.setAllowNoMatch(restRequest.paramAsBoolean(Request.ALLOW_NO_MATCH, request.allowNoMatch())); return channel -> new RestCancellableNodeClient(client, restRequest.getHttpChannel()).execute( GetJobsStatsAction.INSTANCE, request, diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestGetJobsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestGetJobsAction.java index ae8d234d1d8bd..ea63a38ab01a8 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestGetJobsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/job/RestGetJobsAction.java @@ -9,8 +9,6 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.common.Strings; -import org.elasticsearch.core.RestApiVersion; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.Scope; @@ -27,11 +25,9 @@ import java.util.Set; import static org.elasticsearch.rest.RestRequest.Method.GET; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.DEPRECATED_ALLOW_NO_JOBS_PARAM; import static org.elasticsearch.xpack.core.ml.job.config.Job.ID; import static org.elasticsearch.xpack.core.ml.utils.ToXContentParams.EXCLUDE_GENERATED; import static org.elasticsearch.xpack.ml.MachineLearning.BASE_PATH; -import static org.elasticsearch.xpack.ml.rest.RestCompatibilityChecker.checkAndSetDeprecatedParam; @ServerlessScope(Scope.PUBLIC) public class RestGetJobsAction extends BaseRestHandler { @@ -52,16 +48,8 @@ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient if (Strings.isNullOrEmpty(jobId)) { jobId = Metadata.ALL; } - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate ref to RestApiVersion.V_7 Request request = new Request(jobId); - checkAndSetDeprecatedParam( - DEPRECATED_ALLOW_NO_JOBS_PARAM, - Request.ALLOW_NO_MATCH, - RestApiVersion.V_7, - restRequest, - (r, s) -> r.paramAsBoolean(s, request.allowNoMatch()), - request::setAllowNoMatch - ); + request.setAllowNoMatch(restRequest.paramAsBoolean(Request.ALLOW_NO_MATCH, request.allowNoMatch())); return channel -> new RestCancellableNodeClient(client, restRequest.getHttpChannel()).execute( GetJobsAction.INSTANCE, request, diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/results/RestGetOverallBucketsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/results/RestGetOverallBucketsAction.java index 2700e01cb9f6b..c74f49efe0aed 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/results/RestGetOverallBucketsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/results/RestGetOverallBucketsAction.java @@ -7,8 +7,6 @@ package org.elasticsearch.xpack.ml.rest.results; import org.elasticsearch.client.internal.node.NodeClient; -import org.elasticsearch.core.RestApiVersion; -import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.Scope; @@ -24,10 +22,8 @@ import static org.elasticsearch.rest.RestRequest.Method.GET; import static org.elasticsearch.rest.RestRequest.Method.POST; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.DEPRECATED_ALLOW_NO_JOBS_PARAM; import static org.elasticsearch.xpack.core.ml.job.config.Job.ID; import static org.elasticsearch.xpack.ml.MachineLearning.BASE_PATH; -import static org.elasticsearch.xpack.ml.rest.RestCompatibilityChecker.checkAndSetDeprecatedParam; @ServerlessScope(Scope.PUBLIC) public class RestGetOverallBucketsAction extends BaseRestHandler { @@ -48,7 +44,6 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException { String jobId = restRequest.param(Job.ID.getPreferredName()); - @UpdateForV9(owner = UpdateForV9.Owner.MACHINE_LEARNING) // v7 REST API no longer exists: eliminate ref to RestApiVersion.V_7 final Request request; if (restRequest.hasContentOrSourceParam()) { XContentParser parser = restRequest.contentOrSourceParamParser(); @@ -67,14 +62,7 @@ protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient if (restRequest.hasParam(Request.END.getPreferredName())) { request.setEnd(restRequest.param(Request.END.getPreferredName())); } - checkAndSetDeprecatedParam( - DEPRECATED_ALLOW_NO_JOBS_PARAM, - Request.ALLOW_NO_MATCH.getPreferredName(), - RestApiVersion.V_7, - restRequest, - (r, s) -> r.paramAsBoolean(s, request.allowNoMatch()), - request::setAllowNoMatch - ); + request.setAllowNoMatch(restRequest.paramAsBoolean(Request.ALLOW_NO_MATCH.getPreferredName(), request.allowNoMatch())); } return channel -> client.execute(GetOverallBucketsAction.INSTANCE, request, new RestToXContentListener<>(channel)); From 7573312f2ad955c0af47b365860b6aed8c705460 Mon Sep 17 00:00:00 2001 From: Tommaso Teofili Date: Thu, 12 Dec 2024 07:50:18 +0100 Subject: [PATCH 30/77] `_score` should not be a reserved attribute in ES|QL (#118435) --- docs/changelog/118435.yaml | 6 ++++ muted-tests.yml | 6 ++++ .../src/main/resources/scoring.csv-spec | 30 +++++++++++++++++++ .../xpack/esql/analysis/Verifier.java | 9 ------ .../xpack/esql/analysis/VerifierTests.java | 25 ---------------- 5 files changed, 42 insertions(+), 34 deletions(-) create mode 100644 docs/changelog/118435.yaml diff --git a/docs/changelog/118435.yaml b/docs/changelog/118435.yaml new file mode 100644 index 0000000000000..8bccbeb54698d --- /dev/null +++ b/docs/changelog/118435.yaml @@ -0,0 +1,6 @@ +pr: 118435 +summary: '`_score` should not be a reserved attribute in ES|QL' +area: ES|QL +type: enhancement +issues: + - 118460 diff --git a/muted-tests.yml b/muted-tests.yml index 573bd035b872b..5c99a2d5a5efd 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -180,6 +180,12 @@ tests: - class: "org.elasticsearch.xpack.esql.qa.mixed.MixedClusterEsqlSpecIT" method: "test {scoring.*}" issue: https://github.com/elastic/elasticsearch/issues/117641 +- class: "org.elasticsearch.xpack.esql.qa.mixed.MultilusterEsqlSpecIT" + method: "test {scoring.*}" + issue: https://github.com/elastic/elasticsearch/issues/118460 +- class: "org.elasticsearch.xpack.esql.ccq.MultiClusterSpecIT" + method: "test {scoring.*}" + issue: https://github.com/elastic/elasticsearch/issues/118460 - class: org.elasticsearch.xpack.esql.ccq.MultiClusterSpecIT method: test {scoring.QstrWithFieldAndScoringSortedEval} issue: https://github.com/elastic/elasticsearch/issues/117751 diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec index d4c7b8c59fdbc..cb38204a71ab0 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/scoring.csv-spec @@ -283,3 +283,33 @@ book_no:keyword | c_score:double 7350 | 2.0 7140 | 3.0 ; + +QstrScoreManipulation +required_capability: metadata_score +required_capability: qstr_function + +from books metadata _score +| where qstr("title:rings") +| eval _score = _score + 1 +| keep book_no, title, _score +| limit 2; + +book_no:keyword | title:text | _score:double +4023 | A Tolkien Compass: Including J. R. R. Tolkien's Guide to the Names in The Lord of the Rings | 2.6404519081115723 +2714 | Return of the King Being the Third Part of The Lord of the Rings | 2.9239964485168457 +; + +QstrScoreOverride +required_capability: metadata_score +required_capability: qstr_function + +from books metadata _score +| where qstr("title:rings") +| eval _score = "foobar" +| keep book_no, title, _score +| limit 2; + +book_no:keyword | title:text | _score:keyword +4023 | A Tolkien Compass: Including J. R. R. Tolkien's Guide to the Names in The Lord of the Rings | foobar +2714 | Return of the King Being the Third Part of The Lord of the Rings | foobar +; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java index a0728c9a91088..c805adf5d5a57 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java @@ -18,7 +18,6 @@ import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.Expressions; import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; -import org.elasticsearch.xpack.esql.core.expression.MetadataAttribute; import org.elasticsearch.xpack.esql.core.expression.NamedExpression; import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; import org.elasticsearch.xpack.esql.core.expression.function.Function; @@ -208,7 +207,6 @@ else if (p instanceof Lookup lookup) { checkJoin(p, failures); }); checkRemoteEnrich(plan, failures); - checkMetadataScoreNameReserved(plan, failures); if (failures.isEmpty()) { checkLicense(plan, licenseState, failures); @@ -222,13 +220,6 @@ else if (p instanceof Lookup lookup) { return failures; } - private static void checkMetadataScoreNameReserved(LogicalPlan p, Set failures) { - // _score can only be set as metadata attribute - if (p.inputSet().stream().anyMatch(a -> MetadataAttribute.SCORE.equals(a.name()) && (a instanceof MetadataAttribute) == false)) { - failures.add(fail(p, "`" + MetadataAttribute.SCORE + "` is a reserved METADATA attribute")); - } - } - private void checkSort(LogicalPlan p, Set failures) { if (p instanceof OrderBy ob) { ob.order().forEach(o -> { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index d58d233168e2b..84dcdbadef9f0 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -12,7 +12,6 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.action.EsqlCapabilities; -import org.elasticsearch.xpack.esql.core.expression.Attribute; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.core.type.EsField; import org.elasticsearch.xpack.esql.core.type.InvalidMappedField; @@ -22,7 +21,6 @@ import org.elasticsearch.xpack.esql.parser.EsqlParser; import org.elasticsearch.xpack.esql.parser.QueryParam; import org.elasticsearch.xpack.esql.parser.QueryParams; -import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -1805,29 +1803,6 @@ public void testToDatePeriodToTimeDurationWithInvalidType() { ); } - public void testNonMetadataScore() { - assumeTrue("'METADATA _score' is disabled", EsqlCapabilities.Cap.METADATA_SCORE.isEnabled()); - assertEquals("1:12: `_score` is a reserved METADATA attribute", error("from foo | eval _score = 10")); - - assertEquals( - "1:48: `_score` is a reserved METADATA attribute", - error("from foo metadata _score | where qstr(\"bar\") | eval _score = _score + 1") - ); - } - - public void testScoreRenaming() { - assumeTrue("'METADATA _score' is disabled", EsqlCapabilities.Cap.METADATA_SCORE.isEnabled()); - assertEquals("1:33: `_score` is a reserved METADATA attribute", error("from foo METADATA _id, _score | rename _id as _score")); - - assertTrue(passes("from foo metadata _score | rename _score as foo").stream().anyMatch(a -> a.name().equals("foo"))); - } - - private List passes(String query) { - LogicalPlan logicalPlan = defaultAnalyzer.analyze(parser.createStatement(query)); - assertTrue(logicalPlan.resolved()); - return logicalPlan.output(); - } - public void testIntervalAsString() { // DateTrunc for (String interval : List.of("1 minu", "1 dy", "1.5 minutes", "0.5 days", "minutes 1", "day 5")) { From ff8e5e964e545984063dc21b28e6c8da0f5acbcb Mon Sep 17 00:00:00 2001 From: Iraklis Psaroudakis Date: Thu, 12 Dec 2024 09:41:14 +0200 Subject: [PATCH 31/77] Improve InputStreamIndexInput testSkipBytes (#118485) Relates ES-10234 --- .../store/InputStreamIndexInputTests.java | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/common/lucene/store/InputStreamIndexInputTests.java b/server/src/test/java/org/elasticsearch/common/lucene/store/InputStreamIndexInputTests.java index 4bea6f50c7c4b..b982bd7b95aad 100644 --- a/server/src/test/java/org/elasticsearch/common/lucene/store/InputStreamIndexInputTests.java +++ b/server/src/test/java/org/elasticsearch/common/lucene/store/InputStreamIndexInputTests.java @@ -11,6 +11,7 @@ import org.apache.lucene.store.ByteBuffersDirectory; import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FilterIndexInput; import org.apache.lucene.store.IOContext; import org.apache.lucene.store.IndexInput; import org.apache.lucene.store.IndexOutput; @@ -267,17 +268,47 @@ public void testSkipBytes() throws Exception { skipBytesExpected ); - IndexInput input = dir.openInput("test", IOContext.DEFAULT); - InputStreamIndexInput is = new InputStreamIndexInput(input, limit); + var countingInput = new CountingReadBytesIndexInput("test", dir.openInput("test", IOContext.DEFAULT)); + InputStreamIndexInput is = new InputStreamIndexInput(countingInput, limit); is.readNBytes(initialReadBytes); assertThat(is.skip(skipBytes), equalTo((long) skipBytesExpected)); + long expectedActualInitialBytesRead = Math.min(Math.min(initialReadBytes, limit), bytes); + assertThat(countingInput.getBytesRead(), equalTo(expectedActualInitialBytesRead)); int remainingBytes = Math.min(bytes, limit) - seekExpected; for (int i = seekExpected; i < seekExpected + remainingBytes; i++) { assertThat(is.read(), equalTo(i)); } + assertThat(countingInput.getBytesRead(), equalTo(expectedActualInitialBytesRead + remainingBytes)); } + protected static class CountingReadBytesIndexInput extends FilterIndexInput { + private long bytesRead = 0; + + public CountingReadBytesIndexInput(String resourceDescription, IndexInput in) { + super(resourceDescription, in); + } + + @Override + public byte readByte() throws IOException { + long filePointerBefore = getFilePointer(); + byte b = super.readByte(); + bytesRead += getFilePointer() - filePointerBefore; + return b; + } + + @Override + public void readBytes(byte[] b, int offset, int len) throws IOException { + long filePointerBefore = getFilePointer(); + super.readBytes(b, offset, len); + bytesRead += getFilePointer() - filePointerBefore; + } + + public long getBytesRead() { + return bytesRead; + } + }; + public void testReadZeroShouldReturnZero() throws IOException { try (Directory dir = new ByteBuffersDirectory()) { try (IndexOutput output = dir.createOutput("test", IOContext.DEFAULT)) { From 80a1a6f7afef990a7243a2e415866c6746113e28 Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 12 Dec 2024 08:59:52 +0100 Subject: [PATCH 32/77] ESQL: Add CCS tests for FLS and DLS against data streams (#118423) CCS test coverage for https://github.com/elastic/elasticsearch/pull/118378 --- .../security/qa/multi-cluster/build.gradle | 1 + ...teClusterSecurityDataStreamEsqlRcs1IT.java | 402 ++++++++++++++++++ ...teClusterSecurityDataStreamEsqlRcs2IT.java | 126 ++++++ .../src/javaRestTest/resources/roles.yml | 99 +++++ 4 files changed, 628 insertions(+) create mode 100644 x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityDataStreamEsqlRcs1IT.java create mode 100644 x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityDataStreamEsqlRcs2IT.java diff --git a/x-pack/plugin/security/qa/multi-cluster/build.gradle b/x-pack/plugin/security/qa/multi-cluster/build.gradle index 5b682cfdccade..f4eee4ef46c02 100644 --- a/x-pack/plugin/security/qa/multi-cluster/build.gradle +++ b/x-pack/plugin/security/qa/multi-cluster/build.gradle @@ -24,6 +24,7 @@ dependencies { clusterModules project(':x-pack:plugin:enrich') clusterModules project(':x-pack:plugin:autoscaling') clusterModules project(':x-pack:plugin:ml') + clusterModules project(xpackModule('ilm')) clusterModules(project(":modules:ingest-common")) } diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityDataStreamEsqlRcs1IT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityDataStreamEsqlRcs1IT.java new file mode 100644 index 0000000000000..57eb583912c49 --- /dev/null +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityDataStreamEsqlRcs1IT.java @@ -0,0 +1,402 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.remotecluster; + +import org.elasticsearch.Build; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.CheckedFunction; +import org.elasticsearch.core.Strings; +import org.elasticsearch.test.MapMatcher; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.util.resource.Resource; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.json.JsonXContent; +import org.junit.ClassRule; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +import static org.elasticsearch.test.ListMatcher.matchesList; +import static org.elasticsearch.test.MapMatcher.assertMap; +import static org.elasticsearch.test.MapMatcher.matchesMap; + +// TODO consolidate me with RemoteClusterSecurityDataStreamEsqlRcs2IT +public class RemoteClusterSecurityDataStreamEsqlRcs1IT extends AbstractRemoteClusterSecurityTestCase { + static { + fulfillingCluster = ElasticsearchCluster.local() + .name("fulfilling-cluster") + .module("x-pack-autoscaling") + .module("x-pack-esql") + .module("x-pack-enrich") + .module("x-pack-ml") + .module("x-pack-ilm") + .module("ingest-common") + .apply(commonClusterConfig) + .setting("xpack.ml.enabled", "false") + .setting("xpack.security.authc.token.enabled", "true") + .rolesFile(Resource.fromClasspath("roles.yml")) + .build(); + + queryCluster = ElasticsearchCluster.local() + .name("query-cluster") + .module("x-pack-autoscaling") + .module("x-pack-esql") + .module("x-pack-enrich") + .module("x-pack-ml") + .module("x-pack-ilm") + .module("ingest-common") + .apply(commonClusterConfig) + .setting("xpack.ml.enabled", "false") + .setting("xpack.security.authc.token.enabled", "true") + .rolesFile(Resource.fromClasspath("roles.yml")) + .user("logs_foo_all", "x-pack-test-password", "logs_foo_all", false) + .user("logs_foo_16_only", "x-pack-test-password", "logs_foo_16_only", false) + .user("logs_foo_after_2021", "x-pack-test-password", "logs_foo_after_2021", false) + .user("logs_foo_after_2021_pattern", "x-pack-test-password", "logs_foo_after_2021_pattern", false) + .user("logs_foo_after_2021_alias", "x-pack-test-password", "logs_foo_after_2021_alias", false) + .build(); + } + + @ClassRule + public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster); + + public void testDataStreamsWithDlsAndFls() throws Exception { + configureRemoteCluster(REMOTE_CLUSTER_ALIAS, fulfillingCluster, true, randomBoolean(), randomBoolean()); + createDataStreamOnFulfillingCluster(); + setupAdditionalUsersAndRoles(); + + doTestDataStreamsWithFlsAndDls(); + } + + private void setupAdditionalUsersAndRoles() throws IOException { + createUserAndRoleOnQueryCluster("fls_user_logs_pattern", "fls_user_logs_pattern", """ + { + "indices": [ + { + "names": ["logs-*"], + "privileges": ["read"], + "field_security": { + "grant": ["@timestamp", "data_stream.namespace"] + } + } + ] + }"""); + createUserAndRoleOnFulfillingCluster("fls_user_logs_pattern", "fls_user_logs_pattern", """ + { + "indices": [ + { + "names": ["logs-*"], + "privileges": ["read"], + "field_security": { + "grant": ["@timestamp", "data_stream.namespace"] + } + } + ] + }"""); + } + + static void createUserAndRoleOnQueryCluster(String username, String roleName, String roleJson) throws IOException { + final var putRoleRequest = new Request("PUT", "/_security/role/" + roleName); + putRoleRequest.setJsonEntity(roleJson); + assertOK(adminClient().performRequest(putRoleRequest)); + + final var putUserRequest = new Request("PUT", "/_security/user/" + username); + putUserRequest.setJsonEntity(Strings.format(""" + { + "password": "%s", + "roles" : ["%s"] + }""", PASS, roleName)); + assertOK(adminClient().performRequest(putUserRequest)); + } + + static void createUserAndRoleOnFulfillingCluster(String username, String roleName, String roleJson) throws IOException { + final var putRoleRequest = new Request("PUT", "/_security/role/" + roleName); + putRoleRequest.setJsonEntity(roleJson); + assertOK(performRequestAgainstFulfillingCluster(putRoleRequest)); + + final var putUserRequest = new Request("PUT", "/_security/user/" + username); + putUserRequest.setJsonEntity(Strings.format(""" + { + "password": "%s", + "roles" : ["%s"] + }""", PASS, roleName)); + assertOK(performRequestAgainstFulfillingCluster(putUserRequest)); + } + + static Response runESQLCommandAgainstQueryCluster(String user, String command) throws IOException { + if (command.toLowerCase(Locale.ROOT).contains("limit") == false) { + // add a (high) limit to avoid warnings on default limit + command += " | limit 10000000"; + } + XContentBuilder json = JsonXContent.contentBuilder(); + json.startObject(); + json.field("query", command); + addRandomPragmas(json); + json.endObject(); + Request request = new Request("POST", "_query"); + request.setJsonEntity(org.elasticsearch.common.Strings.toString(json)); + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("es-security-runas-user", user)); + request.addParameter("error_trace", "true"); + Response response = adminClient().performRequest(request); + assertOK(response); + return response; + } + + static void addRandomPragmas(XContentBuilder builder) throws IOException { + if (Build.current().isSnapshot()) { + Settings pragmas = randomPragmas(); + if (pragmas != Settings.EMPTY) { + builder.startObject("pragma"); + builder.value(pragmas); + builder.endObject(); + } + } + } + + static Settings randomPragmas() { + Settings.Builder settings = Settings.builder(); + if (randomBoolean()) { + settings.put("page_size", between(1, 5)); + } + if (randomBoolean()) { + settings.put("exchange_buffer_size", between(1, 2)); + } + if (randomBoolean()) { + settings.put("data_partitioning", randomFrom("shard", "segment", "doc")); + } + if (randomBoolean()) { + settings.put("enrich_max_workers", between(1, 5)); + } + if (randomBoolean()) { + settings.put("node_level_reduction", randomBoolean()); + } + return settings.build(); + } + + static void createDataStreamOnFulfillingCluster() throws Exception { + createDataStreamPolicy(AbstractRemoteClusterSecurityTestCase::performRequestAgainstFulfillingCluster); + createDataStreamComponentTemplate(AbstractRemoteClusterSecurityTestCase::performRequestAgainstFulfillingCluster); + createDataStreamIndexTemplate(AbstractRemoteClusterSecurityTestCase::performRequestAgainstFulfillingCluster); + createDataStreamDocuments(AbstractRemoteClusterSecurityTestCase::performRequestAgainstFulfillingCluster); + createDataStreamAlias(AbstractRemoteClusterSecurityTestCase::performRequestAgainstFulfillingCluster); + } + + private static void createDataStreamPolicy(CheckedFunction requestConsumer) throws Exception { + Request request = new Request("PUT", "_ilm/policy/my-lifecycle-policy"); + request.setJsonEntity(""" + { + "policy": { + "phases": { + "hot": { + "actions": { + "rollover": { + "max_primary_shard_size": "50gb" + } + } + }, + "delete": { + "min_age": "735d", + "actions": { + "delete": {} + } + } + } + } + }"""); + + requestConsumer.apply(request); + } + + private static void createDataStreamComponentTemplate(CheckedFunction requestConsumer) throws Exception { + Request request = new Request("PUT", "_component_template/my-template"); + request.setJsonEntity(""" + { + "template": { + "settings": { + "index.lifecycle.name": "my-lifecycle-policy" + }, + "mappings": { + "properties": { + "@timestamp": { + "type": "date", + "format": "date_optional_time||epoch_millis" + }, + "data_stream": { + "properties": { + "namespace": {"type": "keyword"}, + "environment": {"type": "keyword"} + } + } + } + } + } + }"""); + requestConsumer.apply(request); + } + + private static void createDataStreamIndexTemplate(CheckedFunction requestConsumer) throws Exception { + Request request = new Request("PUT", "_index_template/my-index-template"); + request.setJsonEntity(""" + { + "index_patterns": ["logs-*"], + "data_stream": {}, + "composed_of": ["my-template"], + "priority": 500 + }"""); + requestConsumer.apply(request); + } + + private static void createDataStreamDocuments(CheckedFunction requestConsumer) throws Exception { + Request request = new Request("POST", "logs-foo/_bulk"); + request.addParameter("refresh", ""); + request.setJsonEntity(""" + { "create" : {} } + { "@timestamp": "2099-05-06T16:21:15.000Z", "data_stream": {"namespace": "16", "environment": "dev"} } + { "create" : {} } + { "@timestamp": "2001-05-06T16:21:15.000Z", "data_stream": {"namespace": "17", "environment": "prod"} } + """); + assertMap(entityAsMap(requestConsumer.apply(request)), matchesMap().extraOk().entry("errors", false)); + } + + private static void createDataStreamAlias(CheckedFunction requestConsumer) throws Exception { + Request request = new Request("PUT", "_alias"); + request.setJsonEntity(""" + { + "actions": [ + { + "add": { + "index": "logs-foo", + "alias": "alias-foo" + } + } + ] + }"""); + assertMap(entityAsMap(requestConsumer.apply(request)), matchesMap().extraOk().entry("errors", false)); + } + + static void doTestDataStreamsWithFlsAndDls() throws IOException { + // DLS + MapMatcher twoResults = matchesMap().extraOk().entry("values", matchesList().item(matchesList().item(2))); + MapMatcher oneResult = matchesMap().extraOk().entry("values", matchesList().item(matchesList().item(1))); + assertMap( + entityAsMap(runESQLCommandAgainstQueryCluster("logs_foo_all", "FROM my_remote_cluster:logs-foo | STATS COUNT(*)")), + twoResults + ); + assertMap( + entityAsMap(runESQLCommandAgainstQueryCluster("logs_foo_16_only", "FROM my_remote_cluster:logs-foo | STATS COUNT(*)")), + oneResult + ); + assertMap( + entityAsMap(runESQLCommandAgainstQueryCluster("logs_foo_after_2021", "FROM my_remote_cluster:logs-foo | STATS COUNT(*)")), + oneResult + ); + assertMap( + entityAsMap( + runESQLCommandAgainstQueryCluster("logs_foo_after_2021_pattern", "FROM my_remote_cluster:logs-foo | STATS COUNT(*)") + ), + oneResult + ); + assertMap( + entityAsMap(runESQLCommandAgainstQueryCluster("logs_foo_all", "FROM my_remote_cluster:logs-* | STATS COUNT(*)")), + twoResults + ); + assertMap( + entityAsMap(runESQLCommandAgainstQueryCluster("logs_foo_16_only", "FROM my_remote_cluster:logs-* | STATS COUNT(*)")), + oneResult + ); + assertMap( + entityAsMap(runESQLCommandAgainstQueryCluster("logs_foo_after_2021", "FROM my_remote_cluster:logs-* | STATS COUNT(*)")), + oneResult + ); + assertMap( + entityAsMap(runESQLCommandAgainstQueryCluster("logs_foo_after_2021_pattern", "FROM my_remote_cluster:logs-* | STATS COUNT(*)")), + oneResult + ); + + assertMap( + entityAsMap( + runESQLCommandAgainstQueryCluster("logs_foo_after_2021_alias", "FROM my_remote_cluster:alias-foo | STATS COUNT(*)") + ), + oneResult + ); + assertMap( + entityAsMap(runESQLCommandAgainstQueryCluster("logs_foo_after_2021_alias", "FROM my_remote_cluster:alias-* | STATS COUNT(*)")), + oneResult + ); + + // FLS + // logs_foo_all does not have FLS restrictions so should be able to access all fields + assertMap( + entityAsMap( + runESQLCommandAgainstQueryCluster("logs_foo_all", "FROM my_remote_cluster:logs-foo | SORT data_stream.namespace | LIMIT 1") + ), + matchesMap().extraOk() + .entry( + "columns", + List.of( + matchesMap().entry("name", "@timestamp").entry("type", "date"), + matchesMap().entry("name", "data_stream.environment").entry("type", "keyword"), + matchesMap().entry("name", "data_stream.namespace").entry("type", "keyword") + ) + ) + ); + assertMap( + entityAsMap( + runESQLCommandAgainstQueryCluster("logs_foo_all", "FROM my_remote_cluster:logs-* | SORT data_stream.namespace | LIMIT 1") + ), + matchesMap().extraOk() + .entry( + "columns", + List.of( + matchesMap().entry("name", "@timestamp").entry("type", "date"), + matchesMap().entry("name", "data_stream.environment").entry("type", "keyword"), + matchesMap().entry("name", "data_stream.namespace").entry("type", "keyword") + ) + ) + ); + + assertMap( + entityAsMap( + runESQLCommandAgainstQueryCluster( + "fls_user_logs_pattern", + "FROM my_remote_cluster:logs-foo | SORT data_stream.namespace | LIMIT 1" + ) + ), + matchesMap().extraOk() + .entry( + "columns", + List.of( + matchesMap().entry("name", "@timestamp").entry("type", "date"), + matchesMap().entry("name", "data_stream.namespace").entry("type", "keyword") + ) + ) + ); + assertMap( + entityAsMap( + runESQLCommandAgainstQueryCluster( + "fls_user_logs_pattern", + "FROM my_remote_cluster:logs-* | SORT data_stream.namespace | LIMIT 1" + ) + ), + matchesMap().extraOk() + .entry( + "columns", + List.of( + matchesMap().entry("name", "@timestamp").entry("type", "date"), + matchesMap().entry("name", "data_stream.namespace").entry("type", "keyword") + ) + ) + ); + } +} diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityDataStreamEsqlRcs2IT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityDataStreamEsqlRcs2IT.java new file mode 100644 index 0000000000000..c5cf704177020 --- /dev/null +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityDataStreamEsqlRcs2IT.java @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.remotecluster; + +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.util.resource.Resource; +import org.elasticsearch.test.junit.RunnableTestRuleAdapter; +import org.junit.ClassRule; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.elasticsearch.xpack.remotecluster.RemoteClusterSecurityDataStreamEsqlRcs1IT.createDataStreamOnFulfillingCluster; +import static org.elasticsearch.xpack.remotecluster.RemoteClusterSecurityDataStreamEsqlRcs1IT.createUserAndRoleOnQueryCluster; +import static org.elasticsearch.xpack.remotecluster.RemoteClusterSecurityDataStreamEsqlRcs1IT.doTestDataStreamsWithFlsAndDls; + +// TODO consolidate me with RemoteClusterSecurityDataStreamEsqlRcs1IT +public class RemoteClusterSecurityDataStreamEsqlRcs2IT extends AbstractRemoteClusterSecurityTestCase { + private static final AtomicReference> API_KEY_MAP_REF = new AtomicReference<>(); + private static final AtomicBoolean SSL_ENABLED_REF = new AtomicBoolean(); + private static final AtomicBoolean NODE1_RCS_SERVER_ENABLED = new AtomicBoolean(); + private static final AtomicBoolean NODE2_RCS_SERVER_ENABLED = new AtomicBoolean(); + + static { + fulfillingCluster = ElasticsearchCluster.local() + .name("fulfilling-cluster") + .nodes(3) + .module("x-pack-autoscaling") + .module("x-pack-esql") + .module("x-pack-enrich") + .module("x-pack-ml") + .module("x-pack-ilm") + .module("ingest-common") + .apply(commonClusterConfig) + .setting("remote_cluster.port", "0") + .setting("xpack.ml.enabled", "false") + .setting("xpack.security.remote_cluster_server.ssl.enabled", () -> String.valueOf(SSL_ENABLED_REF.get())) + .setting("xpack.security.remote_cluster_server.ssl.key", "remote-cluster.key") + .setting("xpack.security.remote_cluster_server.ssl.certificate", "remote-cluster.crt") + .setting("xpack.security.authc.token.enabled", "true") + .keystore("xpack.security.remote_cluster_server.ssl.secure_key_passphrase", "remote-cluster-password") + .node(0, spec -> spec.setting("remote_cluster_server.enabled", "true")) + .node(1, spec -> spec.setting("remote_cluster_server.enabled", () -> String.valueOf(NODE1_RCS_SERVER_ENABLED.get()))) + .node(2, spec -> spec.setting("remote_cluster_server.enabled", () -> String.valueOf(NODE2_RCS_SERVER_ENABLED.get()))) + .build(); + + queryCluster = ElasticsearchCluster.local() + .name("query-cluster") + .module("x-pack-autoscaling") + .module("x-pack-esql") + .module("x-pack-enrich") + .module("x-pack-ml") + .module("x-pack-ilm") + .module("ingest-common") + .apply(commonClusterConfig) + .setting("xpack.ml.enabled", "false") + .setting("xpack.security.remote_cluster_client.ssl.enabled", () -> String.valueOf(SSL_ENABLED_REF.get())) + .setting("xpack.security.remote_cluster_client.ssl.certificate_authorities", "remote-cluster-ca.crt") + .setting("xpack.security.authc.token.enabled", "true") + .keystore("cluster.remote.my_remote_cluster.credentials", () -> { + if (API_KEY_MAP_REF.get() == null) { + final Map apiKeyMap = createCrossClusterAccessApiKey(""" + { + "search": [ + { + "names": ["logs-*", "alias-*"] + } + ] + }"""); + API_KEY_MAP_REF.set(apiKeyMap); + } + return (String) API_KEY_MAP_REF.get().get("encoded"); + }) + .rolesFile(Resource.fromClasspath("roles.yml")) + .user("logs_foo_all", "x-pack-test-password", "logs_foo_all", false) + .user("logs_foo_16_only", "x-pack-test-password", "logs_foo_16_only", false) + .user("logs_foo_after_2021", "x-pack-test-password", "logs_foo_after_2021", false) + .user("logs_foo_after_2021_pattern", "x-pack-test-password", "logs_foo_after_2021_pattern", false) + .user("logs_foo_after_2021_alias", "x-pack-test-password", "logs_foo_after_2021_alias", false) + .build(); + } + + @ClassRule + // Use a RuleChain to ensure that fulfilling cluster is started before query cluster + // `SSL_ENABLED_REF` is used to control the SSL-enabled setting on the test clusters + // We set it here, since randomization methods are not available in the static initialize context above + public static TestRule clusterRule = RuleChain.outerRule(new RunnableTestRuleAdapter(() -> { + SSL_ENABLED_REF.set(usually()); + NODE1_RCS_SERVER_ENABLED.set(randomBoolean()); + NODE2_RCS_SERVER_ENABLED.set(randomBoolean()); + })).around(fulfillingCluster).around(queryCluster); + + public void testDataStreamsWithDlsAndFls() throws Exception { + configureRemoteCluster(); + createDataStreamOnFulfillingCluster(); + setupAdditionalUsersAndRoles(); + + doTestDataStreamsWithFlsAndDls(); + } + + private void setupAdditionalUsersAndRoles() throws IOException { + createUserAndRoleOnQueryCluster("fls_user_logs_pattern", "fls_user_logs_pattern", """ + { + "indices": [{"names": [""], "privileges": ["read"]}], + "remote_indices": [ + { + "names": ["logs-*"], + "privileges": ["read"], + "field_security": { + "grant": ["@timestamp", "data_stream.namespace"] + }, + "clusters": ["*"] + } + ] + }"""); + } +} diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/resources/roles.yml b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/resources/roles.yml index b61daa068ed1a..c09f9dc620a4c 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/resources/roles.yml +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/resources/roles.yml @@ -41,3 +41,102 @@ ccr_user_role: manage_role: cluster: [ 'manage' ] + +logs_foo_all: + cluster: [] + indices: + - names: [ 'logs-foo' ] + privileges: [ 'read' ] + remote_indices: + - names: [ 'logs-foo' ] + clusters: [ '*' ] + privileges: [ 'read' ] + +logs_foo_16_only: + cluster: [] + indices: + - names: [ 'logs-foo' ] + privileges: [ 'read' ] + query: | + { + "term": { + "data_stream.namespace": "16" + } + } + remote_indices: + - names: [ 'logs-foo' ] + clusters: [ '*' ] + privileges: [ 'read' ] + query: | + { + "term": { + "data_stream.namespace": "16" + } + } + +logs_foo_after_2021: + cluster: [] + indices: + - names: [ 'logs-foo' ] + privileges: [ 'read' ] + query: | + { + "range": { + "@timestamp": {"gte": "2021-01-01T00:00:00"} + } + } + remote_indices: + - names: [ 'logs-foo' ] + clusters: [ '*' ] + privileges: [ 'read' ] + query: | + { + "range": { + "@timestamp": {"gte": "2021-01-01T00:00:00"} + } + } + +logs_foo_after_2021_pattern: + cluster: [] + indices: + - names: [ 'logs-*' ] + privileges: [ 'read' ] + query: | + { + "range": { + "@timestamp": {"gte": "2021-01-01T00:00:00"} + } + } + remote_indices: + - names: [ 'logs-*' ] + clusters: [ '*' ] + privileges: [ 'read' ] + query: | + { + "range": { + "@timestamp": {"gte": "2021-01-01T00:00:00"} + } + } + +logs_foo_after_2021_alias: + cluster: [] + indices: + - names: [ 'alias-foo' ] + privileges: [ 'read' ] + query: | + { + "range": { + "@timestamp": {"gte": "2021-01-01T00:00:00"} + } + } + remote_indices: + - names: [ 'alias-foo' ] + clusters: [ '*' ] + privileges: [ 'read' ] + query: | + { + "range": { + "@timestamp": {"gte": "2021-01-01T00:00:00"} + } + } + From 95315cc08c702cbb53c0cf6544773be58bbc373f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Thu, 12 Dec 2024 09:28:58 +0100 Subject: [PATCH 33/77] Building scope -> entitlements map during PolicyManager initialization (#118070) --- .../runtime/policy/PolicyManager.java | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java index a77c86d5ffd04..8d3efe4eb98e6 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java @@ -18,7 +18,6 @@ import java.lang.module.ModuleFinder; import java.lang.module.ModuleReference; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; @@ -56,8 +55,8 @@ public Stream getEntitlements(Class entitlementCla final Map moduleEntitlementsMap = new HashMap<>(); - protected final Policy serverPolicy; - protected final Map pluginPolicies; + protected final Map> serverEntitlements; + protected final Map>> pluginsEntitlements; private final Function, String> pluginResolver; public static final String ALL_UNNAMED = "ALL-UNNAMED"; @@ -79,19 +78,16 @@ private static Set findSystemModules() { } public PolicyManager(Policy defaultPolicy, Map pluginPolicies, Function, String> pluginResolver) { - this.serverPolicy = Objects.requireNonNull(defaultPolicy); - this.pluginPolicies = Collections.unmodifiableMap(Objects.requireNonNull(pluginPolicies)); + this.serverEntitlements = buildScopeEntitlementsMap(Objects.requireNonNull(defaultPolicy)); + this.pluginsEntitlements = Objects.requireNonNull(pluginPolicies) + .entrySet() + .stream() + .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> buildScopeEntitlementsMap(e.getValue()))); this.pluginResolver = pluginResolver; } - private static List lookupEntitlementsForModule(Policy policy, String moduleName) { - for (int i = 0; i < policy.scopes.size(); ++i) { - var scope = policy.scopes.get(i); - if (scope.name.equals(moduleName)) { - return scope.entitlements; - } - } - return null; + private static Map> buildScopeEntitlementsMap(Policy policy) { + return policy.scopes.stream().collect(Collectors.toUnmodifiableMap(scope -> scope.name, scope -> scope.entitlements)); } public void checkExitVM(Class callerClass) { @@ -141,21 +137,21 @@ ModuleEntitlements getEntitlementsOrThrow(Class callerClass, Module requestin if (isServerModule(requestingModule)) { var scopeName = requestingModule.getName(); - return getModuleEntitlementsOrThrow(callerClass, requestingModule, serverPolicy, scopeName); + return getModuleEntitlementsOrThrow(callerClass, requestingModule, serverEntitlements, scopeName); } // plugins var pluginName = pluginResolver.apply(callerClass); if (pluginName != null) { - var pluginPolicy = pluginPolicies.get(pluginName); - if (pluginPolicy != null) { + var pluginEntitlements = pluginsEntitlements.get(pluginName); + if (pluginEntitlements != null) { final String scopeName; if (requestingModule.isNamed() == false) { scopeName = ALL_UNNAMED; } else { scopeName = requestingModule.getName(); } - return getModuleEntitlementsOrThrow(callerClass, requestingModule, pluginPolicy, scopeName); + return getModuleEntitlementsOrThrow(callerClass, requestingModule, pluginEntitlements, scopeName); } } @@ -167,15 +163,20 @@ private static String buildModuleNoPolicyMessage(Class callerClass, Module re return Strings.format("Missing entitlement policy: caller [%s], module [%s]", callerClass, requestingModule.getName()); } - private ModuleEntitlements getModuleEntitlementsOrThrow(Class callerClass, Module module, Policy policy, String moduleName) { - var entitlements = lookupEntitlementsForModule(policy, moduleName); + private ModuleEntitlements getModuleEntitlementsOrThrow( + Class callerClass, + Module module, + Map> scopeEntitlements, + String moduleName + ) { + var entitlements = scopeEntitlements.get(moduleName); if (entitlements == null) { // Module without entitlements - remember we don't have any moduleEntitlementsMap.put(module, ModuleEntitlements.NONE); throw new NotEntitledException(buildModuleNoPolicyMessage(callerClass, module)); } // We have a policy for this module - var classEntitlements = createClassEntitlements(entitlements); + var classEntitlements = new ModuleEntitlements(entitlements); moduleEntitlementsMap.put(module, classEntitlements); return classEntitlements; } @@ -184,10 +185,6 @@ private static boolean isServerModule(Module requestingModule) { return requestingModule.isNamed() && requestingModule.getLayer() == ModuleLayer.boot(); } - private ModuleEntitlements createClassEntitlements(List entitlements) { - return new ModuleEntitlements(entitlements); - } - private static Module requestingModule(Class callerClass) { if (callerClass != null) { Module callerModule = callerClass.getModule(); @@ -222,6 +219,6 @@ private static boolean isTriviallyAllowed(Module requestingModule) { @Override public String toString() { - return "PolicyManager{" + "serverPolicy=" + serverPolicy + ", pluginPolicies=" + pluginPolicies + '}'; + return "PolicyManager{" + "serverEntitlements=" + serverEntitlements + ", pluginsEntitlements=" + pluginsEntitlements + '}'; } } From adddfa27ad69d270139e05b9e65123740f9225a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Thu, 12 Dec 2024 09:36:50 +0100 Subject: [PATCH 34/77] Add one test for plugin type to `PluginsLoaderTests` (#117725) * Add one test for plugin type to PluginsLoaderTests * Suppress ExtraFs (or PluginsUtils etc could fail with extra0 files) --- .../elasticsearch/plugins/PluginsLoader.java | 6 +- .../plugins/PluginsLoaderTests.java | 249 ++++++++++++++++++ 2 files changed, 252 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/plugins/PluginsLoader.java b/server/src/main/java/org/elasticsearch/plugins/PluginsLoader.java index aadda93f977b6..8dfc1fc27c6aa 100644 --- a/server/src/main/java/org/elasticsearch/plugins/PluginsLoader.java +++ b/server/src/main/java/org/elasticsearch/plugins/PluginsLoader.java @@ -416,7 +416,7 @@ static String toModuleName(String name) { return result; } - static final String toPackageName(String className) { + static String toPackageName(String className) { assert className.endsWith(".") == false; int index = className.lastIndexOf('.'); if (index == -1) { @@ -426,11 +426,11 @@ static final String toPackageName(String className) { } @SuppressForbidden(reason = "I need to convert URL's to Paths") - static final Path[] urlsToPaths(Set urls) { + static Path[] urlsToPaths(Set urls) { return urls.stream().map(PluginsLoader::uncheckedToURI).map(PathUtils::get).toArray(Path[]::new); } - static final URI uncheckedToURI(URL url) { + static URI uncheckedToURI(URL url) { try { return url.toURI(); } catch (URISyntaxException e) { diff --git a/server/src/test/java/org/elasticsearch/plugins/PluginsLoaderTests.java b/server/src/test/java/org/elasticsearch/plugins/PluginsLoaderTests.java index 059cb15551acb..b7d63b7d612c9 100644 --- a/server/src/test/java/org/elasticsearch/plugins/PluginsLoaderTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/PluginsLoaderTests.java @@ -9,12 +9,45 @@ package org.elasticsearch.plugins; +import org.apache.lucene.tests.util.LuceneTestCase; +import org.elasticsearch.Version; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; +import org.elasticsearch.plugin.analysis.CharFilterFactory; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.PrivilegedOperations; +import org.elasticsearch.test.compiler.InMemoryJavaCompiler; +import org.elasticsearch.test.jar.JarUtils; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static java.util.Map.entry; +import static org.elasticsearch.test.LambdaMatchers.transformedMatch; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +@ESTestCase.WithoutSecurityManager +@LuceneTestCase.SuppressFileSystems(value = "ExtrasFS") public class PluginsLoaderTests extends ESTestCase { + private static final Logger logger = LogManager.getLogger(PluginsLoaderTests.class); + + static PluginsLoader newPluginsLoader(Settings settings) { + return PluginsLoader.createPluginsLoader(null, TestEnvironment.newEnvironment(settings).pluginsFile(), false); + } + public void testToModuleName() { assertThat(PluginsLoader.toModuleName("module.name"), equalTo("module.name")); assertThat(PluginsLoader.toModuleName("module-name"), equalTo("module.name")); @@ -28,4 +61,220 @@ public void testToModuleName() { assertThat(PluginsLoader.toModuleName("_module_name"), equalTo("_module_name")); assertThat(PluginsLoader.toModuleName("_"), equalTo("_")); } + + public void testStablePluginLoading() throws Exception { + final Path home = createTempDir(); + final Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), home).build(); + final Path plugins = home.resolve("plugins"); + final Path plugin = plugins.resolve("stable-plugin"); + Files.createDirectories(plugin); + PluginTestUtil.writeStablePluginProperties( + plugin, + "description", + "description", + "name", + "stable-plugin", + "version", + "1.0.0", + "elasticsearch.version", + Version.CURRENT.toString(), + "java.version", + System.getProperty("java.specification.version") + ); + + Path jar = plugin.resolve("impl.jar"); + JarUtils.createJarWithEntries(jar, Map.of("p/A.class", InMemoryJavaCompiler.compile("p.A", """ + package p; + import java.util.Map; + import org.elasticsearch.plugin.analysis.CharFilterFactory; + import org.elasticsearch.plugin.NamedComponent; + import java.io.Reader; + @NamedComponent( "a_name") + public class A implements CharFilterFactory { + @Override + public Reader create(Reader reader) { + return reader; + } + } + """))); + Path namedComponentFile = plugin.resolve("named_components.json"); + Files.writeString(namedComponentFile, """ + { + "org.elasticsearch.plugin.analysis.CharFilterFactory": { + "a_name": "p.A" + } + } + """); + + var pluginsLoader = newPluginsLoader(settings); + try { + var loadedLayers = pluginsLoader.pluginLayers().toList(); + + assertThat(loadedLayers, hasSize(1)); + assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().getName(), equalTo("stable-plugin")); + assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().isStable(), is(true)); + + assertThat(pluginsLoader.pluginDescriptors(), hasSize(1)); + assertThat(pluginsLoader.pluginDescriptors().get(0).getName(), equalTo("stable-plugin")); + assertThat(pluginsLoader.pluginDescriptors().get(0).isStable(), is(true)); + + var pluginClassLoader = loadedLayers.get(0).pluginClassLoader(); + var pluginModuleLayer = loadedLayers.get(0).pluginModuleLayer(); + assertThat(pluginClassLoader, instanceOf(UberModuleClassLoader.class)); + assertThat(pluginModuleLayer, is(not(ModuleLayer.boot()))); + assertThat(pluginModuleLayer.modules(), contains(transformedMatch(Module::getName, equalTo("synthetic.stable.plugin")))); + + if (CharFilterFactory.class.getModule().isNamed() == false) { + // test frameworks run with stable api classes on classpath, so we + // have no choice but to let our class read the unnamed module that + // owns the stable api classes + ((UberModuleClassLoader) pluginClassLoader).addReadsSystemClassLoaderUnnamedModule(); + } + + Class stableClass = pluginClassLoader.loadClass("p.A"); + assertThat(stableClass.getModule().getName(), equalTo("synthetic.stable.plugin")); + } finally { + closePluginLoaders(pluginsLoader); + } + } + + public void testModularPluginLoading() throws Exception { + final Path home = createTempDir(); + final Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), home).build(); + final Path plugins = home.resolve("plugins"); + final Path plugin = plugins.resolve("modular-plugin"); + Files.createDirectories(plugin); + PluginTestUtil.writePluginProperties( + plugin, + "description", + "description", + "name", + "modular-plugin", + "classname", + "p.A", + "modulename", + "modular.plugin", + "version", + "1.0.0", + "elasticsearch.version", + Version.CURRENT.toString(), + "java.version", + System.getProperty("java.specification.version") + ); + + Path jar = plugin.resolve("impl.jar"); + Map sources = Map.ofEntries(entry("module-info", "module modular.plugin { exports p; }"), entry("p.A", """ + package p; + import org.elasticsearch.plugins.Plugin; + + public class A extends Plugin { + } + """)); + + // Usually org.elasticsearch.plugins.Plugin would be in the org.elasticsearch.server module. + // Unfortunately, as tests run non-modular, it will be in the unnamed module, so we need to add a read for it. + var classToBytes = InMemoryJavaCompiler.compile(sources, "--add-reads", "modular.plugin=ALL-UNNAMED"); + + JarUtils.createJarWithEntries( + jar, + Map.ofEntries(entry("module-info.class", classToBytes.get("module-info")), entry("p/A.class", classToBytes.get("p.A"))) + ); + + var pluginsLoader = newPluginsLoader(settings); + try { + var loadedLayers = pluginsLoader.pluginLayers().toList(); + + assertThat(loadedLayers, hasSize(1)); + assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().getName(), equalTo("modular-plugin")); + assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().isStable(), is(false)); + assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().isModular(), is(true)); + + assertThat(pluginsLoader.pluginDescriptors(), hasSize(1)); + assertThat(pluginsLoader.pluginDescriptors().get(0).getName(), equalTo("modular-plugin")); + assertThat(pluginsLoader.pluginDescriptors().get(0).isModular(), is(true)); + + var pluginModuleLayer = loadedLayers.get(0).pluginModuleLayer(); + assertThat(pluginModuleLayer, is(not(ModuleLayer.boot()))); + assertThat(pluginModuleLayer.modules(), contains(transformedMatch(Module::getName, equalTo("modular.plugin")))); + } finally { + closePluginLoaders(pluginsLoader); + } + } + + public void testNonModularPluginLoading() throws Exception { + final Path home = createTempDir(); + final Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), home).build(); + final Path plugins = home.resolve("plugins"); + final Path plugin = plugins.resolve("non-modular-plugin"); + Files.createDirectories(plugin); + PluginTestUtil.writePluginProperties( + plugin, + "description", + "description", + "name", + "non-modular-plugin", + "classname", + "p.A", + "version", + "1.0.0", + "elasticsearch.version", + Version.CURRENT.toString(), + "java.version", + System.getProperty("java.specification.version") + ); + + Path jar = plugin.resolve("impl.jar"); + Map sources = Map.ofEntries(entry("p.A", """ + package p; + import org.elasticsearch.plugins.Plugin; + + public class A extends Plugin { + } + """)); + + var classToBytes = InMemoryJavaCompiler.compile(sources); + + JarUtils.createJarWithEntries(jar, Map.ofEntries(entry("p/A.class", classToBytes.get("p.A")))); + + var pluginsLoader = newPluginsLoader(settings); + try { + var loadedLayers = pluginsLoader.pluginLayers().toList(); + + assertThat(loadedLayers, hasSize(1)); + assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().getName(), equalTo("non-modular-plugin")); + assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().isStable(), is(false)); + assertThat(loadedLayers.get(0).pluginBundle().pluginDescriptor().isModular(), is(false)); + + assertThat(pluginsLoader.pluginDescriptors(), hasSize(1)); + assertThat(pluginsLoader.pluginDescriptors().get(0).getName(), equalTo("non-modular-plugin")); + assertThat(pluginsLoader.pluginDescriptors().get(0).isModular(), is(false)); + + var pluginModuleLayer = loadedLayers.get(0).pluginModuleLayer(); + assertThat(pluginModuleLayer, is(ModuleLayer.boot())); + } finally { + closePluginLoaders(pluginsLoader); + } + } + + // Closes the URLClassLoaders and UberModuleClassloaders created by the given plugin loader. + // We can use the direct ClassLoader from the plugin because tests do not use any parent SPI ClassLoaders. + static void closePluginLoaders(PluginsLoader pluginsLoader) { + pluginsLoader.pluginLayers().forEach(lp -> { + if (lp.pluginClassLoader() instanceof URLClassLoader urlClassLoader) { + try { + PrivilegedOperations.closeURLClassLoader(urlClassLoader); + } catch (IOException unexpected) { + throw new UncheckedIOException(unexpected); + } + } else if (lp.pluginClassLoader() instanceof UberModuleClassLoader loader) { + try { + PrivilegedOperations.closeURLClassLoader(loader.getInternalLoader()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + logger.info("Cannot close unexpected classloader " + lp.pluginClassLoader()); + } + }); + } } From c30ba12e6b679c31fc89a5a58d912b1c40f9ad52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Fred=C3=A9n?= <109296772+jfreden@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:59:10 +0100 Subject: [PATCH 35/77] Disable check_on_startup for KibanaUserRoleIntegTests (#118428) --- muted-tests.yml | 6 ------ .../integration/KibanaUserRoleIntegTests.java | 11 +++++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index 5c99a2d5a5efd..c91c7b50a0808 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -52,12 +52,6 @@ tests: - class: org.elasticsearch.backwards.MixedClusterClientYamlTestSuiteIT method: test {p0=mtermvectors/10_basic/Tests catching other exceptions per item} issue: https://github.com/elastic/elasticsearch/issues/113325 -- class: org.elasticsearch.integration.KibanaUserRoleIntegTests - method: testFieldMappings - issue: https://github.com/elastic/elasticsearch/issues/113592 -- class: org.elasticsearch.integration.KibanaUserRoleIntegTests - method: testSearchAndMSearch - issue: https://github.com/elastic/elasticsearch/issues/113593 - class: org.elasticsearch.xpack.transform.integration.TransformIT method: testStopWaitForCheckpoint issue: https://github.com/elastic/elasticsearch/issues/106113 diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/KibanaUserRoleIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/KibanaUserRoleIntegTests.java index 7d99d5817bdc0..bc730b5695c19 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/KibanaUserRoleIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/integration/KibanaUserRoleIntegTests.java @@ -14,13 +14,16 @@ import org.elasticsearch.action.search.MultiSearchResponse; import org.elasticsearch.cluster.metadata.MappingMetadata; import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Strings; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.test.NativeRealmIntegTestCase; import org.elasticsearch.test.SecuritySettingsSourceField; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import java.util.Map; +import java.util.Random; import static java.util.Collections.singletonMap; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertResponse; @@ -60,6 +63,14 @@ public String configUsersRoles() { return super.configUsersRoles() + "my_kibana_user:kibana_user\n" + "kibana_user:kibana_user"; } + @Override + protected Settings.Builder setRandomIndexSettings(Random random, Settings.Builder builder) { + // Prevent INDEX_CHECK_ON_STARTUP as a random setting since it could result in indices being checked for corruption before opening. + // When corruption is detected, it will prevent the shard from being opened. This check is expensive in terms of CPU and memory + // usage and causes intermittent CI failures due to timeout. + return super.setRandomIndexSettings(random, builder).put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), false); + } + public void testFieldMappings() throws Exception { final String index = "logstash-20-12-2015"; final String field = "foo"; From 671c7d7c766bf3dfa58317c75587ca7d49b6985f Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Thu, 12 Dec 2024 10:40:05 +0100 Subject: [PATCH 36/77] Adjust OldCodecAvailableTests for v9 (#118510) N-2 indices won't be covered by archive indices, but rather supported out-of-the-box. This commit reflects that in the tests expectations and adjusts the related update for v9 annotation. --- .../xpack/lucene/bwc/codecs/OldCodecsAvailableTests.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/old-lucene-versions/src/test/java/org/elasticsearch/xpack/lucene/bwc/codecs/OldCodecsAvailableTests.java b/x-pack/plugin/old-lucene-versions/src/test/java/org/elasticsearch/xpack/lucene/bwc/codecs/OldCodecsAvailableTests.java index 0b72b96b446d4..18cf1b49f5f37 100644 --- a/x-pack/plugin/old-lucene-versions/src/test/java/org/elasticsearch/xpack/lucene/bwc/codecs/OldCodecsAvailableTests.java +++ b/x-pack/plugin/old-lucene-versions/src/test/java/org/elasticsearch/xpack/lucene/bwc/codecs/OldCodecsAvailableTests.java @@ -8,7 +8,7 @@ package org.elasticsearch.xpack.lucene.bwc.codecs; import org.elasticsearch.Version; -import org.elasticsearch.core.UpdateForV9; +import org.elasticsearch.core.UpdateForV10; import org.elasticsearch.test.ESTestCase; public class OldCodecsAvailableTests extends ESTestCase { @@ -17,10 +17,8 @@ public class OldCodecsAvailableTests extends ESTestCase { * Reminder to add Lucene BWC codecs under {@link org.elasticsearch.xpack.lucene.bwc.codecs} whenever Elasticsearch is upgraded * to the next major Lucene version. */ - @UpdateForV9(owner = UpdateForV9.Owner.SEARCH_FOUNDATIONS) - @AwaitsFix(bugUrl = "muted until we add bwc codecs to support 7.x indices in Elasticsearch 9.0") + @UpdateForV10(owner = UpdateForV10.Owner.SEARCH_FOUNDATIONS) public void testLuceneBWCCodecsAvailable() { - assertEquals("Add Lucene BWC codecs for Elasticsearch version 7", 8, Version.CURRENT.major); + assertEquals("Add Lucene BWC codecs for Elasticsearch version 7", 9, Version.CURRENT.major); } - } From 8fb6edab636c462563dc2e7bfc1eab7f1ea8c4b5 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:52:41 +0100 Subject: [PATCH 37/77] [DOCS] Consolidate connectors release notes on one page (#118464) --- .../docs/connectors-release-notes.asciidoc | 74 ++++++++++++++++++- .../connectors-release-notes-8.16.0.asciidoc | 53 ------------- 2 files changed, 71 insertions(+), 56 deletions(-) delete mode 100644 docs/reference/connector/docs/release-notes/connectors-release-notes-8.16.0.asciidoc diff --git a/docs/reference/connector/docs/connectors-release-notes.asciidoc b/docs/reference/connector/docs/connectors-release-notes.asciidoc index e1ed082365c00..ff3d859e1a888 100644 --- a/docs/reference/connector/docs/connectors-release-notes.asciidoc +++ b/docs/reference/connector/docs/connectors-release-notes.asciidoc @@ -9,8 +9,76 @@ Prior to version *8.16.0*, the connector release notes were published as part of the {enterprise-search-ref}/changelog.html[Enterprise Search documentation]. ==== -*Release notes*: +[discrete] +[[es-connectors-release-notes-8-17-0]] +=== 8.17.0 -* <> +No notable changes in this release. -include::release-notes/connectors-release-notes-8.16.0.asciidoc[] +[discrete] +[[es-connectors-release-notes-8-16-1]] +=== 8.16.1 + +[discrete] +[[es-connectors-release-notes-8-16-1-bug-fixes]] +==== Bug fixes + +* Fixed a bug in the Outlook Connector where having deactivated users could cause the sync to fail. +See https://github.com/elastic/connectors/pull/2967[*PR 2967*]. +* Fixed a bug where the Confluence connector was not downloading some blog post documents due to unexpected response format. +See https://github.com/elastic/connectors/pull/2984[*PR 2984*]. + +[discrete] +[[es-connectors-release-notes-8-16-0]] +=== 8.16.0 + +[discrete] +[[es-connectors-release-notes-deprecation-notice]] +==== Deprecation notices + +* *Direct index access for connectors and sync jobs* ++ +IMPORTANT: Directly accessing connector and sync job state through `.elastic-connectors*` indices is deprecated, and will be disallowed entirely in a future release. + +* Instead, the Elasticsearch Connector APIs should be used. Connectors framework code now uses the <> by default. +See https://github.com/elastic/connectors/pull/2884[*PR 2902*]. + +* *Docker `enterprise-search` namespace deprecation* ++ +IMPORTANT: The `enterprise-search` Docker namespace is deprecated and will be discontinued in a future release. ++ +Starting in `8.16.0`, Docker images are being transitioned to the new `integrations` namespace, which will become the sole location for future releases. This affects the https://github.com/elastic/connectors[Elastic Connectors] and https://github.com/elastic/data-extraction-service[Elastic Data Extraction Service]. ++ +During this transition period, images are published to both namespaces: ++ +** *Example*: ++ +Deprecated namespace:: +`docker.elastic.co/enterprise-search/elastic-connectors:v8.16.0` ++ +New namespace:: +`docker.elastic.co/integrations/elastic-connectors:v8.16.0` ++ +Users should migrate to the new `integrations` namespace as soon as possible to ensure continued access to future releases. + +[discrete] +[[es-connectors-release-notes-8-16-0-enhancements]] +==== Enhancements + +* Docker images now use Chainguard's Wolfi base image (`docker.elastic.co/wolfi/jdk:openjdk-11-dev`), replacing the previous `ubuntu:focal` base. + +* The Sharepoint Online connector now works with the `Sites.Selected` permission instead of the broader permission `Sites.Read.All`. +See https://github.com/elastic/connectors/pull/2762[*PR 2762*]. + +* Starting in 8.16.0, connectors will start using proper SEMVER, with `MAJOR.MINOR.PATCH`, which aligns with Elasticsearch/Kibana versions. This drops the previous `.BUILD` suffix, which we used to release connectors between Elastic stack releases. Going forward, these inter-stack-release releases will be suffixed instead with `+`, aligning with Elastic Agent and conforming to SEMVER. +See https://github.com/elastic/connectors/pull/2749[*PR 2749*]. + +* Connector logs now use UTC timestamps, instead of machine-local timestamps. This only impacts logging output. +See https://github.com/elastic/connectors/pull/2695[*PR 2695*]. + +[discrete] +[[es-connectors-release-notes-8-16-0-bug-fixes]] +==== Bug fixes + +* The Dropbox connector now fetches the files from team shared folders. +See https://github.com/elastic/connectors/pull/2718[*PR 2718*]. diff --git a/docs/reference/connector/docs/release-notes/connectors-release-notes-8.16.0.asciidoc b/docs/reference/connector/docs/release-notes/connectors-release-notes-8.16.0.asciidoc deleted file mode 100644 index 7608336073176..0000000000000 --- a/docs/reference/connector/docs/release-notes/connectors-release-notes-8.16.0.asciidoc +++ /dev/null @@ -1,53 +0,0 @@ -[[es-connectors-release-notes-8-16-0]] -=== 8.16.0 connectors release notes - -[discrete] -[[es-connectors-release-notes-deprecation-notice]] -==== Deprecation notices - -* *Direct index access for connectors and sync jobs* -+ -IMPORTANT: Directly accessing connector and sync job state through `.elastic-connectors*` indices is deprecated, and will be disallowed entirely in a future release. - -* Instead, the Elasticsearch Connector APIs should be used. Connectors framework code now uses the <> by default. -See https://github.com/elastic/connectors/pull/2884[*PR 2902*]. - -* *Docker `enterprise-search` namespace deprecation* -+ -IMPORTANT: The `enterprise-search` Docker namespace is deprecated and will be discontinued in a future release. -+ -Starting in `8.16.0`, Docker images are being transitioned to the new `integrations` namespace, which will become the sole location for future releases. This affects the https://github.com/elastic/connectors[Elastic Connectors] and https://github.com/elastic/data-extraction-service[Elastic Data Extraction Service]. -+ -During this transition period, images are published to both namespaces: -+ -** *Example*: -+ -Deprecated namespace:: -`docker.elastic.co/enterprise-search/elastic-connectors:v8.16.0` -+ -New namespace:: -`docker.elastic.co/integrations/elastic-connectors:v8.16.0` -+ -Users should migrate to the new `integrations` namespace as soon as possible to ensure continued access to future releases. - -[discrete] -[[es-connectors-release-notes-8-16-0-enhancements]] -==== Enhancements - -* Docker images now use Chainguard's Wolfi base image (`docker.elastic.co/wolfi/jdk:openjdk-11-dev`), replacing the previous `ubuntu:focal` base. - -* The Sharepoint Online connector now works with the `Sites.Selected` permission instead of the broader permission `Sites.Read.All`. -See https://github.com/elastic/connectors/pull/2762[*PR 2762*]. - -* Starting in 8.16.0, connectors will start using proper SEMVER, with `MAJOR.MINOR.PATCH`, which aligns with Elasticsearch/Kibana versions. This drops the previous `.BUILD` suffix, which we used to release connectors between Elastic stack releases. Going forward, these inter-stack-release releases will be suffixed instead with `+`, aligning with Elastic Agent and conforming to SEMVER. -See https://github.com/elastic/connectors/pull/2749[*PR 2749*]. - -* Connector logs now use UTC timestamps, instead of machine-local timestamps. This only impacts logging output. -See https://github.com/elastic/connectors/pull/2695[*PR 2695*]. - -[discrete] -[[es-connectors-release-notes-8-16-0-bug-fixes]] -==== Bug fixes - -* The Dropbox connector now fetches the files from team shared folders. -See https://github.com/elastic/connectors/pull/2718[*PR 2718*]. \ No newline at end of file From 8a4fc7ceca1b853be06cba812fcf3da9b29f9606 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Thu, 12 Dec 2024 10:02:01 +0000 Subject: [PATCH 38/77] Remove most uses of 7.5 and 7.6 transport versions (#118439) --- .../index/rankeval/RankEvalRequest.java | 9 +--- .../elasticsearch/ElasticsearchException.java | 9 +--- .../org/elasticsearch/TransportVersions.java | 1 - ...ransportNodesListGatewayStartedShards.java | 12 +---- .../transport/RemoteConnectionInfo.java | 48 ++++--------------- .../StringStatsAggregationBuilder.java | 2 +- .../xpack/core/ccr/AutoFollowMetadata.java | 10 +--- .../core/enrich/EnrichFeatureSetUsage.java | 2 +- .../xpack/core/enrich/EnrichMetadata.java | 2 +- .../security/SecurityFeatureSetUsage.java | 8 +--- .../SamlPrepareAuthenticationRequest.java | 9 +--- .../xpack/core/slm/SLMFeatureSetUsage.java | 2 +- .../transform/TransformFeatureSetUsage.java | 2 +- .../transforms/TransformStateTests.java | 38 --------------- 14 files changed, 27 insertions(+), 127 deletions(-) diff --git a/modules/rank-eval/src/main/java/org/elasticsearch/index/rankeval/RankEvalRequest.java b/modules/rank-eval/src/main/java/org/elasticsearch/index/rankeval/RankEvalRequest.java index 8e0d838e602e7..4bb30fdb0dd01 100644 --- a/modules/rank-eval/src/main/java/org/elasticsearch/index/rankeval/RankEvalRequest.java +++ b/modules/rank-eval/src/main/java/org/elasticsearch/index/rankeval/RankEvalRequest.java @@ -9,7 +9,6 @@ package org.elasticsearch.index.rankeval; -import org.elasticsearch.TransportVersions; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.IndicesRequest; @@ -46,9 +45,7 @@ public RankEvalRequest(RankEvalSpec rankingEvaluationSpec, String[] indices) { rankingEvaluationSpec = new RankEvalSpec(in); indices = in.readStringArray(); indicesOptions = IndicesOptions.readIndicesOptions(in); - if (in.getTransportVersion().onOrAfter(TransportVersions.V_7_6_0)) { - searchType = SearchType.fromId(in.readByte()); - } + searchType = SearchType.fromId(in.readByte()); } RankEvalRequest() {} @@ -127,9 +124,7 @@ public void writeTo(StreamOutput out) throws IOException { rankingEvaluationSpec.writeTo(out); out.writeStringArray(indices); indicesOptions.writeIndicesOptions(out); - if (out.getTransportVersion().onOrAfter(TransportVersions.V_7_6_0)) { - out.writeByte(searchType.id()); - } + out.writeByte(searchType.id()); } @Override diff --git a/server/src/main/java/org/elasticsearch/ElasticsearchException.java b/server/src/main/java/org/elasticsearch/ElasticsearchException.java index 11736bfe07deb..a430611559bb4 100644 --- a/server/src/main/java/org/elasticsearch/ElasticsearchException.java +++ b/server/src/main/java/org/elasticsearch/ElasticsearchException.java @@ -40,7 +40,6 @@ import org.elasticsearch.persistent.PersistentTaskNodeNotAssignedException; import org.elasticsearch.rest.ApiNotAvailableException; import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.search.SearchException; import org.elasticsearch.search.TooManyScrollContextsException; import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.MultiBucketConsumerService; @@ -319,10 +318,6 @@ protected void writeTo(StreamOutput out, Writer nestedExceptionsWrite public static ElasticsearchException readException(StreamInput input, int id) throws IOException { CheckedFunction elasticsearchException = ID_TO_SUPPLIER.get(id); if (elasticsearchException == null) { - if (id == 127 && input.getTransportVersion().before(TransportVersions.V_7_5_0)) { - // was SearchContextException - return new SearchException(input); - } throw new IllegalStateException("unknown exception for id: " + id); } return elasticsearchException.apply(input); @@ -1817,13 +1812,13 @@ private enum ElasticsearchExceptionHandle { org.elasticsearch.index.seqno.RetentionLeaseInvalidRetainingSeqNoException.class, org.elasticsearch.index.seqno.RetentionLeaseInvalidRetainingSeqNoException::new, 156, - TransportVersions.V_7_5_0 + UNKNOWN_VERSION_ADDED ), INGEST_PROCESSOR_EXCEPTION( org.elasticsearch.ingest.IngestProcessorException.class, org.elasticsearch.ingest.IngestProcessorException::new, 157, - TransportVersions.V_7_5_0 + UNKNOWN_VERSION_ADDED ), PEER_RECOVERY_NOT_FOUND_EXCEPTION( org.elasticsearch.indices.recovery.PeerRecoveryNotFound.class, diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index d61afbdf98587..ac083862357d6 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -54,7 +54,6 @@ static TransportVersion def(int id) { public static final TransportVersion V_7_0_0 = def(7_00_00_99); public static final TransportVersion V_7_3_0 = def(7_03_00_99); public static final TransportVersion V_7_4_0 = def(7_04_00_99); - public static final TransportVersion V_7_5_0 = def(7_05_00_99); public static final TransportVersion V_7_6_0 = def(7_06_00_99); public static final TransportVersion V_7_7_0 = def(7_07_00_99); public static final TransportVersion V_7_8_0 = def(7_08_00_99); diff --git a/server/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayStartedShards.java b/server/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayStartedShards.java index 737b904849835..c3c33d5de9a00 100644 --- a/server/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayStartedShards.java +++ b/server/src/main/java/org/elasticsearch/gateway/TransportNodesListGatewayStartedShards.java @@ -12,7 +12,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.TransportVersions; import org.elasticsearch.action.ActionType; import org.elasticsearch.action.FailedNodeException; import org.elasticsearch.action.support.ActionFilters; @@ -239,11 +238,7 @@ public static class NodeRequest extends TransportRequest { public NodeRequest(StreamInput in) throws IOException { super(in); shardId = new ShardId(in); - if (in.getTransportVersion().onOrAfter(TransportVersions.V_7_6_0)) { - customDataPath = in.readString(); - } else { - customDataPath = null; - } + customDataPath = in.readString(); } public NodeRequest(Request request) { @@ -255,10 +250,7 @@ public NodeRequest(Request request) { public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); shardId.writeTo(out); - if (out.getTransportVersion().onOrAfter(TransportVersions.V_7_6_0)) { - assert customDataPath != null; - out.writeString(customDataPath); - } + out.writeString(customDataPath); } public ShardId getShardId() { diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java index 7a579cbb98bc2..ae078479ba85c 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteConnectionInfo.java @@ -18,8 +18,6 @@ import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; -import java.util.Arrays; -import java.util.List; import java.util.Objects; /** @@ -49,25 +47,14 @@ public RemoteConnectionInfo( } public RemoteConnectionInfo(StreamInput input) throws IOException { - if (input.getTransportVersion().onOrAfter(TransportVersions.V_7_6_0)) { - RemoteConnectionStrategy.ConnectionStrategy mode = input.readEnum(RemoteConnectionStrategy.ConnectionStrategy.class); - modeInfo = mode.getReader().read(input); - initialConnectionTimeout = input.readTimeValue(); - clusterAlias = input.readString(); - skipUnavailable = input.readBoolean(); - if (input.getTransportVersion().onOrAfter(TransportVersions.V_8_8_0)) { - hasClusterCredentials = input.readBoolean(); - } else { - hasClusterCredentials = false; - } + RemoteConnectionStrategy.ConnectionStrategy mode = input.readEnum(RemoteConnectionStrategy.ConnectionStrategy.class); + modeInfo = mode.getReader().read(input); + initialConnectionTimeout = input.readTimeValue(); + clusterAlias = input.readString(); + skipUnavailable = input.readBoolean(); + if (input.getTransportVersion().onOrAfter(TransportVersions.V_8_8_0)) { + hasClusterCredentials = input.readBoolean(); } else { - List seedNodes = Arrays.asList(input.readStringArray()); - int connectionsPerCluster = input.readVInt(); - initialConnectionTimeout = input.readTimeValue(); - int numNodesConnected = input.readVInt(); - clusterAlias = input.readString(); - skipUnavailable = input.readBoolean(); - modeInfo = new SniffConnectionStrategy.SniffModeInfo(seedNodes, connectionsPerCluster, numNodesConnected); hasClusterCredentials = false; } } @@ -90,24 +77,9 @@ public boolean hasClusterCredentials() { @Override public void writeTo(StreamOutput out) throws IOException { - if (out.getTransportVersion().onOrAfter(TransportVersions.V_7_6_0)) { - out.writeEnum(modeInfo.modeType()); - modeInfo.writeTo(out); - out.writeTimeValue(initialConnectionTimeout); - } else { - if (modeInfo.modeType() == RemoteConnectionStrategy.ConnectionStrategy.SNIFF) { - SniffConnectionStrategy.SniffModeInfo sniffInfo = (SniffConnectionStrategy.SniffModeInfo) this.modeInfo; - out.writeStringCollection(sniffInfo.seedNodes); - out.writeVInt(sniffInfo.maxConnectionsPerCluster); - out.writeTimeValue(initialConnectionTimeout); - out.writeVInt(sniffInfo.numNodesConnected); - } else { - out.writeStringArray(new String[0]); - out.writeVInt(0); - out.writeTimeValue(initialConnectionTimeout); - out.writeVInt(0); - } - } + out.writeEnum(modeInfo.modeType()); + modeInfo.writeTo(out); + out.writeTimeValue(initialConnectionTimeout); out.writeString(clusterAlias); out.writeBoolean(skipUnavailable); if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_8_0)) { diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/StringStatsAggregationBuilder.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/StringStatsAggregationBuilder.java index c75ed46102112..ea5a0adffa0eb 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/StringStatsAggregationBuilder.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/stringstats/StringStatsAggregationBuilder.java @@ -154,6 +154,6 @@ public boolean equals(Object obj) { @Override public TransportVersion getMinimalSupportedVersion() { - return TransportVersions.V_7_6_0; + return TransportVersions.ZERO; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java index 6b54f7a7dddce..5276d7659fb02 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java @@ -282,11 +282,7 @@ private AutoFollowPattern( this.leaderIndexPatterns = leaderIndexPatterns; this.followIndexPattern = followIndexPattern; this.settings = Objects.requireNonNull(settings); - if (in.getTransportVersion().onOrAfter(TransportVersions.V_7_5_0)) { - this.active = in.readBoolean(); - } else { - this.active = true; - } + this.active = in.readBoolean(); if (in.getTransportVersion().onOrAfter(TransportVersions.V_7_14_0)) { this.leaderIndexExclusionPatterns = in.readStringCollectionAsList(); } else { @@ -351,9 +347,7 @@ public void writeTo(StreamOutput out) throws IOException { settings.writeTo(out); } super.writeTo(out); - if (out.getTransportVersion().onOrAfter(TransportVersions.V_7_5_0)) { - out.writeBoolean(active); - } + out.writeBoolean(active); if (out.getTransportVersion().onOrAfter(TransportVersions.V_7_14_0)) { out.writeStringCollection(leaderIndexExclusionPatterns); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrich/EnrichFeatureSetUsage.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrich/EnrichFeatureSetUsage.java index 819b3d86b68c8..b51fa386ddeea 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrich/EnrichFeatureSetUsage.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrich/EnrichFeatureSetUsage.java @@ -27,6 +27,6 @@ public EnrichFeatureSetUsage(StreamInput input) throws IOException { @Override public TransportVersion getMinimalSupportedVersion() { - return TransportVersions.V_7_5_0; + return TransportVersions.ZERO; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrich/EnrichMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrich/EnrichMetadata.java index b949e44ef036a..30ecaf2ff680f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrich/EnrichMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrich/EnrichMetadata.java @@ -84,7 +84,7 @@ public EnumSet context() { @Override public TransportVersion getMinimalSupportedVersion() { - return TransportVersions.V_7_5_0; + return TransportVersions.ZERO; } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java index 726797d2e563a..f44409daa37f8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityFeatureSetUsage.java @@ -61,9 +61,7 @@ public SecurityFeatureSetUsage(StreamInput in) throws IOException { ipFilterUsage = in.readGenericMap(); anonymousUsage = in.readGenericMap(); roleMappingStoreUsage = in.readGenericMap(); - if (in.getTransportVersion().onOrAfter(TransportVersions.V_7_5_0)) { - fips140Usage = in.readGenericMap(); - } + fips140Usage = in.readGenericMap(); if (in.getTransportVersion().onOrAfter(TransportVersions.V_7_11_0)) { operatorPrivilegesUsage = in.readGenericMap(); } @@ -129,9 +127,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeGenericMap(ipFilterUsage); out.writeGenericMap(anonymousUsage); out.writeGenericMap(roleMappingStoreUsage); - if (out.getTransportVersion().onOrAfter(TransportVersions.V_7_5_0)) { - out.writeGenericMap(fips140Usage); - } + out.writeGenericMap(fips140Usage); if (out.getTransportVersion().onOrAfter(TransportVersions.V_7_11_0)) { out.writeGenericMap(operatorPrivilegesUsage); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlPrepareAuthenticationRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlPrepareAuthenticationRequest.java index 3b5ddb21be91c..067bb156aa909 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlPrepareAuthenticationRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/saml/SamlPrepareAuthenticationRequest.java @@ -6,7 +6,6 @@ */ package org.elasticsearch.xpack.core.security.action.saml; -import org.elasticsearch.TransportVersions; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.io.stream.StreamInput; @@ -33,9 +32,7 @@ public SamlPrepareAuthenticationRequest(StreamInput in) throws IOException { super(in); realmName = in.readOptionalString(); assertionConsumerServiceURL = in.readOptionalString(); - if (in.getTransportVersion().onOrAfter(TransportVersions.V_7_5_0)) { - relayState = in.readOptionalString(); - } + relayState = in.readOptionalString(); } public SamlPrepareAuthenticationRequest() {} @@ -87,8 +84,6 @@ public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); out.writeOptionalString(realmName); out.writeOptionalString(assertionConsumerServiceURL); - if (out.getTransportVersion().onOrAfter(TransportVersions.V_7_5_0)) { - out.writeOptionalString(relayState); - } + out.writeOptionalString(relayState); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SLMFeatureSetUsage.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SLMFeatureSetUsage.java index 099eaa2468e1c..53b45827c4a9f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SLMFeatureSetUsage.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/slm/SLMFeatureSetUsage.java @@ -41,7 +41,7 @@ public SLMFeatureSetUsage(@Nullable SnapshotLifecycleStats slmStats) { @Override public TransportVersion getMinimalSupportedVersion() { - return TransportVersions.V_7_5_0; + return TransportVersions.ZERO; } public SnapshotLifecycleStats getStats() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformFeatureSetUsage.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformFeatureSetUsage.java index e4c15a3b9007c..909bf6858eab0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformFeatureSetUsage.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/transform/TransformFeatureSetUsage.java @@ -50,7 +50,7 @@ public TransformFeatureSetUsage( @Override public TransportVersion getMinimalSupportedVersion() { - return TransportVersions.V_7_5_0; + return TransportVersions.ZERO; } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformStateTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformStateTests.java index e01549032be5e..0acf46edc4bdd 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformStateTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/TransformStateTests.java @@ -7,9 +7,6 @@ package org.elasticsearch.xpack.core.transform.transforms; -import org.elasticsearch.TransportVersions; -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.Writeable.Reader; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; @@ -66,39 +63,4 @@ protected boolean supportsUnknownFields() { protected Predicate getRandomFieldsExcludeFilter() { return field -> field.isEmpty() == false; } - - public void testBackwardsSerialization() throws IOException { - TransformState state = new TransformState( - randomFrom(TransformTaskState.values()), - randomFrom(IndexerState.values()), - TransformIndexerPositionTests.randomTransformIndexerPosition(), - randomLongBetween(0, 10), - randomBoolean() ? null : randomAlphaOfLength(10), - randomBoolean() ? null : randomTransformProgress(), - randomBoolean() ? null : randomNodeAttributes(), - false, - randomBoolean() ? null : AuthorizationStateTests.randomAuthorizationState() - ); - // auth_state will be null after BWC deserialization - TransformState expectedState = new TransformState( - state.getTaskState(), - state.getIndexerState(), - state.getPosition(), - state.getCheckpoint(), - state.getReason(), - state.getProgress(), - state.getNode(), - state.shouldStopAtNextCheckpoint(), - null - ); - try (BytesStreamOutput output = new BytesStreamOutput()) { - output.setTransportVersion(TransportVersions.V_7_5_0); - state.writeTo(output); - try (StreamInput in = output.bytes().streamInput()) { - in.setTransportVersion(TransportVersions.V_7_5_0); - TransformState streamedState = new TransformState(in); - assertEquals(expectedState, streamedState); - } - } - } } From 8bbc6b314149163be3fa26d7f9066cc79f68a866 Mon Sep 17 00:00:00 2001 From: Mary Gouseti Date: Thu, 12 Dec 2024 12:05:45 +0200 Subject: [PATCH 39/77] Suppress the for-loop warnings since it is a conscious performance choice. (#118530) --- .../cluster/metadata/IndexNameExpressionResolver.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java index e7914d812e05c..2ce91b66fa789 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -66,6 +66,7 @@ * Note: This class is performance sensitive, so we pay extra attention on the data structure usage and we avoid streams and iterators * when possible in favor of the classic for-i loops. */ +@SuppressWarnings("ForLoopReplaceableByForEach") public class IndexNameExpressionResolver { private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(IndexNameExpressionResolver.class); From 3cf7f9714189b514429948d45455d558dd0f4bf7 Mon Sep 17 00:00:00 2001 From: kosabogi <105062005+kosabogi@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:17:16 +0100 Subject: [PATCH 40/77] Adds CCS matrix for 8.17 (#118527) Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> --- .../ccs-version-compat-matrix.asciidoc | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/docs/reference/search/search-your-data/ccs-version-compat-matrix.asciidoc b/docs/reference/search/search-your-data/ccs-version-compat-matrix.asciidoc index 5859ccd03e511..a68f20fb1c656 100644 --- a/docs/reference/search/search-your-data/ccs-version-compat-matrix.asciidoc +++ b/docs/reference/search/search-your-data/ccs-version-compat-matrix.asciidoc @@ -1,25 +1,26 @@ |==== -| 20+^h| Remote cluster version +| 21+^h| Remote cluster version h| Local cluster version - | 6.8 | 7.1–7.16 | 7.17 | 8.0 | 8.1 | 8.2 | 8.3 | 8.4 | 8.5 | 8.6 | 8.7 | 8.8 | 8.9 | 8.10 | 8.11 | 8.12 | 8.13 | 8.14 | 8.15 | 8.16 -| 6.8 | {yes-icon} | {yes-icon} | {yes-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} -| 7.1–7.16 | {yes-icon} | {yes-icon} | {yes-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} -| 7.17 | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon}| {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.0 | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon}| {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.1 | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon}| {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.2 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon}| {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.3 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon}| {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.4 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon}| {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.5 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.6 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.7 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.8 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.9 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.10 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.11 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.12 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.13 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.14 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.15 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} -| 8.16 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} + | 6.8 | 7.1–7.16 | 7.17 | 8.0 | 8.1 | 8.2 | 8.3 | 8.4 | 8.5 | 8.6 | 8.7 | 8.8 | 8.9 | 8.10 | 8.11 | 8.12 | 8.13 | 8.14 | 8.15 | 8.16 | 8.17 +| 6.8 | {yes-icon} | {yes-icon} | {yes-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} +| 7.1–7.16 | {yes-icon} | {yes-icon} | {yes-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} +| 7.17 | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon}| {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.0 | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon}| {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.1 | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon}| {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.2 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon}| {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.3 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon}| {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.4 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon}| {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.5 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon}| {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.6 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon}| {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.7 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.8 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.9 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.10 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.11 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.12 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.13 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.14 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.15 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.16 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} | {yes-icon} +| 8.17 | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {no-icon} | {yes-icon} | {yes-icon} |==== From a8484ad8efe78be8e6a95e8cc37ed5f96098c469 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Thu, 12 Dec 2024 10:21:05 +0000 Subject: [PATCH 41/77] [ML] Fix timeout ingesting an empty string into a semantic_text field (#117840) --- docs/changelog/117840.yaml | 5 ++ .../chunking/SentenceBoundaryChunker.java | 8 ++- .../chunking/WordBoundaryChunker.java | 4 -- .../EmbeddingRequestChunkerTests.java | 51 ++++++++++++++++++- .../SentenceBoundaryChunkerTests.java | 35 +++++++++++++ .../chunking/WordBoundaryChunkerTests.java | 34 +++++++++++-- .../xpack/ml/integration/PyTorchModelIT.java | 16 ++++++ .../TransportInternalInferModelAction.java | 5 ++ 8 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 docs/changelog/117840.yaml diff --git a/docs/changelog/117840.yaml b/docs/changelog/117840.yaml new file mode 100644 index 0000000000000..e1f469643af42 --- /dev/null +++ b/docs/changelog/117840.yaml @@ -0,0 +1,5 @@ +pr: 117840 +summary: Fix timeout ingesting an empty string into a `semantic_text` field +area: Machine Learning +type: bug +issues: [] diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunker.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunker.java index b2d6c83b89211..bf28e30074a9d 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunker.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunker.java @@ -62,7 +62,8 @@ public List chunk(String input, ChunkingSettings chunkingSettings) * * @param input Text to chunk * @param maxNumberWordsPerChunk Maximum size of the chunk - * @return The input text chunked + * @param includePrecedingSentence Include the previous sentence + * @return The input text offsets */ public List chunk(String input, int maxNumberWordsPerChunk, boolean includePrecedingSentence) { var chunks = new ArrayList(); @@ -158,6 +159,11 @@ public List chunk(String input, int maxNumberWordsPerChunk, boolean chunks.add(new ChunkOffset(chunkStart, input.length())); } + if (chunks.isEmpty()) { + // The input did not chunk, return the entire input + chunks.add(new ChunkOffset(0, input.length())); + } + return chunks; } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunker.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunker.java index b15e2134f4cf7..1ce90a9e416e5 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunker.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunker.java @@ -96,10 +96,6 @@ List chunkPositions(String input, int chunkSize, int overlap) { throw new IllegalArgumentException("Invalid chunking parameters, overlap [" + overlap + "] must be >= 0"); } - if (input.isEmpty()) { - return List.of(); - } - var chunkPositions = new ArrayList(); // This position in the chunk is where the next overlapping chunk will start diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/EmbeddingRequestChunkerTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/EmbeddingRequestChunkerTests.java index a82d2f474ca4a..dec7d15760aa6 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/EmbeddingRequestChunkerTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/EmbeddingRequestChunkerTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.core.inference.results.InferenceTextEmbeddingFloatResults; import org.elasticsearch.xpack.core.inference.results.SparseEmbeddingResults; import org.elasticsearch.xpack.core.ml.search.WeightedToken; +import org.hamcrest.Matchers; import java.util.ArrayList; import java.util.List; @@ -31,16 +32,62 @@ public class EmbeddingRequestChunkerTests extends ESTestCase { - public void testEmptyInput() { + public void testEmptyInput_WordChunker() { var embeddingType = randomFrom(EmbeddingRequestChunker.EmbeddingType.values()); var batches = new EmbeddingRequestChunker(List.of(), 100, 100, 10, embeddingType).batchRequestsWithListeners(testListener()); assertThat(batches, empty()); } - public void testBlankInput() { + public void testEmptyInput_SentenceChunker() { + var embeddingType = randomFrom(EmbeddingRequestChunker.EmbeddingType.values()); + var batches = new EmbeddingRequestChunker(List.of(), 10, embeddingType, new SentenceBoundaryChunkingSettings(250, 1)) + .batchRequestsWithListeners(testListener()); + assertThat(batches, empty()); + } + + public void testWhitespaceInput_SentenceChunker() { + var embeddingType = randomFrom(EmbeddingRequestChunker.EmbeddingType.values()); + var batches = new EmbeddingRequestChunker(List.of(" "), 10, embeddingType, new SentenceBoundaryChunkingSettings(250, 1)) + .batchRequestsWithListeners(testListener()); + assertThat(batches, hasSize(1)); + assertThat(batches.get(0).batch().inputs(), hasSize(1)); + assertThat(batches.get(0).batch().inputs().get(0), Matchers.is(" ")); + } + + public void testBlankInput_WordChunker() { var embeddingType = randomFrom(EmbeddingRequestChunker.EmbeddingType.values()); var batches = new EmbeddingRequestChunker(List.of(""), 100, 100, 10, embeddingType).batchRequestsWithListeners(testListener()); assertThat(batches, hasSize(1)); + assertThat(batches.get(0).batch().inputs(), hasSize(1)); + assertThat(batches.get(0).batch().inputs().get(0), Matchers.is("")); + } + + public void testBlankInput_SentenceChunker() { + var embeddingType = randomFrom(EmbeddingRequestChunker.EmbeddingType.values()); + var batches = new EmbeddingRequestChunker(List.of(""), 10, embeddingType, new SentenceBoundaryChunkingSettings(250, 1)) + .batchRequestsWithListeners(testListener()); + assertThat(batches, hasSize(1)); + assertThat(batches.get(0).batch().inputs(), hasSize(1)); + assertThat(batches.get(0).batch().inputs().get(0), Matchers.is("")); + } + + public void testInputThatDoesNotChunk_WordChunker() { + var embeddingType = randomFrom(EmbeddingRequestChunker.EmbeddingType.values()); + var batches = new EmbeddingRequestChunker(List.of("ABBAABBA"), 100, 100, 10, embeddingType).batchRequestsWithListeners( + testListener() + ); + assertThat(batches, hasSize(1)); + assertThat(batches.get(0).batch().inputs(), hasSize(1)); + assertThat(batches.get(0).batch().inputs().get(0), Matchers.is("ABBAABBA")); + } + + public void testInputThatDoesNotChunk_SentenceChunker() { + var embeddingType = randomFrom(EmbeddingRequestChunker.EmbeddingType.values()); + var batches = new EmbeddingRequestChunker(List.of("ABBAABBA"), 10, embeddingType, new SentenceBoundaryChunkingSettings(250, 1)) + .batchRequestsWithListeners(testListener()); + assertThat(batches, hasSize(1)); + assertThat(batches.get(0).batch().inputs(), hasSize(1)); + assertThat(batches.get(0).batch().inputs().get(0), Matchers.is("ABBAABBA")); } public void testShortInputsAreSingleBatch() { diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunkerTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunkerTests.java index de943f7f57ab8..f81894ccd4bbb 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunkerTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/SentenceBoundaryChunkerTests.java @@ -43,6 +43,41 @@ private List textChunks( return chunkPositions.stream().map(offset -> input.substring(offset.start(), offset.end())).collect(Collectors.toList()); } + public void testEmptyString() { + var chunks = textChunks(new SentenceBoundaryChunker(), "", 100, randomBoolean()); + assertThat(chunks, hasSize(1)); + assertThat(chunks.get(0), Matchers.is("")); + } + + public void testBlankString() { + var chunks = textChunks(new SentenceBoundaryChunker(), " ", 100, randomBoolean()); + assertThat(chunks, hasSize(1)); + assertThat(chunks.get(0), Matchers.is(" ")); + } + + public void testSingleChar() { + var chunks = textChunks(new SentenceBoundaryChunker(), " b", 100, randomBoolean()); + assertThat(chunks, Matchers.contains(" b")); + + chunks = textChunks(new SentenceBoundaryChunker(), "b", 100, randomBoolean()); + assertThat(chunks, Matchers.contains("b")); + + chunks = textChunks(new SentenceBoundaryChunker(), ". ", 100, randomBoolean()); + assertThat(chunks, Matchers.contains(". ")); + + chunks = textChunks(new SentenceBoundaryChunker(), " , ", 100, randomBoolean()); + assertThat(chunks, Matchers.contains(" , ")); + + chunks = textChunks(new SentenceBoundaryChunker(), " ,", 100, randomBoolean()); + assertThat(chunks, Matchers.contains(" ,")); + } + + public void testSingleCharRepeated() { + var input = "a".repeat(32_000); + var chunks = textChunks(new SentenceBoundaryChunker(), input, 100, randomBoolean()); + assertThat(chunks, Matchers.contains(input)); + } + public void testChunkSplitLargeChunkSizes() { for (int maxWordsPerChunk : new int[] { 100, 200 }) { var chunker = new SentenceBoundaryChunker(); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunkerTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunkerTests.java index 2ef28f2cf2e77..b4fa5c9122258 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunkerTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/chunking/WordBoundaryChunkerTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.inference.ChunkingSettings; import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; import java.util.List; import java.util.Locale; @@ -71,10 +72,6 @@ public class WordBoundaryChunkerTests extends ESTestCase { * Use the chunk functions that return offsets where possible */ List textChunks(WordBoundaryChunker chunker, String input, int chunkSize, int overlap) { - if (input.isEmpty()) { - return List.of(""); - } - var chunkPositions = chunker.chunk(input, chunkSize, overlap); return chunkPositions.stream().map(p -> input.substring(p.start(), p.end())).collect(Collectors.toList()); } @@ -240,6 +237,35 @@ public void testWhitespace() { assertThat(chunks, contains(" ")); } + public void testBlankString() { + var chunks = textChunks(new WordBoundaryChunker(), " ", 100, 10); + assertThat(chunks, hasSize(1)); + assertThat(chunks.get(0), Matchers.is(" ")); + } + + public void testSingleChar() { + var chunks = textChunks(new WordBoundaryChunker(), " b", 100, 10); + assertThat(chunks, Matchers.contains(" b")); + + chunks = textChunks(new WordBoundaryChunker(), "b", 100, 10); + assertThat(chunks, Matchers.contains("b")); + + chunks = textChunks(new WordBoundaryChunker(), ". ", 100, 10); + assertThat(chunks, Matchers.contains(". ")); + + chunks = textChunks(new WordBoundaryChunker(), " , ", 100, 10); + assertThat(chunks, Matchers.contains(" , ")); + + chunks = textChunks(new WordBoundaryChunker(), " ,", 100, 10); + assertThat(chunks, Matchers.contains(" ,")); + } + + public void testSingleCharRepeated() { + var input = "a".repeat(32_000); + var chunks = textChunks(new WordBoundaryChunker(), input, 100, 10); + assertThat(chunks, Matchers.contains(input)); + } + public void testPunctuation() { int chunkSize = 1; var chunks = textChunks(new WordBoundaryChunker(), "Comma, separated", chunkSize, 0); diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelIT.java index 4e92cad1026a3..04f349d67d7fe 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/PyTorchModelIT.java @@ -1142,6 +1142,22 @@ public void testDeploymentThreadsIncludedInUsage() throws IOException { } } + public void testInferEmptyInput() throws IOException { + String modelId = "empty_input"; + createPassThroughModel(modelId); + putModelDefinition(modelId); + putVocabulary(List.of("these", "are", "my", "words"), modelId); + startDeployment(modelId); + + Request request = new Request("POST", "/_ml/trained_models/" + modelId + "/_infer?timeout=30s"); + request.setJsonEntity(""" + { "docs": [] } + """); + + var inferenceResponse = client().performRequest(request); + assertThat(EntityUtils.toString(inferenceResponse.getEntity()), equalTo("{\"inference_results\":[]}")); + } + private void putModelDefinition(String modelId) throws IOException { putModelDefinition(modelId, BASE_64_ENCODED_MODEL, RAW_MODEL_SIZE); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportInternalInferModelAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportInternalInferModelAction.java index e0405b1749536..20a4ceeae59b3 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportInternalInferModelAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportInternalInferModelAction.java @@ -132,6 +132,11 @@ protected void doExecute(Task task, Request request, ActionListener li Response.Builder responseBuilder = Response.builder(); TaskId parentTaskId = new TaskId(clusterService.localNode().getId(), task.getId()); + if (request.numberOfDocuments() == 0) { + listener.onResponse(responseBuilder.setId(request.getId()).build()); + return; + } + if (MachineLearning.INFERENCE_AGG_FEATURE.check(licenseState)) { responseBuilder.setLicensed(true); doInfer(task, request, responseBuilder, parentTaskId, listener); From a975927320fc5660b67388fbe636f8b332ad4ef5 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 12 Dec 2024 10:57:57 +0000 Subject: [PATCH 42/77] Add `discovery-ec2` integration test for AZ attr (#118452) Verifies that the plugin sets the `aws_availability_zone` automatically by reading the AZ name from the IMDS at startup. --- .../s3/RepositoryS3EcsCredentialsRestIT.java | 6 +- .../RepositoryS3ImdsV1CredentialsRestIT.java | 7 +-- .../RepositoryS3ImdsV2CredentialsRestIT.java | 7 +-- plugins/discovery-ec2/build.gradle | 3 + ...yEc2AvailabilityZoneAttributeImdsV1IT.java | 37 ++++++++++++ ...yEc2AvailabilityZoneAttributeImdsV2IT.java | 37 ++++++++++++ ...yEc2AvailabilityZoneAttributeNoImdsIT.java | 37 ++++++++++++ ...yEc2AvailabilityZoneAttributeTestCase.java | 52 +++++++++++++++++ .../fixture/aws/imds/Ec2ImdsHttpFixture.java | 24 ++------ .../fixture/aws/imds/Ec2ImdsHttpHandler.java | 13 ++++- .../aws/imds/Ec2ImdsServiceBuilder.java | 57 +++++++++++++++++++ .../aws/imds/Ec2ImdsHttpHandlerTests.java | 26 ++++++--- 12 files changed, 265 insertions(+), 41 deletions(-) create mode 100644 plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeImdsV1IT.java create mode 100644 plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeImdsV2IT.java create mode 100644 plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeNoImdsIT.java create mode 100644 plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeTestCase.java create mode 100644 test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsServiceBuilder.java diff --git a/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3EcsCredentialsRestIT.java b/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3EcsCredentialsRestIT.java index a79ae4de7cc66..4f0bf83000642 100644 --- a/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3EcsCredentialsRestIT.java +++ b/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3EcsCredentialsRestIT.java @@ -10,6 +10,7 @@ package org.elasticsearch.repositories.s3; import fixture.aws.imds.Ec2ImdsHttpFixture; +import fixture.aws.imds.Ec2ImdsServiceBuilder; import fixture.aws.imds.Ec2ImdsVersion; import fixture.s3.DynamicS3Credentials; import fixture.s3.S3HttpFixture; @@ -37,9 +38,8 @@ public class RepositoryS3EcsCredentialsRestIT extends AbstractRepositoryS3RestTe private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials(); private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture( - Ec2ImdsVersion.V1, - dynamicS3Credentials::addValidCredentials, - Set.of("/ecs_credentials_endpoint") + new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).newCredentialsConsumer(dynamicS3Credentials::addValidCredentials) + .alternativeCredentialsEndpoints(Set.of("/ecs_credentials_endpoint")) ); private static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, dynamicS3Credentials::isAuthorized); diff --git a/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV1CredentialsRestIT.java b/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV1CredentialsRestIT.java index ead91981b3fa8..dcdf52e963eef 100644 --- a/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV1CredentialsRestIT.java +++ b/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV1CredentialsRestIT.java @@ -10,6 +10,7 @@ package org.elasticsearch.repositories.s3; import fixture.aws.imds.Ec2ImdsHttpFixture; +import fixture.aws.imds.Ec2ImdsServiceBuilder; import fixture.aws.imds.Ec2ImdsVersion; import fixture.s3.DynamicS3Credentials; import fixture.s3.S3HttpFixture; @@ -23,8 +24,6 @@ import org.junit.rules.RuleChain; import org.junit.rules.TestRule; -import java.util.Set; - @ThreadLeakFilters(filters = { TestContainersThreadFilter.class }) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) // https://github.com/elastic/elasticsearch/issues/102482 public class RepositoryS3ImdsV1CredentialsRestIT extends AbstractRepositoryS3RestTestCase { @@ -37,9 +36,7 @@ public class RepositoryS3ImdsV1CredentialsRestIT extends AbstractRepositoryS3Res private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials(); private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture( - Ec2ImdsVersion.V1, - dynamicS3Credentials::addValidCredentials, - Set.of() + new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).newCredentialsConsumer(dynamicS3Credentials::addValidCredentials) ); private static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, dynamicS3Credentials::isAuthorized); diff --git a/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV2CredentialsRestIT.java b/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV2CredentialsRestIT.java index 67adb096bd1ba..434fc9720fc29 100644 --- a/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV2CredentialsRestIT.java +++ b/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV2CredentialsRestIT.java @@ -10,6 +10,7 @@ package org.elasticsearch.repositories.s3; import fixture.aws.imds.Ec2ImdsHttpFixture; +import fixture.aws.imds.Ec2ImdsServiceBuilder; import fixture.aws.imds.Ec2ImdsVersion; import fixture.s3.DynamicS3Credentials; import fixture.s3.S3HttpFixture; @@ -23,8 +24,6 @@ import org.junit.rules.RuleChain; import org.junit.rules.TestRule; -import java.util.Set; - @ThreadLeakFilters(filters = { TestContainersThreadFilter.class }) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) // https://github.com/elastic/elasticsearch/issues/102482 public class RepositoryS3ImdsV2CredentialsRestIT extends AbstractRepositoryS3RestTestCase { @@ -37,9 +36,7 @@ public class RepositoryS3ImdsV2CredentialsRestIT extends AbstractRepositoryS3Res private static final DynamicS3Credentials dynamicS3Credentials = new DynamicS3Credentials(); private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture( - Ec2ImdsVersion.V2, - dynamicS3Credentials::addValidCredentials, - Set.of() + new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V2).newCredentialsConsumer(dynamicS3Credentials::addValidCredentials) ); private static final S3HttpFixture s3Fixture = new S3HttpFixture(true, BUCKET, BASE_PATH, dynamicS3Credentials::isAuthorized); diff --git a/plugins/discovery-ec2/build.gradle b/plugins/discovery-ec2/build.gradle index 2335577225340..d4b56015edaa8 100644 --- a/plugins/discovery-ec2/build.gradle +++ b/plugins/discovery-ec2/build.gradle @@ -26,6 +26,9 @@ dependencies { api "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}" api "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:${versions.jackson}" api "joda-time:joda-time:2.10.10" + + javaRestTestImplementation project(':plugins:discovery-ec2') + javaRestTestImplementation project(':test:fixtures:ec2-imds-fixture') } tasks.named("dependencyLicenses").configure { diff --git a/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeImdsV1IT.java b/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeImdsV1IT.java new file mode 100644 index 0000000000000..32291236ea158 --- /dev/null +++ b/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeImdsV1IT.java @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.discovery.ec2; + +import fixture.aws.imds.Ec2ImdsHttpFixture; +import fixture.aws.imds.Ec2ImdsServiceBuilder; +import fixture.aws.imds.Ec2ImdsVersion; + +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.junit.ClassRule; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; + +public class DiscoveryEc2AvailabilityZoneAttributeImdsV1IT extends DiscoveryEc2AvailabilityZoneAttributeTestCase { + private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture( + new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).availabilityZoneSupplier( + DiscoveryEc2AvailabilityZoneAttributeTestCase::getAvailabilityZone + ) + ); + + public static ElasticsearchCluster cluster = buildCluster(ec2ImdsHttpFixture::getAddress); + + @ClassRule + public static TestRule ruleChain = RuleChain.outerRule(ec2ImdsHttpFixture).around(cluster); + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } +} diff --git a/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeImdsV2IT.java b/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeImdsV2IT.java new file mode 100644 index 0000000000000..8b785d688e7c4 --- /dev/null +++ b/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeImdsV2IT.java @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.discovery.ec2; + +import fixture.aws.imds.Ec2ImdsHttpFixture; +import fixture.aws.imds.Ec2ImdsServiceBuilder; +import fixture.aws.imds.Ec2ImdsVersion; + +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.junit.ClassRule; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; + +public class DiscoveryEc2AvailabilityZoneAttributeImdsV2IT extends DiscoveryEc2AvailabilityZoneAttributeTestCase { + private static final Ec2ImdsHttpFixture ec2ImdsHttpFixture = new Ec2ImdsHttpFixture( + new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V2).availabilityZoneSupplier( + DiscoveryEc2AvailabilityZoneAttributeTestCase::getAvailabilityZone + ) + ); + + public static ElasticsearchCluster cluster = buildCluster(ec2ImdsHttpFixture::getAddress); + + @ClassRule + public static TestRule ruleChain = RuleChain.outerRule(ec2ImdsHttpFixture).around(cluster); + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } +} diff --git a/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeNoImdsIT.java b/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeNoImdsIT.java new file mode 100644 index 0000000000000..602a98e17970d --- /dev/null +++ b/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeNoImdsIT.java @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.discovery.ec2; + +import org.elasticsearch.client.Request; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.junit.ClassRule; + +import java.io.IOException; + +public class DiscoveryEc2AvailabilityZoneAttributeNoImdsIT extends ESRestTestCase { + @ClassRule + public static ElasticsearchCluster cluster = ElasticsearchCluster.local() + .plugin("discovery-ec2") + .setting(AwsEc2Service.AUTO_ATTRIBUTE_SETTING.getKey(), "true") + .build(); + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } + + public void testAvailabilityZoneAttribute() throws IOException { + final var nodesInfoResponse = assertOKAndCreateObjectPath(client().performRequest(new Request("GET", "/_nodes/_all/_none"))); + for (final var nodeId : nodesInfoResponse.evaluateMapKeys("nodes")) { + assertNull(nodesInfoResponse.evaluateExact("nodes", nodeId, "attributes", "aws_availability_zone")); + } + } +} diff --git a/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeTestCase.java b/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeTestCase.java new file mode 100644 index 0000000000000..7eb18eec5c0b9 --- /dev/null +++ b/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeTestCase.java @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.discovery.ec2; + +import org.elasticsearch.client.Request; +import org.elasticsearch.common.util.concurrent.ConcurrentCollections; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.hamcrest.Matchers; + +import java.io.IOException; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +public abstract class DiscoveryEc2AvailabilityZoneAttributeTestCase extends ESRestTestCase { + + private static final Set createdAvailabilityZones = ConcurrentCollections.newConcurrentSet(); + + protected static String getAvailabilityZone() { + final var zoneName = randomIdentifier(); + createdAvailabilityZones.add(zoneName); + return zoneName; + } + + protected static ElasticsearchCluster buildCluster(Supplier imdsFixtureAddressSupplier) { + return ElasticsearchCluster.local() + .plugin("discovery-ec2") + .setting(AwsEc2Service.AUTO_ATTRIBUTE_SETTING.getKey(), "true") + .systemProperty("com.amazonaws.sdk.ec2MetadataServiceEndpointOverride", imdsFixtureAddressSupplier) + .build(); + } + + public void testAvailabilityZoneAttribute() throws IOException { + final var nodesInfoResponse = assertOKAndCreateObjectPath(client().performRequest(new Request("GET", "/_nodes/_all/_none"))); + for (final var nodeId : nodesInfoResponse.evaluateMapKeys("nodes")) { + assertThat( + createdAvailabilityZones, + Matchers.hasItem( + Objects.requireNonNull(nodesInfoResponse.evaluateExact("nodes", nodeId, "attributes", "aws_availability_zone")) + ) + ); + } + } +} diff --git a/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpFixture.java b/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpFixture.java index c63c65a750d7c..cc268a6021cb3 100644 --- a/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpFixture.java +++ b/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpFixture.java @@ -8,7 +8,6 @@ */ package fixture.aws.imds; -import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import org.junit.rules.ExternalResource; @@ -17,29 +16,14 @@ import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.Objects; -import java.util.Set; -import java.util.function.BiConsumer; public class Ec2ImdsHttpFixture extends ExternalResource { + private final Ec2ImdsServiceBuilder ec2ImdsServiceBuilder; private HttpServer server; - private final Ec2ImdsVersion ec2ImdsVersion; - private final BiConsumer newCredentialsConsumer; - private final Set alternativeCredentialsEndpoints; - - public Ec2ImdsHttpFixture( - Ec2ImdsVersion ec2ImdsVersion, - BiConsumer newCredentialsConsumer, - Set alternativeCredentialsEndpoints - ) { - this.ec2ImdsVersion = Objects.requireNonNull(ec2ImdsVersion); - this.newCredentialsConsumer = Objects.requireNonNull(newCredentialsConsumer); - this.alternativeCredentialsEndpoints = Objects.requireNonNull(alternativeCredentialsEndpoints); - } - - protected HttpHandler createHandler() { - return new Ec2ImdsHttpHandler(ec2ImdsVersion, newCredentialsConsumer, alternativeCredentialsEndpoints); + public Ec2ImdsHttpFixture(Ec2ImdsServiceBuilder ec2ImdsServiceBuilder) { + this.ec2ImdsServiceBuilder = ec2ImdsServiceBuilder; } public String getAddress() { @@ -52,7 +36,7 @@ public void stop(int delay) { protected void before() throws Throwable { server = HttpServer.create(resolveAddress(), 0); - server.createContext("/", Objects.requireNonNull(createHandler())); + server.createContext("/", Objects.requireNonNull(ec2ImdsServiceBuilder.buildHandler())); server.start(); } diff --git a/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpHandler.java b/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpHandler.java index 281465b96de05..fd2044357257b 100644 --- a/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpHandler.java +++ b/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpHandler.java @@ -26,6 +26,7 @@ import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; +import java.util.function.Supplier; import static org.elasticsearch.test.ESTestCase.randomIdentifier; import static org.elasticsearch.test.ESTestCase.randomSecretKey; @@ -43,15 +44,18 @@ public class Ec2ImdsHttpHandler implements HttpHandler { private final BiConsumer newCredentialsConsumer; private final Set validCredentialsEndpoints = ConcurrentCollections.newConcurrentSet(); + private final Supplier availabilityZoneSupplier; public Ec2ImdsHttpHandler( Ec2ImdsVersion ec2ImdsVersion, BiConsumer newCredentialsConsumer, - Collection alternativeCredentialsEndpoints + Collection alternativeCredentialsEndpoints, + Supplier availabilityZoneSupplier ) { this.ec2ImdsVersion = Objects.requireNonNull(ec2ImdsVersion); this.newCredentialsConsumer = Objects.requireNonNull(newCredentialsConsumer); this.validCredentialsEndpoints.addAll(alternativeCredentialsEndpoints); + this.availabilityZoneSupplier = availabilityZoneSupplier; } @Override @@ -98,6 +102,13 @@ public void handle(final HttpExchange exchange) throws IOException { exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length); exchange.getResponseBody().write(response); return; + } else if (path.equals("/latest/meta-data/placement/availability-zone")) { + final var availabilityZone = availabilityZoneSupplier.get(); + final byte[] response = availabilityZone.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "text/plain"); + exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length); + exchange.getResponseBody().write(response); + return; } else if (validCredentialsEndpoints.contains(path)) { final String accessKey = randomIdentifier(); final String sessionToken = randomIdentifier(); diff --git a/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsServiceBuilder.java b/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsServiceBuilder.java new file mode 100644 index 0000000000000..bca43da8683b6 --- /dev/null +++ b/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsServiceBuilder.java @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package fixture.aws.imds; + +import org.elasticsearch.test.ESTestCase; + +import java.util.Collection; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +public class Ec2ImdsServiceBuilder { + + private final Ec2ImdsVersion ec2ImdsVersion; + private BiConsumer newCredentialsConsumer = Ec2ImdsServiceBuilder::rejectNewCredentials; + private Collection alternativeCredentialsEndpoints = Set.of(); + private Supplier availabilityZoneSupplier = Ec2ImdsServiceBuilder::rejectAvailabilityZone; + + public Ec2ImdsServiceBuilder(Ec2ImdsVersion ec2ImdsVersion) { + this.ec2ImdsVersion = ec2ImdsVersion; + } + + public Ec2ImdsServiceBuilder newCredentialsConsumer(BiConsumer newCredentialsConsumer) { + this.newCredentialsConsumer = newCredentialsConsumer; + return this; + } + + private static void rejectNewCredentials(String ignored1, String ignored2) { + ESTestCase.fail("credentials creation not supported"); + } + + public Ec2ImdsServiceBuilder alternativeCredentialsEndpoints(Collection alternativeCredentialsEndpoints) { + this.alternativeCredentialsEndpoints = alternativeCredentialsEndpoints; + return this; + } + + private static String rejectAvailabilityZone() { + return ESTestCase.fail(null, "availability zones not supported"); + } + + public Ec2ImdsServiceBuilder availabilityZoneSupplier(Supplier availabilityZoneSupplier) { + this.availabilityZoneSupplier = availabilityZoneSupplier; + return this; + } + + public Ec2ImdsHttpHandler buildHandler() { + return new Ec2ImdsHttpHandler(ec2ImdsVersion, newCredentialsConsumer, alternativeCredentialsEndpoints, availabilityZoneSupplier); + } + +} diff --git a/test/fixtures/ec2-imds-fixture/src/test/java/fixture/aws/imds/Ec2ImdsHttpHandlerTests.java b/test/fixtures/ec2-imds-fixture/src/test/java/fixture/aws/imds/Ec2ImdsHttpHandlerTests.java index bb613395a0fba..6d3eb3d14e9b2 100644 --- a/test/fixtures/ec2-imds-fixture/src/test/java/fixture/aws/imds/Ec2ImdsHttpHandlerTests.java +++ b/test/fixtures/ec2-imds-fixture/src/test/java/fixture/aws/imds/Ec2ImdsHttpHandlerTests.java @@ -30,6 +30,7 @@ import java.net.InetSocketAddress; import java.net.URI; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -43,7 +44,7 @@ public class Ec2ImdsHttpHandlerTests extends ESTestCase { public void testImdsV1() throws IOException { final Map generatedCredentials = new HashMap<>(); - final var handler = new Ec2ImdsHttpHandler(Ec2ImdsVersion.V1, generatedCredentials::put, Set.of()); + final var handler = new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).newCredentialsConsumer(generatedCredentials::put).buildHandler(); final var roleResponse = handleRequest(handler, "GET", SECURITY_CREDENTIALS_URI); assertEquals(RestStatus.OK, roleResponse.status()); @@ -66,18 +67,14 @@ public void testImdsV1() throws IOException { public void testImdsV2Disabled() { assertEquals( RestStatus.METHOD_NOT_ALLOWED, - handleRequest( - new Ec2ImdsHttpHandler(Ec2ImdsVersion.V1, (accessKey, sessionToken) -> fail(), Set.of()), - "PUT", - "/latest/api/token" - ).status() + handleRequest(new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).buildHandler(), "PUT", "/latest/api/token").status() ); } public void testImdsV2() throws IOException { final Map generatedCredentials = new HashMap<>(); - final var handler = new Ec2ImdsHttpHandler(Ec2ImdsVersion.V2, generatedCredentials::put, Set.of()); + final var handler = new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V2).newCredentialsConsumer(generatedCredentials::put).buildHandler(); final var tokenResponse = handleRequest(handler, "PUT", "/latest/api/token"); assertEquals(RestStatus.OK, tokenResponse.status()); @@ -101,6 +98,21 @@ public void testImdsV2() throws IOException { assertEquals(sessionToken, responseMap.get("Token")); } + public void testAvailabilityZone() { + final Set generatedAvailabilityZones = new HashSet<>(); + final var handler = new Ec2ImdsServiceBuilder(Ec2ImdsVersion.V1).availabilityZoneSupplier(() -> { + final var newAvailabilityZone = randomIdentifier(); + generatedAvailabilityZones.add(newAvailabilityZone); + return newAvailabilityZone; + }).buildHandler(); + + final var availabilityZoneResponse = handleRequest(handler, "GET", "/latest/meta-data/placement/availability-zone"); + assertEquals(RestStatus.OK, availabilityZoneResponse.status()); + final var availabilityZone = availabilityZoneResponse.body().utf8ToString(); + + assertEquals(generatedAvailabilityZones, Set.of(availabilityZone)); + } + private record TestHttpResponse(RestStatus status, BytesReference body) {} private static TestHttpResponse checkImdsV2GetRequest(Ec2ImdsHttpHandler handler, String uri, String token) { From a0f64d2c9d6cefc0f53d67673db89d35e6d83dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mariusz=20J=C3=B3zala?= <377355+jozala@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:13:58 +0100 Subject: [PATCH 43/77] [ci] Add Alma Linux 9 to matrix in packaging and platform jobs (#118331) SmbTestContainer base image upgraded from Ubuntu 16.04 to 24.04 to avoid hanging Python module compilation when installing samba package. Installing SMB had to be moved from container building to starting because SYS_ADMIN capability is required. --- .../pipelines/periodic-packaging.template.yml | 1 + .buildkite/pipelines/periodic-packaging.yml | 1 + .../pipelines/periodic-platform-support.yml | 1 + .../pull-request/packaging-tests-unix.yml | 1 + x-pack/test/smb-fixture/build.gradle | 2 ++ .../test/fixtures/smb/SmbTestContainer.java | 28 +++++++++++++++---- .../resources/smb/provision/installsmb.sh | 2 +- 7 files changed, 29 insertions(+), 7 deletions(-) mode change 100644 => 100755 x-pack/test/smb-fixture/src/main/resources/smb/provision/installsmb.sh diff --git a/.buildkite/pipelines/periodic-packaging.template.yml b/.buildkite/pipelines/periodic-packaging.template.yml index aff0add62a2b6..b00ef51ec8f07 100644 --- a/.buildkite/pipelines/periodic-packaging.template.yml +++ b/.buildkite/pipelines/periodic-packaging.template.yml @@ -18,6 +18,7 @@ steps: - rhel-8 - rhel-9 - almalinux-8 + - almalinux-9 agents: provider: gcp image: family/elasticsearch-{{matrix.image}} diff --git a/.buildkite/pipelines/periodic-packaging.yml b/.buildkite/pipelines/periodic-packaging.yml index 9bcd61ac1273c..c58201258fbbf 100644 --- a/.buildkite/pipelines/periodic-packaging.yml +++ b/.buildkite/pipelines/periodic-packaging.yml @@ -19,6 +19,7 @@ steps: - rhel-8 - rhel-9 - almalinux-8 + - almalinux-9 agents: provider: gcp image: family/elasticsearch-{{matrix.image}} diff --git a/.buildkite/pipelines/periodic-platform-support.yml b/.buildkite/pipelines/periodic-platform-support.yml index 8bee3a78f8316..33e9a040b22cf 100644 --- a/.buildkite/pipelines/periodic-platform-support.yml +++ b/.buildkite/pipelines/periodic-platform-support.yml @@ -18,6 +18,7 @@ steps: - rhel-8 - rhel-9 - almalinux-8 + - almalinux-9 agents: provider: gcp image: family/elasticsearch-{{matrix.image}} diff --git a/.buildkite/pipelines/pull-request/packaging-tests-unix.yml b/.buildkite/pipelines/pull-request/packaging-tests-unix.yml index 6c7dadfd454ed..3b7973d05a338 100644 --- a/.buildkite/pipelines/pull-request/packaging-tests-unix.yml +++ b/.buildkite/pipelines/pull-request/packaging-tests-unix.yml @@ -21,6 +21,7 @@ steps: - rhel-8 - rhel-9 - almalinux-8 + - almalinux-9 PACKAGING_TASK: - docker - docker-cloud-ess diff --git a/x-pack/test/smb-fixture/build.gradle b/x-pack/test/smb-fixture/build.gradle index aeb5626ce9508..a982259edb2dd 100644 --- a/x-pack/test/smb-fixture/build.gradle +++ b/x-pack/test/smb-fixture/build.gradle @@ -2,6 +2,8 @@ apply plugin: 'elasticsearch.java' apply plugin: 'elasticsearch.cache-test-fixtures' dependencies { + implementation "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}" + api project(':test:fixtures:testcontainer-utils') api "junit:junit:${versions.junit}" api "org.testcontainers:testcontainers:${versions.testcontainer}" diff --git a/x-pack/test/smb-fixture/src/main/java/org/elasticsearch/test/fixtures/smb/SmbTestContainer.java b/x-pack/test/smb-fixture/src/main/java/org/elasticsearch/test/fixtures/smb/SmbTestContainer.java index 10f589e4e1df3..27d8257f4be10 100644 --- a/x-pack/test/smb-fixture/src/main/java/org/elasticsearch/test/fixtures/smb/SmbTestContainer.java +++ b/x-pack/test/smb-fixture/src/main/java/org/elasticsearch/test/fixtures/smb/SmbTestContainer.java @@ -7,12 +7,18 @@ package org.elasticsearch.test.fixtures.smb; +import com.github.dockerjava.api.model.Capability; + import org.elasticsearch.test.fixtures.testcontainers.DockerEnvironmentAwareTestContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitAllStrategy; import org.testcontainers.images.builder.ImageFromDockerfile; +import java.time.Duration; + public final class SmbTestContainer extends DockerEnvironmentAwareTestContainer { - private static final String DOCKER_BASE_IMAGE = "ubuntu:16.04"; + private static final String DOCKER_BASE_IMAGE = "ubuntu:24.04"; public static final int AD_LDAP_PORT = 636; public static final int AD_LDAP_GC_PORT = 3269; @@ -20,15 +26,15 @@ public SmbTestContainer() { super( new ImageFromDockerfile("es-smb-fixture").withDockerfileFromBuilder( builder -> builder.from(DOCKER_BASE_IMAGE) - .run("apt-get update -qqy && apt-get install -qqy samba ldap-utils") + .env("TZ", "Etc/UTC") + .run("DEBIAN_FRONTEND=noninteractive apt-get update -qqy && apt-get install -qqy tzdata winbind samba ldap-utils") .copy("fixture/provision/installsmb.sh", "/fixture/provision/installsmb.sh") .copy("fixture/certs/ca.key", "/fixture/certs/ca.key") .copy("fixture/certs/ca.pem", "/fixture/certs/ca.pem") .copy("fixture/certs/cert.pem", "/fixture/certs/cert.pem") .copy("fixture/certs/key.pem", "/fixture/certs/key.pem") .run("chmod +x /fixture/provision/installsmb.sh") - .run("/fixture/provision/installsmb.sh") - .cmd("service samba-ad-dc restart && sleep infinity") + .cmd("/fixture/provision/installsmb.sh && service samba-ad-dc restart && echo Samba started && sleep infinity") .build() ) .withFileFromClasspath("fixture/provision/installsmb.sh", "/smb/provision/installsmb.sh") @@ -37,10 +43,20 @@ public SmbTestContainer() { .withFileFromClasspath("fixture/certs/cert.pem", "/smb/certs/cert.pem") .withFileFromClasspath("fixture/certs/key.pem", "/smb/certs/key.pem") ); - // addExposedPort(389); - // addExposedPort(3268); + addExposedPort(AD_LDAP_PORT); addExposedPort(AD_LDAP_GC_PORT); + + setWaitStrategy( + new WaitAllStrategy().withStartupTimeout(Duration.ofSeconds(120)) + .withStrategy(Wait.forLogMessage(".*Samba started.*", 1)) + .withStrategy(Wait.forListeningPort()) + ); + + getCreateContainerCmdModifiers().add(createContainerCmd -> { + createContainerCmd.getHostConfig().withCapAdd(Capability.SYS_ADMIN); + return createContainerCmd; + }); } public String getAdLdapUrl() { diff --git a/x-pack/test/smb-fixture/src/main/resources/smb/provision/installsmb.sh b/x-pack/test/smb-fixture/src/main/resources/smb/provision/installsmb.sh old mode 100644 new mode 100755 index 463238b9f50c2..fe939431bb435 --- a/x-pack/test/smb-fixture/src/main/resources/smb/provision/installsmb.sh +++ b/x-pack/test/smb-fixture/src/main/resources/smb/provision/installsmb.sh @@ -21,7 +21,7 @@ cat $SSL_DIR/ca.pem >> /etc/ssl/certs/ca-certificates.crt mv /etc/samba/smb.conf /etc/samba/smb.conf.orig -samba-tool domain provision --server-role=dc --use-rfc2307 --dns-backend=SAMBA_INTERNAL --realm=AD.TEST.ELASTICSEARCH.COM --domain=ADES --adminpass=Passw0rd --use-ntvfs +samba-tool domain provision --server-role=dc --use-rfc2307 --dns-backend=SAMBA_INTERNAL --realm=AD.TEST.ELASTICSEARCH.COM --domain=ADES --adminpass=Passw0rd cp /var/lib/samba/private/krb5.conf /etc/krb5.conf From 8c10f0cc384c99718113f0a98542a809dd34ce75 Mon Sep 17 00:00:00 2001 From: kosabogi <105062005+kosabogi@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:15:17 +0100 Subject: [PATCH 44/77] Changes elser service to elasticsearch service in the Semantic search with the inference API page (#118536) --- .../search/search-your-data/semantic-search-inference.asciidoc | 2 +- .../tab-widgets/inference-api/infer-api-requirements.asciidoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/search/search-your-data/semantic-search-inference.asciidoc b/docs/reference/search/search-your-data/semantic-search-inference.asciidoc index 0abc44c809d08..c2fcb88380f53 100644 --- a/docs/reference/search/search-your-data/semantic-search-inference.asciidoc +++ b/docs/reference/search/search-your-data/semantic-search-inference.asciidoc @@ -45,7 +45,7 @@ include::{es-ref-dir}/tab-widgets/inference-api/infer-api-task-widget.asciidoc[] ==== Create the index mapping The mapping of the destination index - the index that contains the embeddings that the model will create based on your input text - must be created. -The destination index must have a field with the <> field type for most models and the <> field type for the sparse vector models like in the case of the `elser` service to index the output of the used model. +The destination index must have a field with the <> field type for most models and the <> field type for the sparse vector models like in the case of the `elasticsearch` service to index the output of the used model. include::{es-ref-dir}/tab-widgets/inference-api/infer-api-mapping-widget.asciidoc[] diff --git a/docs/reference/tab-widgets/inference-api/infer-api-requirements.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-requirements.asciidoc index eeecb4718658a..9e935f79aa0ac 100644 --- a/docs/reference/tab-widgets/inference-api/infer-api-requirements.asciidoc +++ b/docs/reference/tab-widgets/inference-api/infer-api-requirements.asciidoc @@ -8,7 +8,7 @@ the Cohere service. // tag::elser[] ELSER is a model trained by Elastic. If you have an {es} deployment, there is no -further requirement for using the {infer} API with the `elser` service. +further requirement for using the {infer} API with the `elasticsearch` service. // end::elser[] From b2998378a33d41510a52c9efcb22d238cf10b475 Mon Sep 17 00:00:00 2001 From: kosabogi <105062005+kosabogi@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:15:28 +0100 Subject: [PATCH 45/77] [DOCS] Adds default inference endpoints information (#118463) * Adds default inference andpoints information * Update docs/reference/inference/inference-apis.asciidoc Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> --------- Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> --- .../inference/inference-apis.asciidoc | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/reference/inference/inference-apis.asciidoc b/docs/reference/inference/inference-apis.asciidoc index c7b779a994a05..8d5ee1b7d6ba5 100644 --- a/docs/reference/inference/inference-apis.asciidoc +++ b/docs/reference/inference/inference-apis.asciidoc @@ -48,21 +48,21 @@ When adaptive allocations are enabled: For more information about adaptive allocations and resources, refer to the {ml-docs}/ml-nlp-auto-scale.html[trained model autoscaling] documentation. -//[discrete] -//[[default-enpoints]] -//=== Default {infer} endpoints +[discrete] +[[default-enpoints]] +=== Default {infer} endpoints -//Your {es} deployment contains some preconfigured {infer} endpoints that makes it easier for you to use them when defining `semantic_text` fields or {infer} processors. -//The following list contains the default {infer} endpoints listed by `inference_id`: +Your {es} deployment contains preconfigured {infer} endpoints which makes them easier to use when defining `semantic_text` fields or using {infer} processors. +The following list contains the default {infer} endpoints listed by `inference_id`: -//* `.elser-2-elasticsearch`: uses the {ml-docs}/ml-nlp-elser.html[ELSER] built-in trained model for `sparse_embedding` tasks (recommended for English language texts) -//* `.multilingual-e5-small-elasticsearch`: uses the {ml-docs}/ml-nlp-e5.html[E5] built-in trained model for `text_embedding` tasks (recommended for non-English language texts) +* `.elser-2-elasticsearch`: uses the {ml-docs}/ml-nlp-elser.html[ELSER] built-in trained model for `sparse_embedding` tasks (recommended for English language texts) +* `.multilingual-e5-small-elasticsearch`: uses the {ml-docs}/ml-nlp-e5.html[E5] built-in trained model for `text_embedding` tasks (recommended for non-English language texts) -//Use the `inference_id` of the endpoint in a <> field definition or when creating an <>. -//The API call will automatically download and deploy the model which might take a couple of minutes. -//Default {infer} enpoints have {ml-docs}/ml-nlp-auto-scale.html#nlp-model-adaptive-allocations[adaptive allocations] enabled. -//For these models, the minimum number of allocations is `0`. -//If there is no {infer} activity that uses the endpoint, the number of allocations will scale down to `0` automatically after 15 minutes. +Use the `inference_id` of the endpoint in a <> field definition or when creating an <>. +The API call will automatically download and deploy the model which might take a couple of minutes. +Default {infer} enpoints have {ml-docs}/ml-nlp-auto-scale.html#nlp-model-adaptive-allocations[adaptive allocations] enabled. +For these models, the minimum number of allocations is `0`. +If there is no {infer} activity that uses the endpoint, the number of allocations will scale down to `0` automatically after 15 minutes. [discrete] From f3dc0bdd503600723e3dc65e9bf1a321ad94e0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Thu, 12 Dec 2024 13:10:56 +0100 Subject: [PATCH 46/77] [Entitlements] Differentiate between ES modules and plugins (external) (#117973) --- libs/entitlement/qa/build.gradle | 4 +- .../entitlement/qa/EntitlementsAllowedIT.java | 4 +- .../bootstrap/EntitlementBootstrap.java | 11 ++--- .../EntitlementInitialization.java | 21 +++++---- .../runtime/policy/ExternalEntitlement.java | 8 ++++ .../runtime/policy/FileEntitlement.java | 2 +- .../runtime/policy/PolicyParser.java | 8 +++- .../policy/PolicyParserFailureTests.java | 21 ++++++--- .../runtime/policy/PolicyParserTests.java | 14 +++++- .../bootstrap/Elasticsearch.java | 14 +++--- .../elasticsearch/plugins/PluginsLoader.java | 44 +++++++++---------- .../plugins/MockPluginsService.java | 2 +- 12 files changed, 96 insertions(+), 57 deletions(-) diff --git a/libs/entitlement/qa/build.gradle b/libs/entitlement/qa/build.gradle index 86bafc34f4d00..7f46b2fe20a8a 100644 --- a/libs/entitlement/qa/build.gradle +++ b/libs/entitlement/qa/build.gradle @@ -13,8 +13,8 @@ apply plugin: 'elasticsearch.internal-test-artifact' dependencies { javaRestTestImplementation project(':libs:entitlement:qa:common') - clusterPlugins project(':libs:entitlement:qa:entitlement-allowed') - clusterPlugins project(':libs:entitlement:qa:entitlement-allowed-nonmodular') + clusterModules project(':libs:entitlement:qa:entitlement-allowed') + clusterModules project(':libs:entitlement:qa:entitlement-allowed-nonmodular') clusterPlugins project(':libs:entitlement:qa:entitlement-denied') clusterPlugins project(':libs:entitlement:qa:entitlement-denied-nonmodular') } diff --git a/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAllowedIT.java b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAllowedIT.java index 5135fff44531a..2fd4472f5cc65 100644 --- a/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAllowedIT.java +++ b/libs/entitlement/qa/src/javaRestTest/java/org/elasticsearch/entitlement/qa/EntitlementsAllowedIT.java @@ -28,8 +28,8 @@ public class EntitlementsAllowedIT extends ESRestTestCase { @ClassRule public static ElasticsearchCluster cluster = ElasticsearchCluster.local() - .plugin("entitlement-allowed") - .plugin("entitlement-allowed-nonmodular") + .module("entitlement-allowed") + .module("entitlement-allowed-nonmodular") .systemProperty("es.entitlements.enabled", "true") .setting("xpack.security.enabled", "false") .build(); diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java index 01b8f4d574f90..2abfb11964a93 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java @@ -15,7 +15,6 @@ import com.sun.tools.attach.VirtualMachine; import org.elasticsearch.core.SuppressForbidden; -import org.elasticsearch.core.Tuple; import org.elasticsearch.entitlement.initialization.EntitlementInitialization; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; @@ -29,7 +28,9 @@ public class EntitlementBootstrap { - public record BootstrapArgs(Collection> pluginData, Function, String> pluginResolver) {} + public record PluginData(Path pluginPath, boolean isModular, boolean isExternalPlugin) {} + + public record BootstrapArgs(Collection pluginData, Function, String> pluginResolver) {} private static BootstrapArgs bootstrapArgs; @@ -40,11 +41,11 @@ public static BootstrapArgs bootstrapArgs() { /** * Activates entitlement checking. Once this method returns, calls to methods protected by Entitlements from classes without a valid * policy will throw {@link org.elasticsearch.entitlement.runtime.api.NotEntitledException}. - * @param pluginData a collection of (plugin path, boolean), that holds the paths of all the installed Elasticsearch modules and - * plugins, and whether they are Java modular or not. + * @param pluginData a collection of (plugin path, boolean, boolean), that holds the paths of all the installed Elasticsearch modules + * and plugins, whether they are Java modular or not, and whether they are Elasticsearch modules or external plugins. * @param pluginResolver a functor to map a Java Class to the plugin it belongs to (the plugin name). */ - public static void bootstrap(Collection> pluginData, Function, String> pluginResolver) { + public static void bootstrap(Collection pluginData, Function, String> pluginResolver) { logger.debug("Loading entitlement agent"); if (EntitlementBootstrap.bootstrapArgs != null) { throw new IllegalStateException("plugin data is already set"); diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java index fb694308466c6..2956efa8eec31 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java @@ -9,7 +9,6 @@ package org.elasticsearch.entitlement.initialization; -import org.elasticsearch.core.Tuple; import org.elasticsearch.core.internal.provider.ProviderLocator; import org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap; import org.elasticsearch.entitlement.bridge.EntitlementChecker; @@ -96,25 +95,25 @@ private static PolicyManager createPolicyManager() throws IOException { return new PolicyManager(serverPolicy, pluginPolicies, EntitlementBootstrap.bootstrapArgs().pluginResolver()); } - private static Map createPluginPolicies(Collection> pluginData) throws IOException { + private static Map createPluginPolicies(Collection pluginData) throws IOException { Map pluginPolicies = new HashMap<>(pluginData.size()); - for (Tuple entry : pluginData) { - Path pluginRoot = entry.v1(); - boolean isModular = entry.v2(); - + for (var entry : pluginData) { + Path pluginRoot = entry.pluginPath(); String pluginName = pluginRoot.getFileName().toString(); - final Policy policy = loadPluginPolicy(pluginRoot, isModular, pluginName); + + final Policy policy = loadPluginPolicy(pluginRoot, entry.isModular(), pluginName, entry.isExternalPlugin()); pluginPolicies.put(pluginName, policy); } return pluginPolicies; } - private static Policy loadPluginPolicy(Path pluginRoot, boolean isModular, String pluginName) throws IOException { + private static Policy loadPluginPolicy(Path pluginRoot, boolean isModular, String pluginName, boolean isExternalPlugin) + throws IOException { Path policyFile = pluginRoot.resolve(POLICY_FILE_NAME); final Set moduleNames = getModuleNames(pluginRoot, isModular); - final Policy policy = parsePolicyIfExists(pluginName, policyFile); + final Policy policy = parsePolicyIfExists(pluginName, policyFile, isExternalPlugin); // TODO: should this check actually be part of the parser? for (Scope scope : policy.scopes) { @@ -125,9 +124,9 @@ private static Policy loadPluginPolicy(Path pluginRoot, boolean isModular, Strin return policy; } - private static Policy parsePolicyIfExists(String pluginName, Path policyFile) throws IOException { + private static Policy parsePolicyIfExists(String pluginName, Path policyFile, boolean isExternalPlugin) throws IOException { if (Files.exists(policyFile)) { - return new PolicyParser(Files.newInputStream(policyFile, StandardOpenOption.READ), pluginName).parsePolicy(); + return new PolicyParser(Files.newInputStream(policyFile, StandardOpenOption.READ), pluginName, isExternalPlugin).parsePolicy(); } return new Policy(pluginName, List.of()); } diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/ExternalEntitlement.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/ExternalEntitlement.java index bb1205696b49e..768babdb840f5 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/ExternalEntitlement.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/ExternalEntitlement.java @@ -33,4 +33,12 @@ * have to match the parameter names of the constructor. */ String[] parameterNames() default {}; + + /** + * This flag indicates if this Entitlement can be used in external plugins, + * or if it can be used only in Elasticsearch modules ("internal" plugins). + * Using an entitlement that is not {@code pluginsAccessible} in an external + * plugin policy will throw in exception while parsing. + */ + boolean esModulesOnly() default true; } diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileEntitlement.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileEntitlement.java index d0837bc096183..4fdbcc93ea6e0 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileEntitlement.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileEntitlement.java @@ -26,7 +26,7 @@ public class FileEntitlement implements Entitlement { private final String path; private final int actions; - @ExternalEntitlement(parameterNames = { "path", "actions" }) + @ExternalEntitlement(parameterNames = { "path", "actions" }, esModulesOnly = false) public FileEntitlement(String path, List actionsList) { this.path = path; int actionsInt = 0; diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java index 0d1a7c14ece4b..fb63d5ffbeb48 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyParser.java @@ -39,6 +39,7 @@ public class PolicyParser { protected final XContentParser policyParser; protected final String policyName; + private final boolean isExternalPlugin; static String getEntitlementTypeName(Class entitlementClass) { var entitlementClassName = entitlementClass.getSimpleName(); @@ -56,9 +57,10 @@ static String getEntitlementTypeName(Class entitlementCla .collect(Collectors.joining("_")); } - public PolicyParser(InputStream inputStream, String policyName) throws IOException { + public PolicyParser(InputStream inputStream, String policyName, boolean isExternalPlugin) throws IOException { this.policyParser = YamlXContent.yamlXContent.createParser(XContentParserConfiguration.EMPTY, Objects.requireNonNull(inputStream)); this.policyName = policyName; + this.isExternalPlugin = isExternalPlugin; } public Policy parsePolicy() { @@ -125,6 +127,10 @@ protected Entitlement parseEntitlement(String scopeName, String entitlementType) throw newPolicyParserException(scopeName, "unknown entitlement type [" + entitlementType + "]"); } + if (entitlementMetadata.esModulesOnly() && isExternalPlugin) { + throw newPolicyParserException("entitlement type [" + entitlementType + "] is allowed only on modules"); + } + Class[] parameterTypes = entitlementConstructor.getParameterTypes(); String[] parametersNames = entitlementMetadata.parameterNames(); diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserFailureTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserFailureTests.java index 7eb2b1fb476b3..dfcc5d8916f2c 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserFailureTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserFailureTests.java @@ -19,7 +19,7 @@ public class PolicyParserFailureTests extends ESTestCase { public void testParserSyntaxFailures() { PolicyParserException ppe = expectThrows( PolicyParserException.class, - () -> new PolicyParser(new ByteArrayInputStream("[]".getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml") + () -> new PolicyParser(new ByteArrayInputStream("[]".getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml", false) .parsePolicy() ); assertEquals("[1:1] policy parsing error for [test-failure-policy.yaml]: expected object ", ppe.getMessage()); @@ -29,7 +29,7 @@ public void testEntitlementDoesNotExist() { PolicyParserException ppe = expectThrows(PolicyParserException.class, () -> new PolicyParser(new ByteArrayInputStream(""" entitlement-module-name: - does_not_exist: {} - """.getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml").parsePolicy()); + """.getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml", false).parsePolicy()); assertEquals( "[2:5] policy parsing error for [test-failure-policy.yaml] in scope [entitlement-module-name]: " + "unknown entitlement type [does_not_exist]", @@ -41,7 +41,7 @@ public void testEntitlementMissingParameter() { PolicyParserException ppe = expectThrows(PolicyParserException.class, () -> new PolicyParser(new ByteArrayInputStream(""" entitlement-module-name: - file: {} - """.getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml").parsePolicy()); + """.getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml", false).parsePolicy()); assertEquals( "[2:12] policy parsing error for [test-failure-policy.yaml] in scope [entitlement-module-name] " + "for entitlement type [file]: missing entitlement parameter [path]", @@ -52,7 +52,7 @@ public void testEntitlementMissingParameter() { entitlement-module-name: - file: path: test-path - """.getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml").parsePolicy()); + """.getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml", false).parsePolicy()); assertEquals( "[4:1] policy parsing error for [test-failure-policy.yaml] in scope [entitlement-module-name] " + "for entitlement type [file]: missing entitlement parameter [actions]", @@ -68,11 +68,22 @@ public void testEntitlementExtraneousParameter() { actions: - read extra: test - """.getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml").parsePolicy()); + """.getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml", false).parsePolicy()); assertEquals( "[7:1] policy parsing error for [test-failure-policy.yaml] in scope [entitlement-module-name] " + "for entitlement type [file]: extraneous entitlement parameter(s) {extra=test}", ppe.getMessage() ); } + + public void testEntitlementIsNotForExternalPlugins() { + PolicyParserException ppe = expectThrows(PolicyParserException.class, () -> new PolicyParser(new ByteArrayInputStream(""" + entitlement-module-name: + - create_class_loader + """.getBytes(StandardCharsets.UTF_8)), "test-failure-policy.yaml", true).parsePolicy()); + assertEquals( + "[2:5] policy parsing error for [test-failure-policy.yaml]: entitlement type [create_class_loader] is allowed only on modules", + ppe.getMessage() + ); + } } diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserTests.java index a514cfe418895..633c76cb8c04f 100644 --- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserTests.java +++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyParserTests.java @@ -37,7 +37,17 @@ public void testGetEntitlementTypeName() { } public void testPolicyBuilder() throws IOException { - Policy parsedPolicy = new PolicyParser(PolicyParserTests.class.getResourceAsStream("test-policy.yaml"), "test-policy.yaml") + Policy parsedPolicy = new PolicyParser(PolicyParserTests.class.getResourceAsStream("test-policy.yaml"), "test-policy.yaml", false) + .parsePolicy(); + Policy builtPolicy = new Policy( + "test-policy.yaml", + List.of(new Scope("entitlement-module-name", List.of(new FileEntitlement("test/path/to/file", List.of("read", "write"))))) + ); + assertEquals(parsedPolicy, builtPolicy); + } + + public void testPolicyBuilderOnExternalPlugin() throws IOException { + Policy parsedPolicy = new PolicyParser(PolicyParserTests.class.getResourceAsStream("test-policy.yaml"), "test-policy.yaml", true) .parsePolicy(); Policy builtPolicy = new Policy( "test-policy.yaml", @@ -50,7 +60,7 @@ public void testParseCreateClassloader() throws IOException { Policy parsedPolicy = new PolicyParser(new ByteArrayInputStream(""" entitlement-module-name: - create_class_loader - """.getBytes(StandardCharsets.UTF_8)), "test-policy.yaml").parsePolicy(); + """.getBytes(StandardCharsets.UTF_8)), "test-policy.yaml", false).parsePolicy(); Policy builtPolicy = new Policy( "test-policy.yaml", List.of(new Scope("entitlement-module-name", List.of(new CreateClassLoaderEntitlement()))) diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java index ae59f6578f03a..9be23c91db072 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/Elasticsearch.java @@ -30,7 +30,6 @@ import org.elasticsearch.core.AbstractRefCounted; import org.elasticsearch.core.IOUtils; import org.elasticsearch.core.SuppressForbidden; -import org.elasticsearch.core.Tuple; import org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap; import org.elasticsearch.env.Environment; import org.elasticsearch.index.IndexVersion; @@ -59,6 +58,7 @@ import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import static org.elasticsearch.bootstrap.BootstrapSettings.SECURITY_FILTER_BAD_DEFAULTS_SETTING; import static org.elasticsearch.nativeaccess.WindowsFunctions.ConsoleCtrlHandler.CTRL_CLOSE_EVENT; @@ -218,10 +218,14 @@ private static void initPhase2(Bootstrap bootstrap) throws IOException { if (Boolean.parseBoolean(System.getProperty("es.entitlements.enabled"))) { LogManager.getLogger(Elasticsearch.class).info("Bootstrapping Entitlements"); - List> pluginData = pluginsLoader.allBundles() - .stream() - .map(bundle -> Tuple.tuple(bundle.getDir(), bundle.pluginDescriptor().isModular())) - .toList(); + List pluginData = Stream.concat( + pluginsLoader.moduleBundles() + .stream() + .map(bundle -> new EntitlementBootstrap.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), false)), + pluginsLoader.pluginBundles() + .stream() + .map(bundle -> new EntitlementBootstrap.PluginData(bundle.getDir(), bundle.pluginDescriptor().isModular(), true)) + ).toList(); EntitlementBootstrap.bootstrap(pluginData, pluginsResolver::resolveClassToPluginName); } else if (RuntimeVersionFeature.isSecurityManagerAvailable()) { diff --git a/server/src/main/java/org/elasticsearch/plugins/PluginsLoader.java b/server/src/main/java/org/elasticsearch/plugins/PluginsLoader.java index 8dfc1fc27c6aa..c7dc2c405ffba 100644 --- a/server/src/main/java/org/elasticsearch/plugins/PluginsLoader.java +++ b/server/src/main/java/org/elasticsearch/plugins/PluginsLoader.java @@ -122,7 +122,8 @@ public static LayerAndLoader ofUberModuleLoader(UberModuleClassLoader loader) { private final List moduleDescriptors; private final List pluginDescriptors; private final Map loadedPluginLayers; - private final Set allBundles; + private final Set moduleBundles; + private final Set pluginBundles; /** * Constructs a new PluginsLoader @@ -153,37 +154,36 @@ public static PluginsLoader createPluginsLoader(Path modulesDirectory, Path plug Set seenBundles = new LinkedHashSet<>(); // load (elasticsearch) module layers - List moduleDescriptors; + final Set modules; if (modulesDirectory != null) { try { - Set modules = PluginsUtils.getModuleBundles(modulesDirectory); - moduleDescriptors = modules.stream().map(PluginBundle::pluginDescriptor).toList(); + modules = PluginsUtils.getModuleBundles(modulesDirectory); seenBundles.addAll(modules); } catch (IOException ex) { throw new IllegalStateException("Unable to initialize modules", ex); } } else { - moduleDescriptors = Collections.emptyList(); + modules = Collections.emptySet(); } // load plugin layers - List pluginDescriptors; + final Set plugins; if (pluginsDirectory != null) { try { // TODO: remove this leniency, but tests bogusly rely on it if (isAccessibleDirectory(pluginsDirectory, logger)) { PluginsUtils.checkForFailedPluginRemovals(pluginsDirectory); - Set plugins = PluginsUtils.getPluginBundles(pluginsDirectory); - pluginDescriptors = plugins.stream().map(PluginBundle::pluginDescriptor).toList(); + plugins = PluginsUtils.getPluginBundles(pluginsDirectory); + seenBundles.addAll(plugins); } else { - pluginDescriptors = Collections.emptyList(); + plugins = Collections.emptySet(); } } catch (IOException ex) { throw new IllegalStateException("Unable to initialize plugins", ex); } } else { - pluginDescriptors = Collections.emptyList(); + plugins = Collections.emptySet(); } Map loadedPluginLayers = new LinkedHashMap<>(); @@ -197,19 +197,15 @@ public static PluginsLoader createPluginsLoader(Path modulesDirectory, Path plug } } - return new PluginsLoader(moduleDescriptors, pluginDescriptors, loadedPluginLayers, Set.copyOf(seenBundles)); + return new PluginsLoader(modules, plugins, loadedPluginLayers); } - PluginsLoader( - List moduleDescriptors, - List pluginDescriptors, - Map loadedPluginLayers, - Set allBundles - ) { - this.moduleDescriptors = moduleDescriptors; - this.pluginDescriptors = pluginDescriptors; + PluginsLoader(Set modules, Set plugins, Map loadedPluginLayers) { + this.moduleBundles = modules; + this.pluginBundles = plugins; + this.moduleDescriptors = modules.stream().map(PluginBundle::pluginDescriptor).toList(); + this.pluginDescriptors = plugins.stream().map(PluginBundle::pluginDescriptor).toList(); this.loadedPluginLayers = loadedPluginLayers; - this.allBundles = allBundles; } public List moduleDescriptors() { @@ -224,8 +220,12 @@ public Stream pluginLayers() { return loadedPluginLayers.values().stream().map(Function.identity()); } - public Set allBundles() { - return allBundles; + public Set moduleBundles() { + return moduleBundles; + } + + public Set pluginBundles() { + return pluginBundles; } private static void loadPluginLayer( diff --git a/test/framework/src/main/java/org/elasticsearch/plugins/MockPluginsService.java b/test/framework/src/main/java/org/elasticsearch/plugins/MockPluginsService.java index 91875600ec000..0a4c99eb8b52a 100644 --- a/test/framework/src/main/java/org/elasticsearch/plugins/MockPluginsService.java +++ b/test/framework/src/main/java/org/elasticsearch/plugins/MockPluginsService.java @@ -45,7 +45,7 @@ public MockPluginsService(Settings settings, Environment environment, Collection super( settings, environment.configFile(), - new PluginsLoader(Collections.emptyList(), Collections.emptyList(), Collections.emptyMap(), Collections.emptySet()) + new PluginsLoader(Collections.emptySet(), Collections.emptySet(), Collections.emptyMap()) ); List pluginsLoaded = new ArrayList<>(); From d51431535f0d4ae2acf5059ace9c4d5ba8e598d2 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Thu, 12 Dec 2024 12:34:00 +0000 Subject: [PATCH 47/77] Remove DiscoveryNodes.getSmallestNonClientNodeVersion method (#116508) --- .../cluster/node/DiscoveryNodes.java | 18 ------------------ .../SystemIndexMappingUpdateService.java | 2 +- .../cluster/node/DiscoveryNodesTests.java | 1 - .../license/TransportPostStartTrialAction.java | 2 +- 4 files changed, 2 insertions(+), 21 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodes.java b/server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodes.java index 12c698a6ed958..5e6dec7b68062 100644 --- a/server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodes.java +++ b/server/src/main/java/org/elasticsearch/cluster/node/DiscoveryNodes.java @@ -65,7 +65,6 @@ public class DiscoveryNodes implements Iterable, SimpleDiffable ingestNodes, @Nullable String masterNodeId, @Nullable String localNodeId, - Version minNonClientNodeVersion, Version maxNodeVersion, Version minNodeVersion, IndexVersion maxDataNodeCompatibleIndexVersion, @@ -98,7 +96,6 @@ private DiscoveryNodes( assert (masterNodeId == null) == (masterNode == null); this.localNodeId = localNodeId; this.localNode = localNodeId == null ? null : nodes.get(localNodeId); - this.minNonClientNodeVersion = minNonClientNodeVersion; this.minNodeVersion = minNodeVersion; this.maxNodeVersion = maxNodeVersion; this.maxDataNodeCompatibleIndexVersion = maxDataNodeCompatibleIndexVersion; @@ -117,7 +114,6 @@ public DiscoveryNodes withMasterNodeId(@Nullable String masterNodeId) { ingestNodes, masterNodeId, localNodeId, - minNonClientNodeVersion, maxNodeVersion, minNodeVersion, maxDataNodeCompatibleIndexVersion, @@ -346,17 +342,6 @@ public boolean isMixedVersionCluster() { return minNodeVersion.equals(maxNodeVersion) == false; } - /** - * Returns the version of the node with the oldest version in the cluster that is not a client node - * - * If there are no non-client nodes, Version.CURRENT will be returned. - * - * @return the oldest version in the cluster - */ - public Version getSmallestNonClientNodeVersion() { - return minNonClientNodeVersion; - } - /** * Returns the highest index version supported by all data nodes in the cluster */ @@ -853,14 +838,12 @@ public DiscoveryNodes build() { */ Version minNodeVersion = null; Version maxNodeVersion = null; - Version minNonClientNodeVersion = null; IndexVersion maxDataNodeCompatibleIndexVersion = null; IndexVersion minSupportedIndexVersion = null; for (Map.Entry nodeEntry : nodes.entrySet()) { DiscoveryNode discoNode = nodeEntry.getValue(); Version version = discoNode.getVersion(); if (discoNode.canContainData() || discoNode.isMasterNode()) { - minNonClientNodeVersion = min(minNonClientNodeVersion, version); maxDataNodeCompatibleIndexVersion = min(maxDataNodeCompatibleIndexVersion, discoNode.getMaxIndexVersion()); } minNodeVersion = min(minNodeVersion, version); @@ -894,7 +877,6 @@ public DiscoveryNodes build() { filteredNodes(nodes, DiscoveryNode::isIngestNode), masterNodeId, localNodeId, - Objects.requireNonNullElse(minNonClientNodeVersion, Version.CURRENT), Objects.requireNonNullElse(maxNodeVersion, Version.CURRENT), Objects.requireNonNullElse(minNodeVersion, Version.CURRENT.minimumCompatibilityVersion()), Objects.requireNonNullElse(maxDataNodeCompatibleIndexVersion, IndexVersion.current()), diff --git a/server/src/main/java/org/elasticsearch/indices/SystemIndexMappingUpdateService.java b/server/src/main/java/org/elasticsearch/indices/SystemIndexMappingUpdateService.java index f65ed270e29c4..a00e8cdc138f6 100644 --- a/server/src/main/java/org/elasticsearch/indices/SystemIndexMappingUpdateService.java +++ b/server/src/main/java/org/elasticsearch/indices/SystemIndexMappingUpdateService.java @@ -93,7 +93,7 @@ public void clusterChanged(ClusterChangedEvent event) { } // if we're in a mixed-version cluster, exit - if (state.nodes().getMaxNodeVersion().after(state.nodes().getSmallestNonClientNodeVersion())) { + if (state.nodes().isMixedVersionCluster()) { logger.debug("Skipping system indices up-to-date check as cluster has mixed versions"); return; } diff --git a/server/src/test/java/org/elasticsearch/cluster/node/DiscoveryNodesTests.java b/server/src/test/java/org/elasticsearch/cluster/node/DiscoveryNodesTests.java index 3264cf168b638..5101064f293e4 100644 --- a/server/src/test/java/org/elasticsearch/cluster/node/DiscoveryNodesTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/node/DiscoveryNodesTests.java @@ -435,7 +435,6 @@ public void testMinMaxNodeVersions() { assertEquals(Version.fromString("5.0.17"), build.getMaxNodeVersion()); assertEquals(Version.fromString("1.6.0"), build.getMinNodeVersion()); - assertEquals(Version.fromString("2.1.0"), build.getSmallestNonClientNodeVersion()); // doesn't include 1.6.0 observer assertEquals(IndexVersion.fromId(2010099), build.getMaxDataNodeCompatibleIndexVersion()); // doesn't include 2000199 observer assertEquals(IndexVersion.fromId(2000099), build.getMinSupportedIndexVersion()); // also includes observers } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportPostStartTrialAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportPostStartTrialAction.java index 8a7ead2caf3be..212e593c9db98 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportPostStartTrialAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportPostStartTrialAction.java @@ -55,7 +55,7 @@ protected void masterOperation( ClusterState state, ActionListener listener ) throws Exception { - if (state.nodes().getMaxNodeVersion().after(state.nodes().getSmallestNonClientNodeVersion())) { + if (state.nodes().isMixedVersionCluster()) { throw new IllegalStateException( "Please ensure all nodes are on the same version before starting your trial, the highest node version in this cluster is [" + state.nodes().getMaxNodeVersion() From e7a4436e27d1295c842df6fd096251defbdb0c26 Mon Sep 17 00:00:00 2001 From: Bogdan Pintea Date: Thu, 12 Dec 2024 14:29:33 +0100 Subject: [PATCH 48/77] ESQL: Disable remote enrich verification (#118534) This disables verifying the plans generated for remote ENRICHing. It also re-enables corresponding failing test. Related: #118531 Fixes #118307. --- muted-tests.yml | 3 --- .../xpack/esql/optimizer/PhysicalVerifier.java | 8 ++++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/muted-tests.yml b/muted-tests.yml index c91c7b50a0808..b750c0777ce34 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -276,9 +276,6 @@ tests: - class: org.elasticsearch.action.search.SearchQueryThenFetchAsyncActionTests method: testBottomFieldSort issue: https://github.com/elastic/elasticsearch/issues/118214 -- class: org.elasticsearch.xpack.esql.action.CrossClustersEnrichIT - method: testTopNThenEnrichRemote - issue: https://github.com/elastic/elasticsearch/issues/118307 - class: org.elasticsearch.xpack.remotecluster.CrossClusterEsqlRCS1UnavailableRemotesIT method: testEsqlRcs1UnavailableRemoteScenarios issue: https://github.com/elastic/elasticsearch/issues/118350 diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PhysicalVerifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PhysicalVerifier.java index 20528f8dc2826..9132cf87541bb 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PhysicalVerifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PhysicalVerifier.java @@ -12,7 +12,9 @@ import org.elasticsearch.xpack.esql.core.expression.Attribute; import org.elasticsearch.xpack.esql.core.expression.Expressions; import org.elasticsearch.xpack.esql.optimizer.rules.PlanConsistencyChecker; +import org.elasticsearch.xpack.esql.plan.logical.Enrich; import org.elasticsearch.xpack.esql.plan.physical.AggregateExec; +import org.elasticsearch.xpack.esql.plan.physical.EnrichExec; import org.elasticsearch.xpack.esql.plan.physical.FieldExtractExec; import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan; @@ -35,6 +37,12 @@ public Collection verify(PhysicalPlan plan) { Set failures = new LinkedHashSet<>(); Failures depFailures = new Failures(); + // AwaitsFix https://github.com/elastic/elasticsearch/issues/118531 + var enriches = plan.collectFirstChildren(EnrichExec.class::isInstance); + if (enriches.isEmpty() == false && ((EnrichExec) enriches.get(0)).mode() == Enrich.Mode.REMOTE) { + return failures; + } + plan.forEachDown(p -> { if (p instanceof AggregateExec agg) { var exclude = Expressions.references(agg.ordinalAttributes()); From 74a4484101dd65a0194f4adc3bd23fe39c2f2bd7 Mon Sep 17 00:00:00 2001 From: Dmitriy Burlutskiy Date: Thu, 12 Dec 2024 15:57:24 +0100 Subject: [PATCH 49/77] Support mTLS in Elastic Inference Service plugin (#116423) * Introduce new SSL settings under `xpack.inference.elastic.http.ssl`. * Support mTLS connection between Elasticsearch and Elastic Inference Service. --- docs/changelog/116423.yaml | 5 + .../xpack/core/ssl/SSLService.java | 2 + .../core/LocalStateCompositeXPackPlugin.java | 2 +- .../xpack/core/ssl/SSLServiceTests.java | 3 +- .../ShardBulkInferenceActionFilterIT.java | 3 +- .../integration/ModelRegistryIT.java | 4 +- .../inference/src/main/java/module-info.java | 1 + .../xpack/inference/InferencePlugin.java | 101 +++++++++++++----- .../external/http/HttpClientManager.java | 44 ++++++++ .../TextSimilarityRankRetrieverBuilder.java | 11 +- .../ElasticInferenceServiceSettings.java | 24 ++++- .../SemanticTextClusterMetadataTests.java | 3 +- .../xpack/inference/InferencePluginTests.java | 65 +++++++++++ .../inference/LocalStateInferencePlugin.java | 71 ++++++++++++ .../elasticsearch/xpack/inference/Utils.java | 15 --- ...emanticTextNonDynamicFieldMapperTests.java | 3 +- .../TextSimilarityRankMultiNodeTests.java | 4 +- ...SimilarityRankRetrieverTelemetryTests.java | 5 +- .../TextSimilarityRankTests.java | 4 +- .../xpack/ml/LocalStateMachineLearning.java | 7 ++ .../xpack/ml/support/BaseMlIntegTestCase.java | 4 +- .../security/CrossClusterShardTests.java | 2 - 22 files changed, 314 insertions(+), 69 deletions(-) create mode 100644 docs/changelog/116423.yaml create mode 100644 x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InferencePluginTests.java create mode 100644 x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/LocalStateInferencePlugin.java diff --git a/docs/changelog/116423.yaml b/docs/changelog/116423.yaml new file mode 100644 index 0000000000000..d6d10eab410e4 --- /dev/null +++ b/docs/changelog/116423.yaml @@ -0,0 +1,5 @@ +pr: 116423 +summary: Support mTLS for the Elastic Inference Service integration inside the inference API +area: Machine Learning +type: feature +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java index 9704335776f11..d0d5e463f9652 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLService.java @@ -596,6 +596,8 @@ static Map getSSLSettingsMap(Settings settings) { sslSettingsMap.put(WatcherField.EMAIL_NOTIFICATION_SSL_PREFIX, settings.getByPrefix(WatcherField.EMAIL_NOTIFICATION_SSL_PREFIX)); sslSettingsMap.put(XPackSettings.TRANSPORT_SSL_PREFIX, settings.getByPrefix(XPackSettings.TRANSPORT_SSL_PREFIX)); sslSettingsMap.putAll(getTransportProfileSSLSettings(settings)); + // Mount Elastic Inference Service (part of the Inference plugin) configuration + sslSettingsMap.put("xpack.inference.elastic.http.ssl", settings.getByPrefix("xpack.inference.elastic.http.ssl.")); // Only build remote cluster server SSL if the port is enabled if (REMOTE_CLUSTER_SERVER_ENABLED.get(settings)) { sslSettingsMap.put(XPackSettings.REMOTE_CLUSTER_SERVER_SSL_PREFIX, getRemoteClusterServerSslSettings(settings)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java index 1f2c89c473a62..d50f7bb27a5df 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java @@ -623,7 +623,7 @@ public Map getSnapshotCommitSup } @SuppressWarnings("unchecked") - private List filterPlugins(Class type) { + protected List filterPlugins(Class type) { return plugins.stream().filter(x -> type.isAssignableFrom(x.getClass())).map(p -> ((T) p)).collect(Collectors.toList()); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java index 9663e41a647a8..bfac286bc3c35 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLServiceTests.java @@ -614,7 +614,8 @@ public void testGetConfigurationByContextName() throws Exception { "xpack.security.authc.realms.ldap.realm1.ssl", "xpack.security.authc.realms.saml.realm2.ssl", "xpack.monitoring.exporters.mon1.ssl", - "xpack.monitoring.exporters.mon2.ssl" }; + "xpack.monitoring.exporters.mon2.ssl", + "xpack.inference.elastic.http.ssl" }; assumeTrue("Not enough cipher suites are available to support this test", getCipherSuites.length >= contextNames.length); diff --git a/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterIT.java b/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterIT.java index 3b0fc869c8124..c7b3a9d42f579 100644 --- a/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterIT.java +++ b/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterIT.java @@ -22,6 +22,7 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; import org.elasticsearch.xpack.inference.Utils; import org.elasticsearch.xpack.inference.mock.TestDenseInferenceServiceExtension; import org.elasticsearch.xpack.inference.mock.TestSparseInferenceServiceExtension; @@ -58,7 +59,7 @@ public void setup() throws Exception { @Override protected Collection> nodePlugins() { - return Arrays.asList(Utils.TestInferencePlugin.class); + return Arrays.asList(LocalStateInferencePlugin.class); } public void testBulkOperations() throws Exception { diff --git a/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/integration/ModelRegistryIT.java b/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/integration/ModelRegistryIT.java index be6b3725b0f35..d5c156d1d4f46 100644 --- a/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/integration/ModelRegistryIT.java +++ b/x-pack/plugin/inference/src/internalClusterTest/java/org/elasticsearch/xpack/inference/integration/ModelRegistryIT.java @@ -31,7 +31,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xpack.inference.InferencePlugin; +import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; import org.elasticsearch.xpack.inference.chunking.ChunkingSettingsTests; import org.elasticsearch.xpack.inference.registry.ModelRegistry; import org.elasticsearch.xpack.inference.services.elasticsearch.ElasticsearchInternalModel; @@ -76,7 +76,7 @@ public void createComponents() { @Override protected Collection> getPlugins() { - return pluginList(ReindexPlugin.class, InferencePlugin.class); + return pluginList(ReindexPlugin.class, LocalStateInferencePlugin.class); } public void testStoreModel() throws Exception { diff --git a/x-pack/plugin/inference/src/main/java/module-info.java b/x-pack/plugin/inference/src/main/java/module-info.java index 53974657e4e23..1c2240e8c5217 100644 --- a/x-pack/plugin/inference/src/main/java/module-info.java +++ b/x-pack/plugin/inference/src/main/java/module-info.java @@ -34,6 +34,7 @@ requires software.amazon.awssdk.retries.api; requires org.reactivestreams; requires org.elasticsearch.logging; + requires org.elasticsearch.sslconfig; exports org.elasticsearch.xpack.inference.action; exports org.elasticsearch.xpack.inference.registry; diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java index 148a784456361..eef07aefb30c8 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java @@ -28,6 +28,7 @@ import org.elasticsearch.indices.SystemIndexDescriptor; import org.elasticsearch.inference.InferenceServiceExtension; import org.elasticsearch.inference.InferenceServiceRegistry; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.node.PluginComponentBinding; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.ExtensiblePlugin; @@ -44,6 +45,7 @@ import org.elasticsearch.threadpool.ScalingExecutorBuilder; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xpack.core.ClientHelper; +import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction; import org.elasticsearch.xpack.core.inference.action.DeleteInferenceEndpointAction; import org.elasticsearch.xpack.core.inference.action.GetInferenceDiagnosticsAction; @@ -53,6 +55,7 @@ import org.elasticsearch.xpack.core.inference.action.PutInferenceModelAction; import org.elasticsearch.xpack.core.inference.action.UnifiedCompletionAction; import org.elasticsearch.xpack.core.inference.action.UpdateInferenceModelAction; +import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.inference.action.TransportDeleteInferenceEndpointAction; import org.elasticsearch.xpack.inference.action.TransportGetInferenceDiagnosticsAction; import org.elasticsearch.xpack.inference.action.TransportGetInferenceModelAction; @@ -116,7 +119,6 @@ import java.util.Map; import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.Collections.singletonList; @@ -150,6 +152,7 @@ public class InferencePlugin extends Plugin implements ActionPlugin, ExtensibleP private final Settings settings; private final SetOnce httpFactory = new SetOnce<>(); private final SetOnce amazonBedrockFactory = new SetOnce<>(); + private final SetOnce elasicInferenceServiceFactory = new SetOnce<>(); private final SetOnce serviceComponents = new SetOnce<>(); private final SetOnce elasticInferenceServiceComponents = new SetOnce<>(); private final SetOnce inferenceServiceRegistry = new SetOnce<>(); @@ -232,31 +235,31 @@ public Collection createComponents(PluginServices services) { var inferenceServices = new ArrayList<>(inferenceServiceExtensions); inferenceServices.add(this::getInferenceServiceFactories); - // Set elasticInferenceUrl based on feature flags to support transitioning to the new Elastic Inference Service URL without exposing - // internal names like "eis" or "gateway". - ElasticInferenceServiceSettings inferenceServiceSettings = new ElasticInferenceServiceSettings(settings); - - String elasticInferenceUrl = null; + if (isElasticInferenceServiceEnabled()) { + // Create a separate instance of HTTPClientManager with its own SSL configuration (`xpack.inference.elastic.http.ssl.*`). + var elasticInferenceServiceHttpClientManager = HttpClientManager.create( + settings, + services.threadPool(), + services.clusterService(), + throttlerManager, + getSslService() + ); - if (ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()) { - elasticInferenceUrl = inferenceServiceSettings.getElasticInferenceServiceUrl(); - } else if (DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()) { - log.warn( - "Deprecated flag {} detected for enabling {}. Please use {}.", - ELASTIC_INFERENCE_SERVICE_IDENTIFIER, - DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG, - ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG + var elasticInferenceServiceRequestSenderFactory = new HttpRequestSender.Factory( + serviceComponents.get(), + elasticInferenceServiceHttpClientManager, + services.clusterService() ); - elasticInferenceUrl = inferenceServiceSettings.getEisGatewayUrl(); - } + elasicInferenceServiceFactory.set(elasticInferenceServiceRequestSenderFactory); - if (elasticInferenceUrl != null) { + ElasticInferenceServiceSettings inferenceServiceSettings = new ElasticInferenceServiceSettings(settings); + String elasticInferenceUrl = this.getElasticInferenceServiceUrl(inferenceServiceSettings); elasticInferenceServiceComponents.set(new ElasticInferenceServiceComponents(elasticInferenceUrl)); inferenceServices.add( () -> List.of( context -> new ElasticInferenceService( - httpFactory.get(), + elasicInferenceServiceFactory.get(), serviceComponents.get(), elasticInferenceServiceComponents.get() ) @@ -379,16 +382,21 @@ public static ExecutorBuilder inferenceUtilityExecutor(Settings settings) { @Override public List> getSettings() { - return Stream.of( - HttpSettings.getSettingsDefinitions(), - HttpClientManager.getSettingsDefinitions(), - ThrottlerManager.getSettingsDefinitions(), - RetrySettings.getSettingsDefinitions(), - ElasticInferenceServiceSettings.getSettingsDefinitions(), - Truncator.getSettingsDefinitions(), - RequestExecutorServiceSettings.getSettingsDefinitions(), - List.of(SKIP_VALIDATE_AND_START) - ).flatMap(Collection::stream).collect(Collectors.toList()); + ArrayList> settings = new ArrayList<>(); + settings.addAll(HttpSettings.getSettingsDefinitions()); + settings.addAll(HttpClientManager.getSettingsDefinitions()); + settings.addAll(ThrottlerManager.getSettingsDefinitions()); + settings.addAll(RetrySettings.getSettingsDefinitions()); + settings.addAll(Truncator.getSettingsDefinitions()); + settings.addAll(RequestExecutorServiceSettings.getSettingsDefinitions()); + settings.add(SKIP_VALIDATE_AND_START); + + // Register Elastic Inference Service settings definitions if the corresponding feature flag is enabled. + if (isElasticInferenceServiceEnabled()) { + settings.addAll(ElasticInferenceServiceSettings.getSettingsDefinitions()); + } + + return settings; } @Override @@ -431,7 +439,10 @@ public List> getQueries() { @Override public List> getRetrievers() { return List.of( - new RetrieverSpec<>(new ParseField(TextSimilarityRankBuilder.NAME), TextSimilarityRankRetrieverBuilder::fromXContent), + new RetrieverSpec<>( + new ParseField(TextSimilarityRankBuilder.NAME), + (parser, context) -> TextSimilarityRankRetrieverBuilder.fromXContent(parser, context, getLicenseState()) + ), new RetrieverSpec<>(new ParseField(RandomRankBuilder.NAME), RandomRankRetrieverBuilder::fromXContent) ); } @@ -440,4 +451,36 @@ public List> getRetrievers() { public Map getHighlighters() { return Map.of(SemanticTextHighlighter.NAME, new SemanticTextHighlighter()); } + + // Get Elastic Inference service URL based on feature flags to support transitioning + // to the new Elastic Inference Service URL. + private String getElasticInferenceServiceUrl(ElasticInferenceServiceSettings settings) { + String elasticInferenceUrl = null; + + if (ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()) { + elasticInferenceUrl = settings.getElasticInferenceServiceUrl(); + } else if (DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()) { + log.warn( + "Deprecated flag {} detected for enabling {}. Please use {}.", + ELASTIC_INFERENCE_SERVICE_IDENTIFIER, + DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG, + ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG + ); + elasticInferenceUrl = settings.getEisGatewayUrl(); + } + + return elasticInferenceUrl; + } + + protected Boolean isElasticInferenceServiceEnabled() { + return (ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled() || DEPRECATED_ELASTIC_INFERENCE_SERVICE_FEATURE_FLAG.isEnabled()); + } + + protected SSLService getSslService() { + return XPackPlugin.getSharedSslService(); + } + + protected XPackLicenseState getLicenseState() { + return XPackPlugin.getSharedLicenseState(); + } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/HttpClientManager.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/HttpClientManager.java index e5d76b9bb5570..6d09c9e67b363 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/HttpClientManager.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/http/HttpClientManager.java @@ -7,9 +7,14 @@ package org.elasticsearch.xpack.inference.external.http; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager; import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor; import org.apache.http.impl.nio.reactor.IOReactorConfig; +import org.apache.http.nio.conn.NoopIOSessionStrategy; +import org.apache.http.nio.conn.SchemeIOSessionStrategy; +import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy; import org.apache.http.nio.reactor.ConnectingIOReactor; import org.apache.http.nio.reactor.IOReactorException; import org.apache.http.pool.PoolStats; @@ -21,6 +26,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import java.io.Closeable; @@ -28,11 +34,13 @@ import java.util.List; import static org.elasticsearch.core.Strings.format; +import static org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceServiceSettings.ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX; public class HttpClientManager implements Closeable { private static final Logger logger = LogManager.getLogger(HttpClientManager.class); /** * The maximum number of total connections the connection pool can lease to all routes. + * The configuration applies to each instance of HTTPClientManager (max_total_connections=10 and instances=5 leads to 50 connections). * From googling around the connection pools maxTotal value should be close to the number of available threads. * * https://stackoverflow.com/questions/30989637/how-to-decide-optimal-settings-for-setmaxtotal-and-setdefaultmaxperroute @@ -47,6 +55,7 @@ public class HttpClientManager implements Closeable { /** * The max number of connections a single route can lease. + * This configuration applies to each instance of HttpClientManager. */ public static final Setting MAX_ROUTE_CONNECTIONS = Setting.intSetting( "xpack.inference.http.max_route_connections", @@ -98,6 +107,22 @@ public static HttpClientManager create( return new HttpClientManager(settings, connectionManager, threadPool, clusterService, throttlerManager); } + public static HttpClientManager create( + Settings settings, + ThreadPool threadPool, + ClusterService clusterService, + ThrottlerManager throttlerManager, + SSLService sslService + ) { + // Set the sslStrategy to ensure an encrypted connection, as Elastic Inference Service requires it. + SSLIOSessionStrategy sslioSessionStrategy = sslService.sslIOSessionStrategy( + sslService.getSSLConfiguration(ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX) + ); + + PoolingNHttpClientConnectionManager connectionManager = createConnectionManager(sslioSessionStrategy); + return new HttpClientManager(settings, connectionManager, threadPool, clusterService, throttlerManager); + } + // Default for testing HttpClientManager( Settings settings, @@ -121,6 +146,25 @@ public static HttpClientManager create( this.addSettingsUpdateConsumers(clusterService); } + private static PoolingNHttpClientConnectionManager createConnectionManager(SSLIOSessionStrategy sslStrategy) { + ConnectingIOReactor ioReactor; + try { + var configBuilder = IOReactorConfig.custom().setSoKeepAlive(true); + ioReactor = new DefaultConnectingIOReactor(configBuilder.build()); + } catch (IOReactorException e) { + var message = "Failed to initialize HTTP client manager with SSL."; + logger.error(message, e); + throw new ElasticsearchException(message, e); + } + + Registry registry = RegistryBuilder.create() + .register("http", NoopIOSessionStrategy.INSTANCE) + .register("https", sslStrategy) + .build(); + + return new PoolingNHttpClientConnectionManager(ioReactor, registry); + } + private static PoolingNHttpClientConnectionManager createConnectionManager() { ConnectingIOReactor ioReactor; try { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java index fd2427dc8ac6a..f54696895a818 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverBuilder.java @@ -12,6 +12,7 @@ import org.elasticsearch.features.NodeFeature; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.rank.RankDoc; import org.elasticsearch.search.retriever.CompoundRetrieverBuilder; @@ -21,7 +22,6 @@ import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xpack.core.XPackPlugin; import java.io.IOException; import java.util.List; @@ -73,8 +73,11 @@ public class TextSimilarityRankRetrieverBuilder extends CompoundRetrieverBuilder RetrieverBuilder.declareBaseParserFields(TextSimilarityRankBuilder.NAME, PARSER); } - public static TextSimilarityRankRetrieverBuilder fromXContent(XContentParser parser, RetrieverParserContext context) - throws IOException { + public static TextSimilarityRankRetrieverBuilder fromXContent( + XContentParser parser, + RetrieverParserContext context, + XPackLicenseState licenceState + ) throws IOException { if (context.clusterSupportsFeature(TEXT_SIMILARITY_RERANKER_RETRIEVER_SUPPORTED) == false) { throw new ParsingException(parser.getTokenLocation(), "unknown retriever [" + TextSimilarityRankBuilder.NAME + "]"); } @@ -83,7 +86,7 @@ public static TextSimilarityRankRetrieverBuilder fromXContent(XContentParser par "[text_similarity_reranker] retriever composition feature is not supported by all nodes in the cluster" ); } - if (TextSimilarityRankBuilder.TEXT_SIMILARITY_RERANKER_FEATURE.check(XPackPlugin.getSharedLicenseState()) == false) { + if (TextSimilarityRankBuilder.TEXT_SIMILARITY_RERANKER_FEATURE.check(licenceState) == false) { throw LicenseUtils.newComplianceException(TextSimilarityRankBuilder.NAME); } return PARSER.apply(parser, context); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceServiceSettings.java index bc2daddc2a346..431a3647e2879 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elastic/ElasticInferenceServiceSettings.java @@ -9,7 +9,9 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; +import java.util.ArrayList; import java.util.List; public class ElasticInferenceServiceSettings { @@ -17,6 +19,8 @@ public class ElasticInferenceServiceSettings { @Deprecated static final Setting EIS_GATEWAY_URL = Setting.simpleString("xpack.inference.eis.gateway.url", Setting.Property.NodeScope); + public static final String ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX = "xpack.inference.elastic.http.ssl."; + static final Setting ELASTIC_INFERENCE_SERVICE_URL = Setting.simpleString( "xpack.inference.elastic.url", Setting.Property.NodeScope @@ -31,11 +35,27 @@ public class ElasticInferenceServiceSettings { public ElasticInferenceServiceSettings(Settings settings) { eisGatewayUrl = EIS_GATEWAY_URL.get(settings); elasticInferenceServiceUrl = ELASTIC_INFERENCE_SERVICE_URL.get(settings); - } + public static final SSLConfigurationSettings ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_SETTINGS = SSLConfigurationSettings.withPrefix( + ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX, + false + ); + + public static final Setting ELASTIC_INFERENCE_SERVICE_SSL_ENABLED = Setting.boolSetting( + ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX + "enabled", + true, + Setting.Property.NodeScope + ); + public static List> getSettingsDefinitions() { - return List.of(EIS_GATEWAY_URL, ELASTIC_INFERENCE_SERVICE_URL); + ArrayList> settings = new ArrayList<>(); + settings.add(EIS_GATEWAY_URL); + settings.add(ELASTIC_INFERENCE_SERVICE_URL); + settings.add(ELASTIC_INFERENCE_SERVICE_SSL_ENABLED); + settings.addAll(ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_SETTINGS.getEnabledSettings()); + + return settings; } @Deprecated diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/cluster/metadata/SemanticTextClusterMetadataTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/cluster/metadata/SemanticTextClusterMetadataTests.java index bfec2d5ac3484..61033a0211065 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/cluster/metadata/SemanticTextClusterMetadataTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/cluster/metadata/SemanticTextClusterMetadataTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.index.IndexService; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.inference.InferencePlugin; import org.hamcrest.Matchers; @@ -28,7 +29,7 @@ public class SemanticTextClusterMetadataTests extends ESSingleNodeTestCase { @Override protected Collection> getPlugins() { - return List.of(InferencePlugin.class); + return List.of(XPackPlugin.class, InferencePlugin.class); } public void testCreateIndexWithSemanticTextField() { diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InferencePluginTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InferencePluginTests.java new file mode 100644 index 0000000000000..d1db5b8b12cc6 --- /dev/null +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InferencePluginTests.java @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.inference; + +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.inference.services.elastic.ElasticInferenceServiceSettings; +import org.junit.After; +import org.junit.Before; + +import static org.hamcrest.Matchers.is; + +public class InferencePluginTests extends ESTestCase { + private InferencePlugin inferencePlugin; + + private Boolean elasticInferenceServiceEnabled = true; + + private void setElasticInferenceServiceEnabled(Boolean elasticInferenceServiceEnabled) { + this.elasticInferenceServiceEnabled = elasticInferenceServiceEnabled; + } + + @Before + public void setUp() throws Exception { + super.setUp(); + + Settings settings = Settings.builder().build(); + inferencePlugin = new InferencePlugin(settings) { + @Override + protected Boolean isElasticInferenceServiceEnabled() { + return elasticInferenceServiceEnabled; + } + }; + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + } + + public void testElasticInferenceServiceSettingsPresent() throws Exception { + setElasticInferenceServiceEnabled(true); // enable elastic inference service + boolean anyMatch = inferencePlugin.getSettings() + .stream() + .map(Setting::getKey) + .anyMatch(key -> key.startsWith(ElasticInferenceServiceSettings.ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX)); + + assertThat("xpack.inference.elastic settings are present", anyMatch, is(true)); + } + + public void testElasticInferenceServiceSettingsNotPresent() throws Exception { + setElasticInferenceServiceEnabled(false); // disable elastic inference service + boolean noneMatch = inferencePlugin.getSettings() + .stream() + .map(Setting::getKey) + .noneMatch(key -> key.startsWith(ElasticInferenceServiceSettings.ELASTIC_INFERENCE_SERVICE_SSL_CONFIGURATION_PREFIX)); + + assertThat("xpack.inference.elastic settings are not present", noneMatch, is(true)); + } +} diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/LocalStateInferencePlugin.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/LocalStateInferencePlugin.java new file mode 100644 index 0000000000000..68ea175bd9870 --- /dev/null +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/LocalStateInferencePlugin.java @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.inference; + +import org.elasticsearch.action.support.MappedActionFilter; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.mapper.Mapper; +import org.elasticsearch.inference.InferenceServiceExtension; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.plugins.SearchPlugin; +import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.inference.mock.TestDenseInferenceServiceExtension; +import org.elasticsearch.xpack.inference.mock.TestSparseInferenceServiceExtension; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.toList; + +public class LocalStateInferencePlugin extends LocalStateCompositeXPackPlugin { + private final InferencePlugin inferencePlugin; + + public LocalStateInferencePlugin(final Settings settings, final Path configPath) throws Exception { + super(settings, configPath); + LocalStateInferencePlugin thisVar = this; + this.inferencePlugin = new InferencePlugin(settings) { + @Override + protected SSLService getSslService() { + return thisVar.getSslService(); + } + + @Override + protected XPackLicenseState getLicenseState() { + return thisVar.getLicenseState(); + } + + @Override + public List getInferenceServiceFactories() { + return List.of( + TestSparseInferenceServiceExtension.TestInferenceService::new, + TestDenseInferenceServiceExtension.TestInferenceService::new + ); + } + }; + plugins.add(inferencePlugin); + } + + @Override + public List> getRetrievers() { + return this.filterPlugins(SearchPlugin.class).stream().flatMap(p -> p.getRetrievers().stream()).collect(toList()); + } + + @Override + public Map getMappers() { + return inferencePlugin.getMappers(); + } + + @Override + public Collection getMappedActionFilters() { + return inferencePlugin.getMappedActionFilters(); + } + +} diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/Utils.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/Utils.java index 9395ae222e9ba..0f322e64755be 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/Utils.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/Utils.java @@ -14,7 +14,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; -import org.elasticsearch.inference.InferenceServiceExtension; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelConfigurations; import org.elasticsearch.inference.ModelSecrets; @@ -143,20 +142,6 @@ private static void blockingCall( latch.await(); } - public static class TestInferencePlugin extends InferencePlugin { - public TestInferencePlugin(Settings settings) { - super(settings); - } - - @Override - public List getInferenceServiceFactories() { - return List.of( - TestSparseInferenceServiceExtension.TestInferenceService::new, - TestDenseInferenceServiceExtension.TestInferenceService::new - ); - } - } - public static Model getInvalidModel(String inferenceEntityId, String serviceName) { var mockConfigs = mock(ModelConfigurations.class); when(mockConfigs.getInferenceEntityId()).thenReturn(inferenceEntityId); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextNonDynamicFieldMapperTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextNonDynamicFieldMapperTests.java index 1f58c4165056d..24183b21f73e7 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextNonDynamicFieldMapperTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextNonDynamicFieldMapperTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.index.mapper.NonDynamicFieldMapperTests; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; import org.elasticsearch.xpack.inference.Utils; import org.elasticsearch.xpack.inference.mock.TestSparseInferenceServiceExtension; import org.junit.Before; @@ -26,7 +27,7 @@ public void setup() throws Exception { @Override protected Collection> getPlugins() { - return List.of(Utils.TestInferencePlugin.class); + return List.of(LocalStateInferencePlugin.class); } @Override diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankMultiNodeTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankMultiNodeTests.java index 6d6403b69ea11..daed03c198e0d 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankMultiNodeTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankMultiNodeTests.java @@ -10,7 +10,7 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.rank.RankBuilder; import org.elasticsearch.search.rank.rerank.AbstractRerankerIT; -import org.elasticsearch.xpack.inference.InferencePlugin; +import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; import java.util.Collection; import java.util.List; @@ -40,7 +40,7 @@ protected RankBuilder getThrowingRankBuilder(int rankWindowSize, String rankFeat @Override protected Collection> pluginsNeeded() { - return List.of(InferencePlugin.class, TextSimilarityTestPlugin.class); + return List.of(LocalStateInferencePlugin.class, TextSimilarityTestPlugin.class); } public void testQueryPhaseShardThrowingAllShardsFail() throws Exception { diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverTelemetryTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverTelemetryTests.java index 084a7f3de4a53..ba6924ba0ff3b 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverTelemetryTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankRetrieverTelemetryTests.java @@ -24,8 +24,7 @@ import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; -import org.elasticsearch.xpack.core.XPackPlugin; -import org.elasticsearch.xpack.inference.InferencePlugin; +import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; import org.junit.Before; import java.io.IOException; @@ -47,7 +46,7 @@ protected boolean addMockHttpTransport() { @Override protected Collection> nodePlugins() { - return List.of(InferencePlugin.class, XPackPlugin.class, TextSimilarityTestPlugin.class); + return List.of(LocalStateInferencePlugin.class, TextSimilarityTestPlugin.class); } @Override diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankTests.java index a042fca44fdb5..f81f2965c392e 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/rank/textsimilarity/TextSimilarityRankTests.java @@ -20,7 +20,7 @@ import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.test.hamcrest.ElasticsearchAssertions; import org.elasticsearch.xpack.core.inference.action.InferenceAction; -import org.elasticsearch.xpack.inference.InferencePlugin; +import org.elasticsearch.xpack.inference.LocalStateInferencePlugin; import org.junit.Before; import java.util.Collection; @@ -108,7 +108,7 @@ protected InferenceAction.Request generateRequest(List docFeatures) { @Override protected Collection> getPlugins() { - return List.of(InferencePlugin.class, TextSimilarityTestPlugin.class); + return List.of(LocalStateInferencePlugin.class, TextSimilarityTestPlugin.class); } @Before diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/LocalStateMachineLearning.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/LocalStateMachineLearning.java index bab012afc3101..ff1a1d19779df 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/LocalStateMachineLearning.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/LocalStateMachineLearning.java @@ -27,6 +27,7 @@ import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; import org.elasticsearch.xpack.core.rollup.action.GetRollupIndexCapsAction; import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.inference.InferencePlugin; import org.elasticsearch.xpack.monitoring.Monitoring; import org.elasticsearch.xpack.security.Security; @@ -86,6 +87,12 @@ protected XPackLicenseState getLicenseState() { } }); plugins.add(new MockedRollupPlugin()); + plugins.add(new InferencePlugin(settings) { + @Override + protected SSLService getSslService() { + return thisVar.getSslService(); + } + }); } @Override diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java index aeebfabdce704..5cf15454e47f2 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/support/BaseMlIntegTestCase.java @@ -82,7 +82,6 @@ import org.elasticsearch.xpack.core.ml.job.process.autodetect.state.DataCounts; import org.elasticsearch.xpack.core.ml.utils.MlTaskState; import org.elasticsearch.xpack.ilm.IndexLifecycle; -import org.elasticsearch.xpack.inference.InferencePlugin; import org.elasticsearch.xpack.ml.LocalStateMachineLearning; import org.elasticsearch.xpack.ml.MachineLearning; import org.elasticsearch.xpack.ml.MlSingleNodeTestCase; @@ -161,8 +160,7 @@ protected Collection> nodePlugins() { DataStreamsPlugin.class, // To remove errors from parsing build in templates that contain scaled_float MapperExtrasPlugin.class, - Wildcard.class, - InferencePlugin.class + Wildcard.class ); } diff --git a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java index ab5be0f48f5f3..057ebdece5c61 100644 --- a/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java +++ b/x-pack/plugin/security/qa/consistency-checks/src/test/java/org/elasticsearch/xpack/security/CrossClusterShardTests.java @@ -35,7 +35,6 @@ import org.elasticsearch.xpack.frozen.FrozenIndices; import org.elasticsearch.xpack.graph.Graph; import org.elasticsearch.xpack.ilm.IndexLifecycle; -import org.elasticsearch.xpack.inference.InferencePlugin; import org.elasticsearch.xpack.profiling.ProfilingPlugin; import org.elasticsearch.xpack.rollup.Rollup; import org.elasticsearch.xpack.search.AsyncSearch; @@ -89,7 +88,6 @@ protected Collection> getPlugins() { FrozenIndices.class, Graph.class, IndexLifecycle.class, - InferencePlugin.class, IngestCommonPlugin.class, IngestTestPlugin.class, MustachePlugin.class, From 3d191a809353330003ec2fe6d80031eb02cc45c9 Mon Sep 17 00:00:00 2001 From: Mary Gouseti Date: Thu, 12 Dec 2024 17:05:13 +0200 Subject: [PATCH 50/77] Remove deprecation logger from RestResizeHandler (#118561) https://github.com/elastic/elasticsearch/pull/114850 has already removed the `deprecated copy_settings` in this PR we remove the unused deprecation logger. --- .../rest/action/admin/indices/RestResizeHandler.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestResizeHandler.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestResizeHandler.java index ee1710f39ce41..5fd4ec83c1a18 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestResizeHandler.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestResizeHandler.java @@ -13,7 +13,6 @@ import org.elasticsearch.action.admin.indices.shrink.ResizeType; import org.elasticsearch.action.support.ActiveShardCount; import org.elasticsearch.client.internal.node.NodeClient; -import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.action.RestToXContentListener; @@ -27,7 +26,6 @@ import static org.elasticsearch.rest.RestUtils.getMasterNodeTimeout; public abstract class RestResizeHandler extends BaseRestHandler { - private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(RestResizeHandler.class); RestResizeHandler() {} From 0f937069a545892ff2b3711e7bebdedb06ddd972 Mon Sep 17 00:00:00 2001 From: Joe Gallo Date: Thu, 12 Dec 2024 10:24:05 -0500 Subject: [PATCH 51/77] Clean up some older collections idioms in ILM (#118508) --- .../xpack/core/ilm/AllocateAction.java | 10 +- .../xpack/core/ilm/AllocationRoutedStep.java | 4 +- .../xpack/core/ilm/DeleteAction.java | 5 +- .../xpack/core/ilm/ForceMergeStep.java | 6 +- .../xpack/core/ilm/FreezeAction.java | 3 +- .../xpack/core/ilm/LifecyclePolicyUtils.java | 7 +- .../xpack/core/ilm/ReadOnlyAction.java | 3 +- .../xpack/core/ilm/RolloverAction.java | 3 +- .../xpack/core/ilm/SegmentCountStep.java | 5 +- .../xpack/core/ilm/SetPriorityAction.java | 3 +- .../xpack/core/ilm/ShrinkAction.java | 3 +- .../core/ilm/TimeseriesLifecycleType.java | 15 +- .../xpack/core/ilm/UnfollowAction.java | 3 +- .../core/ilm/WaitForFollowShardTasksStep.java | 3 +- .../xpack/core/ilm/WaitForSnapshotAction.java | 3 +- .../xpack/core/ilm/AllocateActionTests.java | 21 ++- .../core/ilm/AllocationRoutedStepTests.java | 7 +- .../core/ilm/CheckShrinkReadyStepTests.java | 12 +- .../core/ilm/CloseFollowerIndexStepTests.java | 11 +- .../xpack/core/ilm/CloseIndexStepTests.java | 6 +- ...usterStateWaitUntilThresholdStepTests.java | 9 +- .../ilm/ExplainLifecycleResponseTests.java | 3 +- .../xpack/core/ilm/ForceMergeActionTests.java | 3 +- .../ilm/GenerateSnapshotNameStepTests.java | 6 +- .../IndexLifecycleExplainResponseTests.java | 4 +- .../core/ilm/LifecyclePolicyClientTests.java | 3 +- .../ilm/LifecyclePolicyMetadataTests.java | 5 +- .../xpack/core/ilm/LifecyclePolicyTests.java | 42 +++--- .../core/ilm/LifecyclePolicyUtilsTests.java | 34 ++--- .../xpack/core/ilm/MockAction.java | 6 +- .../ilm/OperationModeUpdateTaskTests.java | 9 +- .../core/ilm/PauseFollowerIndexStepTests.java | 14 +- .../core/ilm/PhaseCacheManagementTests.java | 25 ++- .../core/ilm/PhaseExecutionInfoTests.java | 4 +- .../xpack/core/ilm/PhaseTests.java | 13 +- .../xpack/core/ilm/RolloverStepTests.java | 10 +- .../xpack/core/ilm/SegmentCountStepTests.java | 10 +- .../xpack/core/ilm/ShrinkActionTests.java | 11 +- .../core/ilm/ShrinkSetAliasStepTests.java | 3 +- .../xpack/core/ilm/ShrinkStepTests.java | 4 +- ...pAliasesAndDeleteSourceIndexStepTests.java | 3 +- .../ilm/TimeseriesLifecycleTypeTests.java | 142 ++++++++---------- .../ilm/UnfollowFollowerIndexStepTests.java | 10 +- .../UpdateRolloverLifecycleDateStepTests.java | 5 +- .../core/ilm/WaitForDataTierStepTests.java | 4 +- .../ilm/WaitForFollowShardTasksStepTests.java | 12 +- .../ilm/WaitForIndexingCompleteStepTests.java | 6 +- .../ilm/WaitForRolloverReadyStepTests.java | 9 +- .../ilm/action/GetLifecycleResponseTests.java | 3 +- .../ilm/action/PutLifecycleRequestTests.java | 5 +- ...moveIndexLifecyclePolicyResponseTests.java | 8 +- .../xpack/ilm/CCRIndexLifecycleIT.java | 5 +- .../xpack/MigrateToDataTiersIT.java | 13 +- .../xpack/TimeSeriesRestDriver.java | 14 +- .../xpack/ilm/ChangePolicyForIndexIT.java | 17 +-- .../ilm/TimeSeriesLifecycleActionsIT.java | 7 +- .../actions/SearchableSnapshotActionIT.java | 39 +++-- .../xpack/ilm/actions/ShrinkActionIT.java | 3 +- .../xpack/security/PermissionsIT.java | 5 +- .../ClusterStateWaitThresholdBreachTests.java | 3 +- .../xpack/ilm/DataTiersMigrationsTests.java | 17 +-- .../xpack/ilm/ILMMultiNodeIT.java | 15 +- .../ilm/ILMMultiNodeWithCCRDisabledIT.java | 13 +- .../IndexLifecycleInitialisationTests.java | 16 +- ...adataMigrateToDataTiersRoutingService.java | 3 +- .../xpack/ilm/IlmHealthIndicatorService.java | 9 +- .../xpack/ilm/IndexLifecycleService.java | 3 +- .../action/TransportGetLifecycleAction.java | 3 +- .../xpack/ilm/history/ILMHistoryItem.java | 4 +- ...MigrateToDataTiersRoutingServiceTests.java | 46 ++---- .../ilm/ExecuteStepsUpdateTaskTests.java | 30 ++-- .../ilm/IlmHealthIndicatorServiceTests.java | 5 +- ...ndexLifecycleInfoTransportActionTests.java | 13 +- .../ilm/IndexLifecycleMetadataTests.java | 19 +-- .../xpack/ilm/IndexLifecycleRunnerTests.java | 37 +++-- .../xpack/ilm/IndexLifecycleServiceTests.java | 108 +++++-------- .../ilm/IndexLifecycleTransitionTests.java | 102 +++++-------- .../ilm/MoveToErrorStepUpdateTaskTests.java | 7 +- .../ilm/MoveToNextStepUpdateTaskTests.java | 10 +- .../xpack/ilm/PolicyStepsRegistryTests.java | 30 ++-- .../ilm/StagnatingIndicesFinderTests.java | 3 +- .../action/TransportStopILMActionTests.java | 5 +- 82 files changed, 470 insertions(+), 692 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AllocateAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AllocateAction.java index 311f3484900f2..bc9c3474ee63a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AllocateAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AllocateAction.java @@ -20,8 +20,6 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -73,17 +71,17 @@ public AllocateAction( Map require ) { if (include == null) { - this.include = Collections.emptyMap(); + this.include = Map.of(); } else { this.include = include; } if (exclude == null) { - this.exclude = Collections.emptyMap(); + this.exclude = Map.of(); } else { this.exclude = exclude; } if (require == null) { - this.require = Collections.emptyMap(); + this.require = Map.of(); } else { this.require = require; } @@ -201,7 +199,7 @@ public List toSteps(Client client, String phase, StepKey nextStepKey) { } UpdateSettingsStep allocateStep = new UpdateSettingsStep(allocateKey, allocationRoutedKey, client, newSettings.build()); AllocationRoutedStep routedCheckStep = new AllocationRoutedStep(allocationRoutedKey, nextStepKey); - return Arrays.asList(allocateStep, routedCheckStep); + return List.of(allocateStep, routedCheckStep); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AllocationRoutedStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AllocationRoutedStep.java index 7cdef6207c487..bc3fc0ccae02c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AllocationRoutedStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/AllocationRoutedStep.java @@ -22,7 +22,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.Index; -import java.util.Collections; +import java.util.List; import static org.elasticsearch.xpack.core.ilm.step.info.AllocationInfo.allShardsActiveAllocationInfo; import static org.elasticsearch.xpack.core.ilm.step.info.AllocationInfo.waitingForActiveShardsAllocationInfo; @@ -62,7 +62,7 @@ public Result isConditionMet(Index index, ClusterState clusterState) { } AllocationDeciders allocationDeciders = new AllocationDeciders( - Collections.singletonList( + List.of( new FilterAllocationDecider( clusterState.getMetadata().settings(), new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/DeleteAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/DeleteAction.java index d212492f14d01..8712cefac5d31 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/DeleteAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/DeleteAction.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.time.Instant; -import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -99,7 +98,7 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) ); CleanupSnapshotStep cleanupSnapshotStep = new CleanupSnapshotStep(cleanSnapshotKey, deleteStepKey, client); DeleteStep deleteStep = new DeleteStep(deleteStepKey, nextStepKey, client); - return Arrays.asList(waitForNoFollowersStep, waitUntilTimeSeriesEndTimeStep, cleanupSnapshotStep, deleteStep); + return List.of(waitForNoFollowersStep, waitUntilTimeSeriesEndTimeStep, cleanupSnapshotStep, deleteStep); } else { WaitForNoFollowersStep waitForNoFollowersStep = new WaitForNoFollowersStep( waitForNoFollowerStepKey, @@ -113,7 +112,7 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) client ); DeleteStep deleteStep = new DeleteStep(deleteStepKey, nextStepKey, client); - return Arrays.asList(waitForNoFollowersStep, waitUntilTimeSeriesEndTimeStep, deleteStep); + return List.of(waitForNoFollowersStep, waitUntilTimeSeriesEndTimeStep, deleteStep); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeStep.java index f3afe9e4d52cc..741fff63f61f5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ForceMergeStep.java @@ -20,7 +20,6 @@ import java.util.Arrays; import java.util.Objects; -import java.util.stream.Collectors; /** * Invokes a force merge on a single index. @@ -67,10 +66,7 @@ public void performAction( policyName, failures == null ? "n/a" - : Strings.collectionToDelimitedString( - Arrays.stream(failures).map(Strings::toString).collect(Collectors.toList()), - "," - ), + : Strings.collectionToDelimitedString(Arrays.stream(failures).map(Strings::toString).toList(), ","), NAME ); logger.warn(errorMessage); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/FreezeAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/FreezeAction.java index 67763e781e5a5..09e625b96135c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/FreezeAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/FreezeAction.java @@ -18,7 +18,6 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; -import java.util.Arrays; import java.util.List; /** @@ -98,7 +97,7 @@ public List toSteps(Client client, String phase, StepKey nextStepKey) { ); CheckNotDataStreamWriteIndexStep checkNoWriteIndexStep = new CheckNotDataStreamWriteIndexStep(checkNotWriteIndex, freezeStepKey); FreezeStep freezeStep = new FreezeStep(freezeStepKey, nextStepKey, client); - return Arrays.asList(conditionalSkipFreezeStep, checkNoWriteIndexStep, freezeStep); + return List.of(conditionalSkipFreezeStep, checkNoWriteIndexStep, freezeStep); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyUtils.java index 1a64e589d20b5..6a272b0d2271e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyUtils.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyUtils.java @@ -26,7 +26,6 @@ import java.io.IOException; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; /** * A utility class used for index lifecycle policies @@ -121,7 +120,7 @@ public static ItemUsage calculateUsage( .stream() .filter(indexMetadata -> policyName.equals(indexMetadata.getLifecyclePolicyName())) .map(indexMetadata -> indexMetadata.getIndex().getName()) - .collect(Collectors.toList()); + .toList(); final List allDataStreams = indexNameExpressionResolver.dataStreamNames( state, @@ -136,12 +135,12 @@ public static ItemUsage calculateUsage( } else { return false; } - }).collect(Collectors.toList()); + }).toList(); final List composableTemplates = state.metadata().templatesV2().keySet().stream().filter(templateName -> { Settings settings = MetadataIndexTemplateService.resolveSettings(state.metadata(), templateName); return policyName.equals(LifecycleSettings.LIFECYCLE_NAME_SETTING.get(settings)); - }).collect(Collectors.toList()); + }).toList(); return new ItemUsage(indices, dataStreams, composableTemplates); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ReadOnlyAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ReadOnlyAction.java index 117abecafeab3..2b03dc77eb5b6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ReadOnlyAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ReadOnlyAction.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.time.Instant; -import java.util.Arrays; import java.util.List; /** @@ -72,7 +71,7 @@ public List toSteps(Client client, String phase, StepKey nextStepKey) { client ); ReadOnlyStep readOnlyStep = new ReadOnlyStep(readOnlyKey, nextStepKey, client); - return Arrays.asList(checkNotWriteIndexStep, waitUntilTimeSeriesEndTimeStep, readOnlyStep); + return List.of(checkNotWriteIndexStep, waitUntilTimeSeriesEndTimeStep, readOnlyStep); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/RolloverAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/RolloverAction.java index 515941bce841a..f3c72004d6cc9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/RolloverAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/RolloverAction.java @@ -22,7 +22,6 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; -import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -172,7 +171,7 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) client, INDEXING_COMPLETE ); - return Arrays.asList(waitForRolloverReadyStep, rolloverStep, waitForActiveShardsStep, updateDateStep, setIndexingCompleteStep); + return List.of(waitForRolloverReadyStep, rolloverStep, waitForActiveShardsStep, updateDateStep, setIndexingCompleteStep); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SegmentCountStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SegmentCountStep.java index ad8f450fb0849..95ca049740c73 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SegmentCountStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SegmentCountStep.java @@ -67,10 +67,7 @@ public void evaluateCondition(Metadata metadata, Index index, Listener listener, response.getFailedShards(), failures == null ? "n/a" - : Strings.collectionToDelimitedString( - Arrays.stream(failures).map(Strings::toString).collect(Collectors.toList()), - "," - ) + : Strings.collectionToDelimitedString(Arrays.stream(failures).map(Strings::toString).toList(), ",") ); listener.onResponse(true, new Info(-1)); } else { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SetPriorityAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SetPriorityAction.java index 376567bc2004c..5f7c1d0c3bf3a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SetPriorityAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/SetPriorityAction.java @@ -21,7 +21,6 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; -import java.util.Collections; import java.util.List; import java.util.Objects; @@ -101,7 +100,7 @@ public List toSteps(Client client, String phase, StepKey nextStepKey) { Settings indexPriority = recoveryPriority == null ? NULL_PRIORITY_SETTINGS : Settings.builder().put(IndexMetadata.INDEX_PRIORITY_SETTING.getKey(), recoveryPriority).build(); - return Collections.singletonList(new UpdateSettingsStep(key, nextStepKey, client, indexPriority)); + return List.of(new UpdateSettingsStep(key, nextStepKey, client, indexPriority)); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java index 401d87f853360..70ec5da1d8a2a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/ShrinkAction.java @@ -31,7 +31,6 @@ import java.time.Instant; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; import java.util.stream.Stream; import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; @@ -329,7 +328,7 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) allowWriteAfterShrinkStep ); - return steps.filter(Objects::nonNull).collect(Collectors.toList()); + return steps.filter(Objects::nonNull).toList(); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleType.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleType.java index 48a0e65bddf22..0fd280f440f39 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleType.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleType.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -30,8 +29,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import static java.util.stream.Collectors.toList; - /** * Represents the lifecycle of an index from creation to deletion. A * {@link TimeseriesLifecycleType} is made up of a set of {@link Phase}s which it will @@ -114,7 +111,7 @@ public class TimeseriesLifecycleType implements LifecycleType { // Set of actions that cannot be defined (executed) after the managed index has been mounted as searchable snapshot. // It's ordered to produce consistent error messages which can be unit tested. public static final Set ACTIONS_CANNOT_FOLLOW_SEARCHABLE_SNAPSHOT = Collections.unmodifiableSet( - new LinkedHashSet<>(Arrays.asList(ForceMergeAction.NAME, FreezeAction.NAME, ShrinkAction.NAME, DownsampleAction.NAME)) + new LinkedHashSet<>(List.of(ForceMergeAction.NAME, FreezeAction.NAME, ShrinkAction.NAME, DownsampleAction.NAME)) ); private TimeseriesLifecycleType() {} @@ -180,11 +177,11 @@ public static boolean shouldInjectMigrateStepForPhase(Phase phase) { public List getOrderedActions(Phase phase) { Map actions = phase.getActions(); return switch (phase.getName()) { - case HOT_PHASE -> ORDERED_VALID_HOT_ACTIONS.stream().map(actions::get).filter(Objects::nonNull).collect(toList()); - case WARM_PHASE -> ORDERED_VALID_WARM_ACTIONS.stream().map(actions::get).filter(Objects::nonNull).collect(toList()); - case COLD_PHASE -> ORDERED_VALID_COLD_ACTIONS.stream().map(actions::get).filter(Objects::nonNull).collect(toList()); - case FROZEN_PHASE -> ORDERED_VALID_FROZEN_ACTIONS.stream().map(actions::get).filter(Objects::nonNull).collect(toList()); - case DELETE_PHASE -> ORDERED_VALID_DELETE_ACTIONS.stream().map(actions::get).filter(Objects::nonNull).collect(toList()); + case HOT_PHASE -> ORDERED_VALID_HOT_ACTIONS.stream().map(actions::get).filter(Objects::nonNull).toList(); + case WARM_PHASE -> ORDERED_VALID_WARM_ACTIONS.stream().map(actions::get).filter(Objects::nonNull).toList(); + case COLD_PHASE -> ORDERED_VALID_COLD_ACTIONS.stream().map(actions::get).filter(Objects::nonNull).toList(); + case FROZEN_PHASE -> ORDERED_VALID_FROZEN_ACTIONS.stream().map(actions::get).filter(Objects::nonNull).toList(); + case DELETE_PHASE -> ORDERED_VALID_DELETE_ACTIONS.stream().map(actions::get).filter(Objects::nonNull).toList(); default -> throw new IllegalArgumentException("lifecycle type [" + TYPE + "] does not support phase [" + phase.getName() + "]"); }; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/UnfollowAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/UnfollowAction.java index 31aaba551a3f3..6bb0178f1471e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/UnfollowAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/UnfollowAction.java @@ -17,7 +17,6 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; -import java.util.Arrays; import java.util.List; import java.util.Map; @@ -72,7 +71,7 @@ public List toSteps(Client client, String phase, StepKey nextStepKey) { UnfollowFollowerIndexStep step5 = new UnfollowFollowerIndexStep(unfollowFollowerIndex, openFollowerIndex, client); OpenIndexStep step6 = new OpenIndexStep(openFollowerIndex, waitForYellowStep, client); WaitForIndexColorStep step7 = new WaitForIndexColorStep(waitForYellowStep, nextStepKey, ClusterHealthStatus.YELLOW); - return Arrays.asList(conditionalSkipUnfollowStep, step1, step2, step3, step4, step5, step6, step7); + return List.of(conditionalSkipUnfollowStep, step1, step2, step3, step4, step5, step6, step7); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStep.java index 224319722297c..590890405b8d7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStep.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import static org.elasticsearch.xpack.core.ilm.UnfollowAction.CCR_METADATA_KEY; @@ -78,7 +77,7 @@ static void handleResponse(FollowStatsAction.StatsResponses responses, Listener status.followerGlobalCheckpoint() ) ) - .collect(Collectors.toList()); + .toList(); listener.onResponse(false, new Info(shardFollowTaskInfos)); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForSnapshotAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForSnapshotAction.java index 08a884f0b8f3c..2633656d7c30c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForSnapshotAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/WaitForSnapshotAction.java @@ -17,7 +17,6 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.io.IOException; -import java.util.Collections; import java.util.List; import java.util.Objects; @@ -62,7 +61,7 @@ public String getPolicy() { @Override public List toSteps(Client client, String phase, StepKey nextStepKey) { StepKey waitForSnapshotKey = new StepKey(phase, NAME, WaitForSnapshotStep.NAME); - return Collections.singletonList(new WaitForSnapshotStep(waitForSnapshotKey, nextStepKey, client, policy)); + return List.of(new WaitForSnapshotStep(waitForSnapshotKey, nextStepKey, client, policy)); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AllocateActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AllocateActionTests.java index 1fc0afafde353..c5a8185f8511b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AllocateActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AllocateActionTests.java @@ -14,7 +14,6 @@ import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ilm.Step.StepKey; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -44,20 +43,20 @@ static AllocateAction randomInstance() { includes = randomAllocationRoutingMap(1, 100); hasAtLeastOneMap = true; } else { - includes = randomBoolean() ? null : Collections.emptyMap(); + includes = randomBoolean() ? null : Map.of(); } Map excludes; if (randomBoolean()) { hasAtLeastOneMap = true; excludes = randomAllocationRoutingMap(1, 100); } else { - excludes = randomBoolean() ? null : Collections.emptyMap(); + excludes = randomBoolean() ? null : Map.of(); } Map requires; if (hasAtLeastOneMap == false || randomBoolean()) { requires = randomAllocationRoutingMap(1, 100); } else { - requires = randomBoolean() ? null : Collections.emptyMap(); + requires = randomBoolean() ? null : Map.of(); } Integer numberOfReplicas = randomBoolean() ? null : randomIntBetween(0, 10); Integer totalShardsPerNode = randomBoolean() ? null : randomIntBetween(-1, 10); @@ -97,9 +96,9 @@ protected AllocateAction mutateInstance(AllocateAction instance) { } public void testAllMapsNullOrEmpty() { - Map include = randomBoolean() ? null : Collections.emptyMap(); - Map exclude = randomBoolean() ? null : Collections.emptyMap(); - Map require = randomBoolean() ? null : Collections.emptyMap(); + Map include = randomBoolean() ? null : Map.of(); + Map exclude = randomBoolean() ? null : Map.of(); + Map require = randomBoolean() ? null : Map.of(); IllegalArgumentException exception = expectThrows( IllegalArgumentException.class, () -> new AllocateAction(null, null, include, exclude, require) @@ -124,8 +123,8 @@ public void testAllMapsNullOrEmpty() { public void testInvalidNumberOfReplicas() { Map include = randomAllocationRoutingMap(1, 5); - Map exclude = randomBoolean() ? null : Collections.emptyMap(); - Map require = randomBoolean() ? null : Collections.emptyMap(); + Map exclude = randomBoolean() ? null : Map.of(); + Map require = randomBoolean() ? null : Map.of(); IllegalArgumentException exception = expectThrows( IllegalArgumentException.class, () -> new AllocateAction(randomIntBetween(-1000, -1), randomIntBetween(0, 300), include, exclude, require) @@ -135,8 +134,8 @@ public void testInvalidNumberOfReplicas() { public void testInvalidTotalShardsPerNode() { Map include = randomAllocationRoutingMap(1, 5); - Map exclude = randomBoolean() ? null : Collections.emptyMap(); - Map require = randomBoolean() ? null : Collections.emptyMap(); + Map exclude = randomBoolean() ? null : Map.of(); + Map require = randomBoolean() ? null : Map.of(); IllegalArgumentException exception = expectThrows( IllegalArgumentException.class, () -> new AllocateAction(randomIntBetween(0, 300), randomIntBetween(-1000, -2), include, exclude, require) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AllocationRoutedStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AllocationRoutedStepTests.java index afad708ddbe2c..708c3630b8b8a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AllocationRoutedStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/AllocationRoutedStepTests.java @@ -27,7 +27,6 @@ import org.elasticsearch.xpack.core.ilm.ClusterStateWaitStep.Result; import org.elasticsearch.xpack.core.ilm.Step.StepKey; -import java.util.Collections; import java.util.Map; import static org.elasticsearch.cluster.routing.TestShardRouting.buildUnassignedInfo; @@ -109,7 +108,7 @@ public void testConditionMet() { public void testRequireConditionMetOnlyOneCopyAllocated() { Index index = new Index(randomAlphaOfLengthBetween(1, 20), randomAlphaOfLengthBetween(1, 20)); - Map requires = Collections.singletonMap(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "foo", "bar"); + Map requires = Map.of(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_SETTING.getKey() + "foo", "bar"); Settings.Builder existingSettings = Settings.builder() .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()) .put(IndexMetadata.SETTING_INDEX_UUID, index.getUUID()); @@ -187,7 +186,7 @@ public void testClusterExcludeFiltersConditionMetOnlyOneCopyAllocated() { public void testExcludeConditionMetOnlyOneCopyAllocated() { Index index = new Index(randomAlphaOfLengthBetween(1, 20), randomAlphaOfLengthBetween(1, 20)); - Map excludes = Collections.singletonMap(IndexMetadata.INDEX_ROUTING_EXCLUDE_GROUP_SETTING.getKey() + "foo", "bar"); + Map excludes = Map.of(IndexMetadata.INDEX_ROUTING_EXCLUDE_GROUP_SETTING.getKey() + "foo", "bar"); Settings.Builder existingSettings = Settings.builder() .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()) .put(IndexMetadata.SETTING_INDEX_UUID, index.getUUID()); @@ -218,7 +217,7 @@ public void testExcludeConditionMetOnlyOneCopyAllocated() { public void testIncludeConditionMetOnlyOneCopyAllocated() { Index index = new Index(randomAlphaOfLengthBetween(1, 20), randomAlphaOfLengthBetween(1, 20)); - Map includes = Collections.singletonMap(IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + "foo", "bar"); + Map includes = Map.of(IndexMetadata.INDEX_ROUTING_INCLUDE_GROUP_SETTING.getKey() + "foo", "bar"); Settings.Builder existingSettings = Settings.builder() .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()) .put(IndexMetadata.SETTING_INDEX_UUID, index.getUUID()); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStepTests.java index 8dcd8fc7ddd55..72bf7cedb2fb9 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CheckShrinkReadyStepTests.java @@ -29,9 +29,9 @@ import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.node.Node; -import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import static org.elasticsearch.cluster.metadata.SingleNodeShutdownMetadata.Type.SIGTERM; import static org.elasticsearch.cluster.routing.TestShardRouting.shardRoutingBuilder; @@ -340,7 +340,7 @@ public void testExecuteAllocateReplicaUnassigned() { */ public void testExecuteReplicasNotAllocatedOnSingleNode() { Index index = new Index(randomAlphaOfLengthBetween(1, 20), randomAlphaOfLengthBetween(1, 20)); - Map requires = Collections.singletonMap("_id", "node1"); + Map requires = Map.of("_id", "node1"); Settings.Builder existingSettings = Settings.builder() .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()) .put(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_PREFIX + "._id", "node1") @@ -376,7 +376,7 @@ public void testExecuteReplicasNotAllocatedOnSingleNode() { public void testExecuteReplicasButCopiesNotPresent() { Index index = new Index(randomAlphaOfLengthBetween(1, 20), randomAlphaOfLengthBetween(1, 20)); - Map requires = Collections.singletonMap("_id", "node1"); + Map requires = Map.of("_id", "node1"); Settings.Builder existingSettings = Settings.builder() .put(IndexMetadata.SETTING_VERSION_CREATED, IndexVersion.current()) .put(IndexMetadata.INDEX_ROUTING_REQUIRE_GROUP_PREFIX + "._id", "node1") @@ -458,7 +458,7 @@ public void testStepCompletableIfAllShardsActive() { .putCustom( NodesShutdownMetadata.TYPE, new NodesShutdownMetadata( - Collections.singletonMap( + Map.of( "node1", SingleNodeShutdownMetadata.builder() .setType(type) @@ -537,7 +537,7 @@ public void testStepBecomesUncompletable() { .putCustom( NodesShutdownMetadata.TYPE, new NodesShutdownMetadata( - Collections.singletonMap( + Map.of( "node1", SingleNodeShutdownMetadata.builder() .setType(type) @@ -649,7 +649,7 @@ public static UnassignedInfo randomUnassignedInfo(String message) { System.currentTimeMillis(), delayed, UnassignedInfo.AllocationStatus.NO_ATTEMPT, - Collections.emptySet(), + Set.of(), lastAllocatedNodeId ); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseFollowerIndexStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseFollowerIndexStepTests.java index 7ce078826b49a..ef7325be0a496 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseFollowerIndexStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseFollowerIndexStepTests.java @@ -13,7 +13,8 @@ import org.elasticsearch.index.IndexVersion; import org.mockito.Mockito; -import java.util.Collections; +import java.util.List; +import java.util.Map; import static org.elasticsearch.xpack.core.ilm.UnfollowAction.CCR_METADATA_KEY; import static org.hamcrest.Matchers.equalTo; @@ -24,7 +25,7 @@ public class CloseFollowerIndexStepTests extends AbstractStepTestCase listener = (ActionListener) invocation.getArguments()[1]; - listener.onResponse(new CloseIndexResponse(true, true, Collections.emptyList())); + listener.onResponse(new CloseIndexResponse(true, true, List.of())); return null; }).when(indicesClient).close(Mockito.any(), Mockito.any()); @@ -54,7 +55,7 @@ public void testRequestNotAcknowledged() { assertThat(closeIndexRequest.indices()[0], equalTo("follower-index")); @SuppressWarnings("unchecked") ActionListener listener = (ActionListener) invocation.getArguments()[1]; - listener.onResponse(new CloseIndexResponse(false, false, Collections.emptyList())); + listener.onResponse(new CloseIndexResponse(false, false, List.of())); return null; }).when(indicesClient).close(Mockito.any(), Mockito.any()); @@ -85,7 +86,7 @@ public void testCloseFollowingIndexFailed() { public void testCloseFollowerIndexIsNoopForAlreadyClosedIndex() throws Exception { IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true")) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .state(IndexMetadata.State.CLOSE) .numberOfShards(1) .numberOfReplicas(0) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTests.java index 02fb49ac71adf..b546aeaa20687 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/CloseIndexStepTests.java @@ -20,7 +20,7 @@ import org.mockito.Mockito; import org.mockito.stubbing.Answer; -import java.util.Collections; +import java.util.List; import static org.hamcrest.Matchers.equalTo; @@ -77,9 +77,7 @@ public void testPerformAction() { @SuppressWarnings("unchecked") ActionListener listener = (ActionListener) invocation.getArguments()[1]; assertThat(request.indices(), equalTo(new String[] { indexMetadata.getIndex().getName() })); - listener.onResponse( - new CloseIndexResponse(true, true, Collections.singletonList(new CloseIndexResponse.IndexResult(indexMetadata.getIndex()))) - ); + listener.onResponse(new CloseIndexResponse(true, true, List.of(new CloseIndexResponse.IndexResult(indexMetadata.getIndex())))); return null; }).when(indicesClient).close(Mockito.any(), Mockito.any()); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStepTests.java index f24f0f86de7db..eeddda4199665 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ClusterStateWaitUntilThresholdStepTests.java @@ -20,7 +20,6 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneId; -import java.util.Collections; import java.util.Map; import java.util.UUID; @@ -83,7 +82,7 @@ public void testIsConditionMetForUnderlyingStep() { .put(LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD, "480h") ) .putCustom(ILM_CUSTOM_METADATA_KEY, Map.of("step_time", String.valueOf(System.currentTimeMillis()))) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); @@ -107,7 +106,7 @@ public void testIsConditionMetForUnderlyingStep() { .put(LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD, "48h") ) .putCustom(ILM_CUSTOM_METADATA_KEY, Map.of("step_time", String.valueOf(System.currentTimeMillis()))) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); @@ -140,7 +139,7 @@ public void testIsConditionMetForUnderlyingStep() { settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true") .put(LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD, "1s") ) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .putCustom(ILM_CUSTOM_METADATA_KEY, Map.of("step_time", String.valueOf(1234L))) .numberOfShards(1) .numberOfReplicas(0) @@ -168,7 +167,7 @@ public void testIsConditionMetForUnderlyingStep() { settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "false") .put(LifecycleSettings.LIFECYCLE_STEP_WAIT_TIME_THRESHOLD, "1h") ) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .putCustom(ILM_CUSTOM_METADATA_KEY, Map.of("step_time", String.valueOf(1234L))) .numberOfShards(1) .numberOfReplicas(0) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ExplainLifecycleResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ExplainLifecycleResponseTests.java index 937502281b64d..c4138d228719e 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ExplainLifecycleResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ExplainLifecycleResponseTests.java @@ -18,7 +18,6 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -81,7 +80,7 @@ protected boolean assertToXContentEquivalence() { protected NamedWriteableRegistry getNamedWriteableRegistry() { return new NamedWriteableRegistry( - Arrays.asList(new NamedWriteableRegistry.Entry(LifecycleAction.class, MockAction.NAME, MockAction::new)) + List.of(new NamedWriteableRegistry.Entry(LifecycleAction.class, MockAction.NAME, MockAction::new)) ); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java index aecf029a1357a..b8d480200fb5d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ForceMergeActionTests.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.util.List; -import java.util.stream.Collectors; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; @@ -109,7 +108,7 @@ private void assertBestCompression(ForceMergeAction instance) { // available .skip(1) .map(s -> new Tuple<>(s.getKey(), s.getNextStepKey())) - .collect(Collectors.toList()); + .toList(); StepKey checkNotWriteIndex = new StepKey(phase, ForceMergeAction.NAME, CheckNotDataStreamWriteIndexStep.NAME); StepKey waitTimeSeriesEndTimePassesKey = new StepKey(phase, ForceMergeAction.NAME, WaitUntilTimeSeriesEndTimePassesStep.NAME); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStepTests.java index bee6351582bc9..908e7b764f136 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/GenerateSnapshotNameStepTests.java @@ -17,7 +17,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.IndexVersion; -import java.util.Collections; +import java.util.List; import java.util.Locale; import static org.elasticsearch.cluster.metadata.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; @@ -82,7 +82,7 @@ private void testPerformAction(String policyName, String expectedPolicyName) { .metadata( Metadata.builder() .put(indexMetadata, false) - .putCustom(RepositoriesMetadata.TYPE, new RepositoriesMetadata(Collections.singletonList(repo))) + .putCustom(RepositoriesMetadata.TYPE, new RepositoriesMetadata(List.of(repo))) .build() ) .build(); @@ -167,7 +167,7 @@ public void testPerformActionWillOverwriteCachedRepository() { .metadata( Metadata.builder() .put(indexMetadata, false) - .putCustom(RepositoriesMetadata.TYPE, new RepositoriesMetadata(Collections.singletonList(repo))) + .putCustom(RepositoriesMetadata.TYPE, new RepositoriesMetadata(List.of(repo))) .build() ) .build(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponseTests.java index ea3c9cc5926ab..6fc98d4c2c728 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/IndexLifecycleExplainResponseTests.java @@ -23,7 +23,7 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; -import java.util.Arrays; +import java.util.List; import java.util.Objects; import java.util.function.Supplier; @@ -292,7 +292,7 @@ protected IndexLifecycleExplainResponse mutateInstance(IndexLifecycleExplainResp protected NamedWriteableRegistry getNamedWriteableRegistry() { return new NamedWriteableRegistry( - Arrays.asList(new NamedWriteableRegistry.Entry(LifecycleAction.class, MockAction.NAME, MockAction::new)) + List.of(new NamedWriteableRegistry.Entry(LifecycleAction.class, MockAction.NAME, MockAction::new)) ); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyClientTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyClientTests.java index 753edfbe334b9..7dd6bfd209660 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyClientTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyClientTests.java @@ -21,7 +21,6 @@ import org.elasticsearch.xpack.core.ClientHelper; import org.mockito.Mockito; -import java.util.Collections; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -56,7 +55,7 @@ public void testExecuteWithHeadersAsyncNoHeaders() throws InterruptedException { SearchRequest request = new SearchRequest("foo"); - final var policyClient = new LifecyclePolicySecurityClient(client, ClientHelper.INDEX_LIFECYCLE_ORIGIN, Collections.emptyMap()); + final var policyClient = new LifecyclePolicySecurityClient(client, ClientHelper.INDEX_LIFECYCLE_ORIGIN, Map.of()); policyClient.execute(TransportSearchAction.TYPE, request, listener); latch.await(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyMetadataTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyMetadataTests.java index 3e9fd0105feae..b58d7184f741c 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyMetadataTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyMetadataTests.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -37,7 +36,7 @@ public void setup() { @Override protected NamedWriteableRegistry getNamedWriteableRegistry() { return new NamedWriteableRegistry( - Arrays.asList( + List.of( new NamedWriteableRegistry.Entry(LifecycleAction.class, MockAction.NAME, MockAction::new), new NamedWriteableRegistry.Entry( LifecycleType.class, @@ -65,7 +64,7 @@ protected NamedWriteableRegistry getNamedWriteableRegistry() { protected NamedXContentRegistry xContentRegistry() { List entries = new ArrayList<>(ClusterModule.getNamedXWriteables()); entries.addAll( - Arrays.asList( + List.of( new NamedXContentRegistry.Entry( LifecycleType.class, new ParseField(TimeseriesLifecycleType.TYPE), diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java index 70f75f1cfcdfa..1bea0ac6d192c 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyTests.java @@ -20,8 +20,6 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -30,7 +28,6 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Function; -import java.util.stream.Collectors; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -48,7 +45,7 @@ protected LifecyclePolicy doParseInstance(XContentParser parser) { @Override protected NamedWriteableRegistry getNamedWriteableRegistry() { return new NamedWriteableRegistry( - Arrays.asList( + List.of( new NamedWriteableRegistry.Entry( LifecycleType.class, TimeseriesLifecycleType.TYPE, @@ -75,7 +72,7 @@ protected NamedWriteableRegistry getNamedWriteableRegistry() { protected NamedXContentRegistry xContentRegistry() { List entries = new ArrayList<>(ClusterModule.getNamedXWriteables()); entries.addAll( - Arrays.asList( + List.of( new NamedXContentRegistry.Entry( LifecycleType.class, new ParseField(TimeseriesLifecycleType.TYPE), @@ -150,7 +147,7 @@ public static LifecyclePolicy randomTimeseriesLifecyclePolicy(@Nullable String l ).stream() // Remove the frozen phase, we'll randomly re-add it later .filter(pn -> TimeseriesLifecycleType.FROZEN_PHASE.equals(pn) == false) - .collect(Collectors.toList()); + .toList(); // let's order the phases so we can reason about actions in a previous phase in order to generate a random *valid* policy List orderedPhases = new ArrayList<>(phaseNames.size()); @@ -218,7 +215,7 @@ public static LifecyclePolicy randomTimeseriesLifecyclePolicy(@Nullable String l new Phase( TimeseriesLifecycleType.FROZEN_PHASE, frozenTime, - Collections.singletonMap( + Map.of( SearchableSnapshotAction.NAME, new SearchableSnapshotAction( randomAlphaOfLength(10), @@ -300,11 +297,11 @@ protected LifecyclePolicy mutateInstance(LifecyclePolicy instance) { () -> randomFrom( TimeseriesLifecycleType.ORDERED_VALID_PHASES.stream() .filter(pn -> TimeseriesLifecycleType.FROZEN_PHASE.equals(pn) == false) - .collect(Collectors.toList()) + .toList() ) ); phases = new LinkedHashMap<>(phases); - phases.put(phaseName, new Phase(phaseName, null, Collections.emptyMap())); + phases.put(phaseName, new Phase(phaseName, null, Map.of())); } case 2 -> metadata = randomValueOtherThan(metadata, LifecyclePolicyTests::randomMeta); case 3 -> deprecated = instance.isDeprecated() ? randomFrom(false, null) : true; @@ -337,8 +334,8 @@ public void testToStepsWithOneStep() { lifecycleName = randomAlphaOfLengthBetween(1, 20); Map phases = new LinkedHashMap<>(); - LifecycleAction firstAction = new MockAction(Arrays.asList(mockStep)); - Map actions = Collections.singletonMap(MockAction.NAME, firstAction); + LifecycleAction firstAction = new MockAction(List.of(mockStep)); + Map actions = Map.of(MockAction.NAME, firstAction); Phase firstPhase = new Phase("test", TimeValue.ZERO, actions); phases.put(firstPhase.getName(), firstPhase); LifecyclePolicy policy = new LifecyclePolicy(TestLifecycleType.INSTANCE, lifecycleName, phases, randomMeta()); @@ -372,10 +369,10 @@ public void testToStepsWithTwoPhases() { lifecycleName = randomAlphaOfLengthBetween(1, 20); Map phases = new LinkedHashMap<>(); - LifecycleAction firstAction = new MockAction(Arrays.asList(firstActionStep, firstActionAnotherStep)); - LifecycleAction secondAction = new MockAction(Arrays.asList(secondActionStep)); - Map firstActions = Collections.singletonMap(MockAction.NAME, firstAction); - Map secondActions = Collections.singletonMap(MockAction.NAME, secondAction); + LifecycleAction firstAction = new MockAction(List.of(firstActionStep, firstActionAnotherStep)); + LifecycleAction secondAction = new MockAction(List.of(secondActionStep)); + Map firstActions = Map.of(MockAction.NAME, firstAction); + Map secondActions = Map.of(MockAction.NAME, secondAction); Phase firstPhase = new Phase("first_phase", TimeValue.ZERO, firstActions); Phase secondPhase = new Phase("second_phase", TimeValue.ZERO, secondActions); phases.put(firstPhase.getName(), firstPhase); @@ -401,10 +398,10 @@ public void testToStepsWithTwoPhases() { public void testIsActionSafe() { Map phases = new LinkedHashMap<>(); - LifecycleAction firstAction = new MockAction(Collections.emptyList(), true); - LifecycleAction secondAction = new MockAction(Collections.emptyList(), false); - Map firstActions = Collections.singletonMap(MockAction.NAME, firstAction); - Map secondActions = Collections.singletonMap(MockAction.NAME, secondAction); + LifecycleAction firstAction = new MockAction(List.of(), true); + LifecycleAction secondAction = new MockAction(List.of(), false); + Map firstActions = Map.of(MockAction.NAME, firstAction); + Map secondActions = Map.of(MockAction.NAME, secondAction); Phase firstPhase = new Phase("first_phase", TimeValue.ZERO, firstActions); Phase secondPhase = new Phase("second_phase", TimeValue.ZERO, secondActions); phases.put(firstPhase.getName(), firstPhase); @@ -458,12 +455,9 @@ public void testValidatePolicyName() { public static Map randomMeta() { if (randomBoolean()) { if (randomBoolean()) { - return Collections.singletonMap(randomAlphaOfLength(4), randomAlphaOfLength(4)); + return Map.of(randomAlphaOfLength(4), randomAlphaOfLength(4)); } else { - return Collections.singletonMap( - randomAlphaOfLength(5), - Collections.singletonMap(randomAlphaOfLength(4), randomAlphaOfLength(4)) - ); + return Map.of(randomAlphaOfLength(5), Map.of(randomAlphaOfLength(4), randomAlphaOfLength(4))); } } else { return null; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyUtilsTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyUtilsTests.java index 3efe2dc04ea19..978486c6c0d39 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyUtilsTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyUtilsTests.java @@ -23,8 +23,8 @@ import org.elasticsearch.indices.EmptySystemIndices; import org.elasticsearch.test.ESTestCase; -import java.util.Arrays; -import java.util.Collections; +import java.util.List; +import java.util.Map; import static org.hamcrest.Matchers.equalTo; @@ -40,7 +40,7 @@ public void testCalculateUsage() { ClusterState state = ClusterState.builder(new ClusterName("mycluster")).build(); assertThat( LifecyclePolicyUtils.calculateUsage(iner, state, "mypolicy"), - equalTo(new ItemUsage(Collections.emptyList(), Collections.emptyList(), Collections.emptyList())) + equalTo(new ItemUsage(List.of(), List.of(), List.of())) ); } @@ -52,7 +52,7 @@ public void testCalculateUsage() { .putCustom( IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( - Collections.singletonMap("mypolicy", LifecyclePolicyMetadataTests.createRandomPolicyMetadata("mypolicy")), + Map.of("mypolicy", LifecyclePolicyMetadataTests.createRandomPolicyMetadata("mypolicy")), OperationMode.RUNNING ) ) @@ -61,7 +61,7 @@ public void testCalculateUsage() { .build(); assertThat( LifecyclePolicyUtils.calculateUsage(iner, state, "mypolicy"), - equalTo(new ItemUsage(Collections.emptyList(), Collections.emptyList(), Collections.emptyList())) + equalTo(new ItemUsage(List.of(), List.of(), List.of())) ); } @@ -73,7 +73,7 @@ public void testCalculateUsage() { .putCustom( IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( - Collections.singletonMap("mypolicy", LifecyclePolicyMetadataTests.createRandomPolicyMetadata("mypolicy")), + Map.of("mypolicy", LifecyclePolicyMetadataTests.createRandomPolicyMetadata("mypolicy")), OperationMode.RUNNING ) ) @@ -86,7 +86,7 @@ public void testCalculateUsage() { .build(); assertThat( LifecyclePolicyUtils.calculateUsage(iner, state, "mypolicy"), - equalTo(new ItemUsage(Collections.singleton("myindex"), Collections.emptyList(), Collections.emptyList())) + equalTo(new ItemUsage(List.of("myindex"), List.of(), List.of())) ); } @@ -98,7 +98,7 @@ public void testCalculateUsage() { .putCustom( IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( - Collections.singletonMap("mypolicy", LifecyclePolicyMetadataTests.createRandomPolicyMetadata("mypolicy")), + Map.of("mypolicy", LifecyclePolicyMetadataTests.createRandomPolicyMetadata("mypolicy")), OperationMode.RUNNING ) ) @@ -109,10 +109,10 @@ public void testCalculateUsage() { .putCustom( ComposableIndexTemplateMetadata.TYPE, new ComposableIndexTemplateMetadata( - Collections.singletonMap( + Map.of( "mytemplate", ComposableIndexTemplate.builder() - .indexPatterns(Collections.singletonList("myds")) + .indexPatterns(List.of("myds")) .template( new Template( Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, "mypolicy").build(), @@ -130,7 +130,7 @@ public void testCalculateUsage() { .build(); assertThat( LifecyclePolicyUtils.calculateUsage(iner, state, "mypolicy"), - equalTo(new ItemUsage(Collections.singleton("myindex"), Collections.emptyList(), Collections.singleton("mytemplate"))) + equalTo(new ItemUsage(List.of("myindex"), List.of(), List.of("mytemplate"))) ); } @@ -139,7 +139,7 @@ public void testCalculateUsage() { .putCustom( IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( - Collections.singletonMap("mypolicy", LifecyclePolicyMetadataTests.createRandomPolicyMetadata("mypolicy")), + Map.of("mypolicy", LifecyclePolicyMetadataTests.createRandomPolicyMetadata("mypolicy")), OperationMode.RUNNING ) ) @@ -159,10 +159,10 @@ public void testCalculateUsage() { .putCustom( ComposableIndexTemplateMetadata.TYPE, new ComposableIndexTemplateMetadata( - Collections.singletonMap( + Map.of( "mytemplate", ComposableIndexTemplate.builder() - .indexPatterns(Collections.singletonList("myds")) + .indexPatterns(List.of("myds")) .template( new Template(Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, "mypolicy").build(), null, null) ) @@ -172,15 +172,13 @@ public void testCalculateUsage() { ) ); // Need to get the real Index instance of myindex: - mBuilder.put(DataStreamTestHelper.newInstance("myds", Collections.singletonList(mBuilder.get("myindex").getIndex()))); + mBuilder.put(DataStreamTestHelper.newInstance("myds", List.of(mBuilder.get("myindex").getIndex()))); // Test where policy exists and is used by an index, datastream, and template ClusterState state = ClusterState.builder(new ClusterName("mycluster")).metadata(mBuilder.build()).build(); assertThat( LifecyclePolicyUtils.calculateUsage(iner, state, "mypolicy"), - equalTo( - new ItemUsage(Arrays.asList("myindex", "another"), Collections.singleton("myds"), Collections.singleton("mytemplate")) - ) + equalTo(new ItemUsage(List.of("myindex", "another"), List.of("myds"), List.of("mytemplate"))) ); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MockAction.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MockAction.java index 0de234615f4c7..79f8a051abe25 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MockAction.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/MockAction.java @@ -15,10 +15,8 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; public class MockAction implements LifecycleAction { public static final String NAME = "TEST_ACTION"; @@ -32,7 +30,7 @@ public static MockAction parse(XContentParser parser) { } public MockAction() { - this(Collections.emptyList()); + this(List.of()); } public MockAction(List steps) { @@ -77,7 +75,7 @@ public List toSteps(Client client, String phase, Step.StepKey nextStepKey) @Override public void writeTo(StreamOutput out) throws IOException { - out.writeCollection(steps.stream().map(MockStep::new).collect(Collectors.toList())); + out.writeCollection(steps.stream().map(MockStep::new).toList()); out.writeBoolean(safe); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTaskTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTaskTests.java index 9871cb79b595b..475161676f2e8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTaskTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/OperationModeUpdateTaskTests.java @@ -14,7 +14,6 @@ import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; import org.elasticsearch.xpack.core.slm.SnapshotLifecycleStats; -import java.util.Collections; import java.util.Map; import java.util.Optional; @@ -97,9 +96,9 @@ private OperationMode executeILMUpdate( OperationMode requestMode, boolean assertSameClusterState ) { - IndexLifecycleMetadata indexLifecycleMetadata = new IndexLifecycleMetadata(Collections.emptyMap(), currentMode); + IndexLifecycleMetadata indexLifecycleMetadata = new IndexLifecycleMetadata(Map.of(), currentMode); SnapshotLifecycleMetadata snapshotLifecycleMetadata = new SnapshotLifecycleMetadata( - Collections.emptyMap(), + Map.of(), currentMode, new SnapshotLifecycleStats() ); @@ -131,9 +130,9 @@ private OperationMode executeSLMUpdate( OperationMode requestMode, boolean assertSameClusterState ) { - IndexLifecycleMetadata indexLifecycleMetadata = new IndexLifecycleMetadata(Collections.emptyMap(), currentMode); + IndexLifecycleMetadata indexLifecycleMetadata = new IndexLifecycleMetadata(Map.of(), currentMode); SnapshotLifecycleMetadata snapshotLifecycleMetadata = new SnapshotLifecycleMetadata( - Collections.emptyMap(), + Map.of(), currentMode, new SnapshotLifecycleStats() ); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PauseFollowerIndexStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PauseFollowerIndexStepTests.java index 51ebc98176955..da5d6eddfc72d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PauseFollowerIndexStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PauseFollowerIndexStepTests.java @@ -22,7 +22,7 @@ import org.elasticsearch.xpack.core.ccr.action.ShardFollowTask; import org.mockito.Mockito; -import java.util.Collections; +import java.util.Map; import static org.elasticsearch.xpack.core.ilm.UnfollowAction.CCR_METADATA_KEY; import static org.hamcrest.Matchers.equalTo; @@ -38,7 +38,7 @@ protected PauseFollowerIndexStep newInstance(Step.StepKey key, Step.StepKey next public void testPauseFollowingIndex() throws Exception { IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true")) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); @@ -60,7 +60,7 @@ public void testPauseFollowingIndex() throws Exception { public void testRequestNotAcknowledged() { IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true")) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); @@ -81,7 +81,7 @@ public void testRequestNotAcknowledged() { public void testPauseFollowingIndexFailed() { IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true")) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); @@ -107,7 +107,7 @@ public void testPauseFollowingIndexFailed() { public final void testNoShardFollowPersistentTasks() throws Exception { IndexMetadata indexMetadata = IndexMetadata.builder("managed-index") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true")) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); @@ -138,7 +138,7 @@ public final void testNoShardFollowTasksForManagedIndex() throws Exception { IndexMetadata followerIndex = IndexMetadata.builder("follower-index") .settings(settings(IndexVersion.current())) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); @@ -171,7 +171,7 @@ private static ClusterState setupClusterStateWithFollowingIndex(IndexMetadata fo new ByteSizeValue(512, ByteSizeUnit.MB), TimeValue.timeValueMillis(10), TimeValue.timeValueMillis(10), - Collections.emptyMap() + Map.of() ), null ); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagementTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagementTests.java index 952741032fc90..7e78a81776a7a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagementTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseCacheManagementTests.java @@ -21,7 +21,6 @@ import org.elasticsearch.xcontent.ParseField; import java.io.IOException; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -84,9 +83,9 @@ public void testRefreshPhaseJson() throws IOException { actions.put("rollover", new RolloverAction(null, null, null, 1L, null, null, null, null, null, null)); actions.put("set_priority", new SetPriorityAction(100)); Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); - Map phases = Collections.singletonMap("hot", hotPhase); + Map phases = Map.of("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); - LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(newPolicy, Collections.emptyMap(), 2L, 2L); + LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(newPolicy, Map.of(), 2L, 2L); ClusterState existingState = ClusterState.builder(ClusterState.EMPTY_STATE) .metadata(Metadata.builder(Metadata.EMPTY_METADATA).put(meta, false).build()) @@ -315,7 +314,7 @@ public void testIndexCanBeSafelyUpdated() { actions.put("rollover", new RolloverAction(null, null, null, 1L, null, null, null, null, null, null)); actions.put("set_priority", new SetPriorityAction(100)); Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); - Map phases = Collections.singletonMap("hot", hotPhase); + Map phases = Map.of("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); assertTrue(isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy, null)); @@ -351,7 +350,7 @@ public void testIndexCanBeSafelyUpdated() { Map actions = new HashMap<>(); actions.put("set_priority", new SetPriorityAction(150)); Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); - Map phases = Collections.singletonMap("hot", hotPhase); + Map phases = Map.of("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); assertFalse(isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy, null)); @@ -390,7 +389,7 @@ public void testIndexCanBeSafelyUpdated() { new RolloverAction(null, null, TimeValue.timeValueSeconds(5), null, null, null, null, null, null, null) ); Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); - Map phases = Collections.singletonMap("hot", hotPhase); + Map phases = Map.of("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); assertFalse(isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy, null)); @@ -422,7 +421,7 @@ public void testIndexCanBeSafelyUpdated() { actions.put("rollover", new RolloverAction(null, null, null, 1L, null, null, null, null, null, null)); actions.put("set_priority", new SetPriorityAction(100)); Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); - Map phases = Collections.singletonMap("hot", hotPhase); + Map phases = Map.of("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); assertFalse(isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy, null)); @@ -443,7 +442,7 @@ public void testIndexCanBeSafelyUpdated() { actions.put("rollover", new RolloverAction(null, null, null, 1L, null, null, null, null, null, null)); actions.put("set_priority", new SetPriorityAction(100)); Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); - Map phases = Collections.singletonMap("hot", hotPhase); + Map phases = Map.of("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); assertFalse(isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy, null)); @@ -482,16 +481,16 @@ public void testUpdateIndicesForPolicy() throws IOException { oldActions.put("rollover", new RolloverAction(null, null, null, 1L, null, null, null, null, null, null)); oldActions.put("set_priority", new SetPriorityAction(100)); Phase oldHotPhase = new Phase("hot", TimeValue.ZERO, oldActions); - Map oldPhases = Collections.singletonMap("hot", oldHotPhase); + Map oldPhases = Map.of("hot", oldHotPhase); LifecyclePolicy oldPolicy = new LifecyclePolicy("my-policy", oldPhases); Map actions = new HashMap<>(); actions.put("rollover", new RolloverAction(null, null, null, 1L, null, null, null, null, null, null)); actions.put("set_priority", new SetPriorityAction(100)); Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); - Map phases = Collections.singletonMap("hot", hotPhase); + Map phases = Map.of("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); - LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(newPolicy, Collections.emptyMap(), 2L, 2L); + LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(newPolicy, Map.of(), 2L, 2L); assertTrue(isIndexPhaseDefinitionUpdatable(REGISTRY, client, meta, newPolicy, null)); @@ -509,9 +508,9 @@ public void testUpdateIndicesForPolicy() throws IOException { actions.put("rollover", new RolloverAction(null, null, null, 2L, null, null, null, null, null, null)); actions.put("set_priority", new SetPriorityAction(150)); hotPhase = new Phase("hot", TimeValue.ZERO, actions); - phases = Collections.singletonMap("hot", hotPhase); + phases = Map.of("hot", hotPhase); newPolicy = new LifecyclePolicy("my-policy", phases); - policyMetadata = new LifecyclePolicyMetadata(newPolicy, Collections.emptyMap(), 2L, 2L); + policyMetadata = new LifecyclePolicyMetadata(newPolicy, Map.of(), 2L, 2L); logger.info("--> update with changed policy, but not configured in settings"); updatedState = updateIndicesForPolicy(existingState, REGISTRY, client, oldPolicy, policyMetadata, null); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseExecutionInfoTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseExecutionInfoTests.java index 7622118d2b99f..ce477a07c2f0b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseExecutionInfoTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseExecutionInfoTests.java @@ -18,7 +18,7 @@ import org.junit.Before; import java.io.IOException; -import java.util.Arrays; +import java.util.List; public class PhaseExecutionInfoTests extends AbstractXContentSerializingTestCase { @@ -71,7 +71,7 @@ protected PhaseExecutionInfo mutateInstance(PhaseExecutionInfo instance) { protected NamedWriteableRegistry getNamedWriteableRegistry() { return new NamedWriteableRegistry( - Arrays.asList(new NamedWriteableRegistry.Entry(LifecycleAction.class, MockAction.NAME, MockAction::new)) + List.of(new NamedWriteableRegistry.Entry(LifecycleAction.class, MockAction.NAME, MockAction::new)) ); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseTests.java index bf925c4282fc1..5a194b48f7701 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/PhaseTests.java @@ -18,9 +18,8 @@ import org.junit.Before; import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -42,9 +41,9 @@ static Phase randomTestPhase(String phaseName) { if (randomBoolean()) { after = randomTimeValue(0, 1_000_000_000, TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS); } - Map actions = Collections.emptyMap(); + Map actions = Map.of(); if (randomBoolean()) { - actions = Collections.singletonMap(MockAction.NAME, new MockAction()); + actions = Map.of(MockAction.NAME, new MockAction()); } return new Phase(phaseName, after, actions); } @@ -61,7 +60,7 @@ protected Reader instanceReader() { protected NamedWriteableRegistry getNamedWriteableRegistry() { return new NamedWriteableRegistry( - Arrays.asList(new NamedWriteableRegistry.Entry(LifecycleAction.class, MockAction.NAME, MockAction::new)) + List.of(new NamedWriteableRegistry.Entry(LifecycleAction.class, MockAction.NAME, MockAction::new)) ); } @@ -85,7 +84,7 @@ protected Phase mutateInstance(Phase instance) { case 1 -> after = TimeValue.timeValueSeconds(after.getSeconds() + randomIntBetween(1, 1000)); case 2 -> { actions = new HashMap<>(actions); - actions.put(MockAction.NAME + "another", new MockAction(Collections.emptyList())); + actions.put(MockAction.NAME + "another", new MockAction(List.of())); } default -> throw new AssertionError("Illegal randomisation branch"); } @@ -93,7 +92,7 @@ protected Phase mutateInstance(Phase instance) { } public void testDefaultAfter() { - Phase phase = new Phase(randomAlphaOfLength(20), null, Collections.emptyMap()); + Phase phase = new Phase(randomAlphaOfLength(20), null, Map.of()); assertEquals(TimeValue.ZERO, phase.getMinimumAge()); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/RolloverStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/RolloverStepTests.java index 4af25d094f5fe..3683690763d93 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/RolloverStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/RolloverStepTests.java @@ -23,9 +23,9 @@ import org.hamcrest.Matchers; import org.mockito.Mockito; -import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.newInstance; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -185,7 +185,7 @@ private void mockClientRolloverCall(String rolloverTarget) { @SuppressWarnings("unchecked") ActionListener listener = (ActionListener) invocation.getArguments()[1]; assertRolloverIndexRequest(request, rolloverTarget); - listener.onResponse(new RolloverResponse(null, null, Collections.emptyMap(), request.isDryRun(), true, true, true, false)); + listener.onResponse(new RolloverResponse(null, null, Map.of(), request.isDryRun(), true, true, true, false)); return null; }).when(indicesClient).rolloverIndex(Mockito.any(), Mockito.any()); } @@ -214,11 +214,7 @@ public void testPerformActionSkipsRolloverForAlreadyRolledIndex() throws Excepti .putAlias(AliasMetadata.builder(rolloverAlias)) .settings(settings(IndexVersion.current()).put(RolloverAction.LIFECYCLE_ROLLOVER_ALIAS, rolloverAlias)) .putRolloverInfo( - new RolloverInfo( - rolloverAlias, - Collections.singletonList(new MaxSizeCondition(ByteSizeValue.ofBytes(2L))), - System.currentTimeMillis() - ) + new RolloverInfo(rolloverAlias, List.of(new MaxSizeCondition(ByteSizeValue.ofBytes(2L))), System.currentTimeMillis()) ) .numberOfShards(randomIntBetween(1, 5)) .numberOfReplicas(randomIntBetween(0, 5)) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SegmentCountStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SegmentCountStepTests.java index 1d14bfb261fc2..9f04e202022c9 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SegmentCountStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SegmentCountStepTests.java @@ -77,14 +77,14 @@ public void testIsConditionMet() { ShardSegments shardSegmentsOne = Mockito.mock(ShardSegments.class); ShardSegments[] shardSegmentsArray = new ShardSegments[] { shardSegmentsOne }; IndexShardSegments indexShardSegments = new IndexShardSegments(ShardId.fromString("[idx][123]"), shardSegmentsArray); - Map indexShards = Collections.singletonMap(0, indexShardSegments); + Map indexShards = Map.of(0, indexShardSegments); Spliterator iss = indexShards.values().spliterator(); List segments = new ArrayList<>(); for (int i = 0; i < maxNumSegments - randomIntBetween(0, 3); i++) { segments.add(null); } Mockito.when(indicesSegmentResponse.getStatus()).thenReturn(RestStatus.OK); - Mockito.when(indicesSegmentResponse.getIndices()).thenReturn(Collections.singletonMap(index.getName(), indexSegments)); + Mockito.when(indicesSegmentResponse.getIndices()).thenReturn(Map.of(index.getName(), indexSegments)); Mockito.when(indexSegments.spliterator()).thenReturn(iss); Mockito.when(shardSegmentsOne.getSegments()).thenReturn(segments); @@ -129,14 +129,14 @@ public void testIsConditionIsTrueEvenWhenMoreSegments() { ShardSegments shardSegmentsOne = Mockito.mock(ShardSegments.class); ShardSegments[] shardSegmentsArray = new ShardSegments[] { shardSegmentsOne }; IndexShardSegments indexShardSegments = new IndexShardSegments(ShardId.fromString("[idx][123]"), shardSegmentsArray); - Map indexShards = Collections.singletonMap(0, indexShardSegments); + Map indexShards = Map.of(0, indexShardSegments); Spliterator iss = indexShards.values().spliterator(); List segments = new ArrayList<>(); for (int i = 0; i < maxNumSegments + randomIntBetween(1, 3); i++) { segments.add(null); } Mockito.when(indicesSegmentResponse.getStatus()).thenReturn(RestStatus.OK); - Mockito.when(indicesSegmentResponse.getIndices()).thenReturn(Collections.singletonMap(index.getName(), indexSegments)); + Mockito.when(indicesSegmentResponse.getIndices()).thenReturn(Map.of(index.getName(), indexSegments)); Mockito.when(indexSegments.spliterator()).thenReturn(iss); Mockito.when(shardSegmentsOne.getSegments()).thenReturn(segments); @@ -181,7 +181,7 @@ public void testFailedToRetrieveSomeSegments() { ShardSegments shardSegmentsOne = Mockito.mock(ShardSegments.class); ShardSegments[] shardSegmentsArray = new ShardSegments[] { shardSegmentsOne }; IndexShardSegments indexShardSegments = new IndexShardSegments(ShardId.fromString("[idx][123]"), shardSegmentsArray); - Map indexShards = Collections.singletonMap(0, indexShardSegments); + Map indexShards = Map.of(0, indexShardSegments); Spliterator iss = indexShards.values().spliterator(); List segments = new ArrayList<>(); for (int i = 0; i < maxNumSegments + randomIntBetween(1, 3); i++) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkActionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkActionTests.java index a33d6e3332a40..60fa69708e111 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkActionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkActionTests.java @@ -28,8 +28,8 @@ import org.mockito.Mockito; import java.io.IOException; -import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -203,11 +203,11 @@ public void assertPerformAction( LifecyclePolicy policy = new LifecyclePolicy( lifecycleName, - Collections.singletonMap("warm", new Phase("warm", TimeValue.ZERO, Collections.singletonMap(action.getWriteableName(), action))) + Map.of("warm", new Phase("warm", TimeValue.ZERO, Map.of(action.getWriteableName(), action))) ); LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata( policy, - Collections.emptyMap(), + Map.of(), randomNonNegativeLong(), randomNonNegativeLong() ); @@ -216,10 +216,7 @@ public void assertPerformAction( Metadata.builder() .putCustom( IndexLifecycleMetadata.TYPE, - new IndexLifecycleMetadata( - Collections.singletonMap(policyMetadata.getName(), policyMetadata), - OperationMode.RUNNING - ) + new IndexLifecycleMetadata(Map.of(policyMetadata.getName(), policyMetadata), OperationMode.RUNNING) ) .put( indexMetadataBuilder.putCustom( diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStepTests.java index 7a03343b461de..c8efce288260f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkSetAliasStepTests.java @@ -17,7 +17,6 @@ import org.mockito.Mockito; import org.mockito.stubbing.Answer; -import java.util.Arrays; import java.util.List; import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; @@ -73,7 +72,7 @@ public void testPerformAction() throws Exception { String sourceIndex = indexMetadata.getIndex().getName(); String shrunkenIndex = SHRUNKEN_INDEX_PREFIX + sourceIndex; - List expectedAliasActions = Arrays.asList( + List expectedAliasActions = List.of( IndicesAliasesRequest.AliasActions.removeIndex().index(sourceIndex), IndicesAliasesRequest.AliasActions.add().index(shrunkenIndex).alias(sourceIndex), IndicesAliasesRequest.AliasActions.add() diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkStepTests.java index 257df32b4d950..b138339c25197 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/ShrinkStepTests.java @@ -21,8 +21,8 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import org.mockito.Mockito; -import java.util.Collections; import java.util.Map; +import java.util.Set; import static org.elasticsearch.cluster.metadata.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; import static org.elasticsearch.common.IndexNameGenerator.generateValidIndexName; @@ -101,7 +101,7 @@ public void testPerformAction() throws Exception { @SuppressWarnings("unchecked") ActionListener listener = (ActionListener) invocation.getArguments()[1]; assertThat(request.getSourceIndex(), equalTo(sourceIndexMetadata.getIndex().getName())); - assertThat(request.getTargetIndexRequest().aliases(), equalTo(Collections.emptySet())); + assertThat(request.getTargetIndexRequest().aliases(), equalTo(Set.of())); Settings.Builder builder = Settings.builder(); builder.put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, sourceIndexMetadata.getNumberOfReplicas()) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SwapAliasesAndDeleteSourceIndexStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SwapAliasesAndDeleteSourceIndexStepTests.java index f9f06b10ad2f9..1a99043b86ad7 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SwapAliasesAndDeleteSourceIndexStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/SwapAliasesAndDeleteSourceIndexStepTests.java @@ -23,7 +23,6 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.ilm.Step.StepKey; -import java.util.Arrays; import java.util.List; import java.util.function.BiFunction; @@ -92,7 +91,7 @@ public void testPerformAction() { String targetIndexPrefix = "index_prefix"; String targetIndexName = targetIndexPrefix + sourceIndexName; - List expectedAliasActions = Arrays.asList( + List expectedAliasActions = List.of( AliasActions.removeIndex().index(sourceIndexName), AliasActions.add().index(targetIndexName).alias(sourceIndexName), AliasActions.add() diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java index 55fa3792fa6c7..f7d1ff5294f58 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/TimeseriesLifecycleTypeTests.java @@ -13,9 +13,7 @@ import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -51,13 +49,7 @@ public class TimeseriesLifecycleTypeTests extends ESTestCase { - private static final AllocateAction TEST_ALLOCATE_ACTION = new AllocateAction( - 2, - 20, - Collections.singletonMap("node", "node1"), - null, - null - ); + private static final AllocateAction TEST_ALLOCATE_ACTION = new AllocateAction(2, 20, Map.of("node", "node1"), null, null); private static final DeleteAction TEST_DELETE_ACTION = DeleteAction.WITH_SNAPSHOT_DELETE; private static final WaitForSnapshotAction TEST_WAIT_FOR_SNAPSHOT_ACTION = new WaitForSnapshotAction("policy"); @@ -91,7 +83,7 @@ public void testValidatePhases() { if (invalid) { phaseName += randomAlphaOfLength(5); } - Map phases = Collections.singletonMap(phaseName, new Phase(phaseName, TimeValue.ZERO, Collections.emptyMap())); + Map phases = Map.of(phaseName, new Phase(phaseName, TimeValue.ZERO, Map.of())); if (invalid) { Exception e = expectThrows(IllegalArgumentException.class, () -> TimeseriesLifecycleType.INSTANCE.validate(phases.values())); assertThat(e.getMessage(), equalTo("Timeseries lifecycle does not support phase [" + phaseName + "]")); @@ -109,7 +101,7 @@ public void testValidateHotPhase() { invalidAction = getTestAction(randomFrom("allocate", "delete", "freeze")); actions.put(invalidAction.getWriteableName(), invalidAction); } - Map hotPhase = Collections.singletonMap("hot", new Phase("hot", TimeValue.ZERO, actions)); + Map hotPhase = Map.of("hot", new Phase("hot", TimeValue.ZERO, actions)); if (invalidAction != null) { Exception e = expectThrows(IllegalArgumentException.class, () -> TimeseriesLifecycleType.INSTANCE.validate(hotPhase.values())); @@ -123,14 +115,14 @@ public void testValidateHotPhase() { final Map hotActionMap = hotActions.stream() .map(this::getTestAction) .collect(Collectors.toMap(LifecycleAction::getWriteableName, Function.identity())); - TimeseriesLifecycleType.INSTANCE.validate(Collections.singleton(new Phase("hot", TimeValue.ZERO, hotActionMap))); + TimeseriesLifecycleType.INSTANCE.validate(List.of(new Phase("hot", TimeValue.ZERO, hotActionMap))); }; - validateHotActions.accept(Arrays.asList(RolloverAction.NAME)); - validateHotActions.accept(Arrays.asList(RolloverAction.NAME, ForceMergeAction.NAME)); + validateHotActions.accept(List.of(RolloverAction.NAME)); + validateHotActions.accept(List.of(RolloverAction.NAME, ForceMergeAction.NAME)); IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> validateHotActions.accept(Arrays.asList(ForceMergeAction.NAME)) + () -> validateHotActions.accept(List.of(ForceMergeAction.NAME)) ); assertThat( e.getMessage(), @@ -148,7 +140,7 @@ public void testValidateWarmPhase() { invalidAction = getTestAction(randomFrom("rollover", "delete", "freeze")); actions.put(invalidAction.getWriteableName(), invalidAction); } - Map warmPhase = Collections.singletonMap("warm", new Phase("warm", TimeValue.ZERO, actions)); + Map warmPhase = Map.of("warm", new Phase("warm", TimeValue.ZERO, actions)); if (invalidAction != null) { Exception e = expectThrows(IllegalArgumentException.class, () -> TimeseriesLifecycleType.INSTANCE.validate(warmPhase.values())); @@ -167,7 +159,7 @@ public void testValidateColdPhase() { invalidAction = getTestAction(randomFrom("rollover", "delete", "forcemerge", "shrink")); actions.put(invalidAction.getWriteableName(), invalidAction); } - Map coldPhase = Collections.singletonMap("cold", new Phase("cold", TimeValue.ZERO, actions)); + Map coldPhase = Map.of("cold", new Phase("cold", TimeValue.ZERO, actions)); if (invalidAction != null) { Exception e = expectThrows(IllegalArgumentException.class, () -> TimeseriesLifecycleType.INSTANCE.validate(coldPhase.values())); @@ -188,7 +180,7 @@ public void testValidateFrozenPhase() { invalidAction = getTestAction(randomFrom("rollover", "delete", "forcemerge", "shrink")); actions.put(invalidAction.getWriteableName(), invalidAction); } - Map frozenPhase = Collections.singletonMap("frozen", new Phase("frozen", TimeValue.ZERO, actions)); + Map frozenPhase = Map.of("frozen", new Phase("frozen", TimeValue.ZERO, actions)); if (invalidAction != null) { Exception e = expectThrows( @@ -210,7 +202,7 @@ public void testValidateDeletePhase() { invalidAction = getTestAction(randomFrom("allocate", "rollover", "forcemerge", "shrink", "freeze", "set_priority")); actions.put(invalidAction.getWriteableName(), invalidAction); } - Map deletePhase = Collections.singletonMap("delete", new Phase("delete", TimeValue.ZERO, actions)); + Map deletePhase = Map.of("delete", new Phase("delete", TimeValue.ZERO, actions)); if (invalidAction != null) { Exception e = expectThrows( @@ -459,7 +451,7 @@ public void testValidateDownsamplingAction() { public void testGetOrderedPhases() { Map phaseMap = new HashMap<>(); for (String phaseName : randomSubsetOf(randomIntBetween(0, ORDERED_VALID_PHASES.size()), ORDERED_VALID_PHASES)) { - phaseMap.put(phaseName, new Phase(phaseName, TimeValue.ZERO, Collections.emptyMap())); + phaseMap.put(phaseName, new Phase(phaseName, TimeValue.ZERO, Map.of())); } assertTrue(isSorted(TimeseriesLifecycleType.INSTANCE.getOrderedPhases(phaseMap), Phase::getName, ORDERED_VALID_PHASES)); @@ -509,7 +501,7 @@ private boolean isUnfollowInjected(String phaseName, String actionName) { public void testGetOrderedActionsInvalidPhase() { IllegalArgumentException exception = expectThrows( IllegalArgumentException.class, - () -> TimeseriesLifecycleType.INSTANCE.getOrderedActions(new Phase("invalid", TimeValue.ZERO, Collections.emptyMap())) + () -> TimeseriesLifecycleType.INSTANCE.getOrderedActions(new Phase("invalid", TimeValue.ZERO, Map.of())) ); assertThat(exception.getMessage(), equalTo("lifecycle type [timeseries] does not support phase [invalid]")); } @@ -583,25 +575,25 @@ public void testShouldMigrateDataToTiers() { { // not inject in hot phase - Phase phase = new Phase(HOT_PHASE, TimeValue.ZERO, Collections.emptyMap()); + Phase phase = new Phase(HOT_PHASE, TimeValue.ZERO, Map.of()); assertThat(TimeseriesLifecycleType.shouldInjectMigrateStepForPhase(phase), is(false)); } { // not inject in frozen phase - Phase phase = new Phase(FROZEN_PHASE, TimeValue.ZERO, Collections.emptyMap()); + Phase phase = new Phase(FROZEN_PHASE, TimeValue.ZERO, Map.of()); assertThat(TimeseriesLifecycleType.shouldInjectMigrateStepForPhase(phase), is(false)); } { // not inject in delete phase - Phase phase = new Phase(DELETE_PHASE, TimeValue.ZERO, Collections.emptyMap()); + Phase phase = new Phase(DELETE_PHASE, TimeValue.ZERO, Map.of()); assertThat(TimeseriesLifecycleType.shouldInjectMigrateStepForPhase(phase), is(false)); } { // return false for invalid phase - Phase phase = new Phase(HOT_PHASE + randomAlphaOfLength(5), TimeValue.ZERO, Collections.emptyMap()); + Phase phase = new Phase(HOT_PHASE + randomAlphaOfLength(5), TimeValue.ZERO, Map.of()); assertThat(TimeseriesLifecycleType.shouldInjectMigrateStepForPhase(phase), is(false)); } } @@ -620,7 +612,7 @@ public void testValidatingSearchableSnapshotRepos() { Phase coldPhase = new Phase(HOT_PHASE, TimeValue.ZERO, coldActions); Phase frozenPhase = new Phase(HOT_PHASE, TimeValue.ZERO, frozenActions); - validateAllSearchableSnapshotActionsUseSameRepository(Arrays.asList(hotPhase, coldPhase, frozenPhase)); + validateAllSearchableSnapshotActionsUseSameRepository(List.of(hotPhase, coldPhase, frozenPhase)); } { @@ -634,7 +626,7 @@ public void testValidatingSearchableSnapshotRepos() { IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> validateAllSearchableSnapshotActionsUseSameRepository(Arrays.asList(hotPhase, coldPhase, frozenPhase)) + () -> validateAllSearchableSnapshotActionsUseSameRepository(List.of(hotPhase, coldPhase, frozenPhase)) ); assertThat( e.getMessage(), @@ -649,25 +641,25 @@ public void testValidatingSearchableSnapshotRepos() { public void testValidatingIncreasingAges() { { - Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); - Phase warmPhase = new Phase(WARM_PHASE, TimeValue.ZERO, Collections.emptyMap()); - Phase coldPhase = new Phase(COLD_PHASE, TimeValue.ZERO, Collections.emptyMap()); - Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.ZERO, Collections.emptyMap()); - Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.ZERO, Collections.emptyMap()); + Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(1), Map.of()); + Phase warmPhase = new Phase(WARM_PHASE, TimeValue.ZERO, Map.of()); + Phase coldPhase = new Phase(COLD_PHASE, TimeValue.ZERO, Map.of()); + Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.ZERO, Map.of()); + Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.ZERO, Map.of()); assertFalse( Strings.hasText( - validateMonotonicallyIncreasingPhaseTimings(Arrays.asList(hotPhase, warmPhase, coldPhase, frozenPhase, deletePhase)) + validateMonotonicallyIncreasingPhaseTimings(List.of(hotPhase, warmPhase, coldPhase, frozenPhase, deletePhase)) ) ); } { - Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); - Phase warmPhase = new Phase(WARM_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); - Phase coldPhase = new Phase(COLD_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); - Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); - Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); + Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(1), Map.of()); + Phase warmPhase = new Phase(WARM_PHASE, TimeValue.timeValueDays(1), Map.of()); + Phase coldPhase = new Phase(COLD_PHASE, TimeValue.timeValueDays(1), Map.of()); + Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.timeValueDays(1), Map.of()); + Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.timeValueDays(1), Map.of()); List phases = new ArrayList<>(); phases.add(hotPhase); @@ -687,15 +679,13 @@ public void testValidatingIncreasingAges() { } { - Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); - Phase warmPhase = new Phase(WARM_PHASE, TimeValue.ZERO, Collections.emptyMap()); - Phase coldPhase = new Phase(COLD_PHASE, TimeValue.timeValueHours(12), Collections.emptyMap()); - Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.ZERO, Collections.emptyMap()); - Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.ZERO, Collections.emptyMap()); + Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(1), Map.of()); + Phase warmPhase = new Phase(WARM_PHASE, TimeValue.ZERO, Map.of()); + Phase coldPhase = new Phase(COLD_PHASE, TimeValue.timeValueHours(12), Map.of()); + Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.ZERO, Map.of()); + Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.ZERO, Map.of()); - String err = validateMonotonicallyIncreasingPhaseTimings( - Arrays.asList(hotPhase, warmPhase, coldPhase, frozenPhase, deletePhase) - ); + String err = validateMonotonicallyIncreasingPhaseTimings(List.of(hotPhase, warmPhase, coldPhase, frozenPhase, deletePhase)); assertThat( err, @@ -708,15 +698,13 @@ public void testValidatingIncreasingAges() { } { - Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); - Phase warmPhase = new Phase(WARM_PHASE, TimeValue.timeValueDays(3), Collections.emptyMap()); - Phase coldPhase = new Phase(COLD_PHASE, null, Collections.emptyMap()); - Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); - Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.timeValueDays(2), Collections.emptyMap()); + Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(1), Map.of()); + Phase warmPhase = new Phase(WARM_PHASE, TimeValue.timeValueDays(3), Map.of()); + Phase coldPhase = new Phase(COLD_PHASE, null, Map.of()); + Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.timeValueDays(1), Map.of()); + Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.timeValueDays(2), Map.of()); - String err = validateMonotonicallyIncreasingPhaseTimings( - Arrays.asList(hotPhase, warmPhase, coldPhase, frozenPhase, deletePhase) - ); + String err = validateMonotonicallyIncreasingPhaseTimings(List.of(hotPhase, warmPhase, coldPhase, frozenPhase, deletePhase)); assertThat( err, @@ -729,15 +717,13 @@ public void testValidatingIncreasingAges() { } { - Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); - Phase warmPhase = new Phase(WARM_PHASE, TimeValue.timeValueDays(3), Collections.emptyMap()); - Phase coldPhase = new Phase(COLD_PHASE, null, Collections.emptyMap()); - Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.timeValueDays(2), Collections.emptyMap()); - Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); + Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(1), Map.of()); + Phase warmPhase = new Phase(WARM_PHASE, TimeValue.timeValueDays(3), Map.of()); + Phase coldPhase = new Phase(COLD_PHASE, null, Map.of()); + Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.timeValueDays(2), Map.of()); + Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.timeValueDays(1), Map.of()); - String err = validateMonotonicallyIncreasingPhaseTimings( - Arrays.asList(hotPhase, warmPhase, coldPhase, frozenPhase, deletePhase) - ); + String err = validateMonotonicallyIncreasingPhaseTimings(List.of(hotPhase, warmPhase, coldPhase, frozenPhase, deletePhase)); assertThat( err, @@ -750,15 +736,13 @@ public void testValidatingIncreasingAges() { } { - Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(3), Collections.emptyMap()); - Phase warmPhase = new Phase(WARM_PHASE, TimeValue.timeValueDays(2), Collections.emptyMap()); - Phase coldPhase = new Phase(COLD_PHASE, null, Collections.emptyMap()); - Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.timeValueDays(2), Collections.emptyMap()); - Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); + Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(3), Map.of()); + Phase warmPhase = new Phase(WARM_PHASE, TimeValue.timeValueDays(2), Map.of()); + Phase coldPhase = new Phase(COLD_PHASE, null, Map.of()); + Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.timeValueDays(2), Map.of()); + Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.timeValueDays(1), Map.of()); - String err = validateMonotonicallyIncreasingPhaseTimings( - Arrays.asList(hotPhase, warmPhase, coldPhase, frozenPhase, deletePhase) - ); + String err = validateMonotonicallyIncreasingPhaseTimings(List.of(hotPhase, warmPhase, coldPhase, frozenPhase, deletePhase)); assertThat( err, @@ -772,15 +756,13 @@ public void testValidatingIncreasingAges() { } { - Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(3), Collections.emptyMap()); - Phase warmPhase = new Phase(WARM_PHASE, TimeValue.timeValueDays(2), Collections.emptyMap()); - Phase coldPhase = new Phase(COLD_PHASE, TimeValue.timeValueDays(2), Collections.emptyMap()); - Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.timeValueDays(2), Collections.emptyMap()); - Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.timeValueDays(1), Collections.emptyMap()); + Phase hotPhase = new Phase(HOT_PHASE, TimeValue.timeValueDays(3), Map.of()); + Phase warmPhase = new Phase(WARM_PHASE, TimeValue.timeValueDays(2), Map.of()); + Phase coldPhase = new Phase(COLD_PHASE, TimeValue.timeValueDays(2), Map.of()); + Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.timeValueDays(2), Map.of()); + Phase deletePhase = new Phase(DELETE_PHASE, TimeValue.timeValueDays(1), Map.of()); - String err = validateMonotonicallyIncreasingPhaseTimings( - Arrays.asList(hotPhase, warmPhase, coldPhase, frozenPhase, deletePhase) - ); + String err = validateMonotonicallyIncreasingPhaseTimings(List.of(hotPhase, warmPhase, coldPhase, frozenPhase, deletePhase)); assertThat( err, @@ -799,7 +781,7 @@ public void testValidateFrozenPhaseHasSearchableSnapshot() { Map frozenActions = new HashMap<>(); frozenActions.put(SearchableSnapshotAction.NAME, new SearchableSnapshotAction("repo1", randomBoolean())); Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.ZERO, frozenActions); - validateFrozenPhaseHasSearchableSnapshotAction(Collections.singleton(frozenPhase)); + validateFrozenPhaseHasSearchableSnapshotAction(List.of(frozenPhase)); } { @@ -807,7 +789,7 @@ public void testValidateFrozenPhaseHasSearchableSnapshot() { Phase frozenPhase = new Phase(FROZEN_PHASE, TimeValue.ZERO, frozenActions); IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> validateFrozenPhaseHasSearchableSnapshotAction(Collections.singleton(frozenPhase)) + () -> validateFrozenPhaseHasSearchableSnapshotAction(List.of(frozenPhase)) ); assertThat( e.getMessage(), diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/UnfollowFollowerIndexStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/UnfollowFollowerIndexStepTests.java index 71f7ea2925f16..8e40d3af86d81 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/UnfollowFollowerIndexStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/UnfollowFollowerIndexStepTests.java @@ -14,7 +14,7 @@ import org.elasticsearch.xpack.core.ccr.action.UnfollowAction; import org.mockito.Mockito; -import java.util.Collections; +import java.util.Map; import static org.elasticsearch.xpack.core.ilm.UnfollowAction.CCR_METADATA_KEY; import static org.hamcrest.Matchers.equalTo; @@ -30,7 +30,7 @@ protected UnfollowFollowerIndexStep newInstance(Step.StepKey key, Step.StepKey n public void testUnFollow() throws Exception { IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true")) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); @@ -51,7 +51,7 @@ public void testUnFollow() throws Exception { public void testRequestNotAcknowledged() { IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true")) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); @@ -71,7 +71,7 @@ public void testRequestNotAcknowledged() { public void testUnFollowUnfollowFailed() { IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true")) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); @@ -93,7 +93,7 @@ public void testUnFollowUnfollowFailed() { public void testFailureToReleaseRetentionLeases() throws Exception { IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true")) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/UpdateRolloverLifecycleDateStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/UpdateRolloverLifecycleDateStepTests.java index e4bcfd88737f2..3ede4d7668cd0 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/UpdateRolloverLifecycleDateStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/UpdateRolloverLifecycleDateStepTests.java @@ -17,7 +17,6 @@ import org.elasticsearch.index.IndexVersion; import org.elasticsearch.xpack.core.ilm.Step.StepKey; -import java.util.Collections; import java.util.List; import java.util.function.LongSupplier; @@ -68,7 +67,7 @@ public void testPerformAction() { .numberOfReplicas(randomIntBetween(0, 5)) .build(); IndexMetadata indexMetadata = IndexMetadata.builder(randomAlphaOfLength(10)) - .putRolloverInfo(new RolloverInfo(alias, Collections.emptyList(), rolloverTime)) + .putRolloverInfo(new RolloverInfo(alias, List.of(), rolloverTime)) .settings(settings(IndexVersion.current()).put(RolloverAction.LIFECYCLE_ROLLOVER_ALIAS, alias)) .numberOfShards(randomIntBetween(1, 5)) .numberOfReplicas(randomIntBetween(0, 5)) @@ -88,7 +87,7 @@ public void testPerformActionOnDataStream() { long rolloverTime = randomValueOtherThan(creationDate, () -> randomNonNegativeLong()); String dataStreamName = "test-datastream"; IndexMetadata originalIndexMeta = IndexMetadata.builder(DataStream.getDefaultBackingIndexName(dataStreamName, 1)) - .putRolloverInfo(new RolloverInfo(dataStreamName, Collections.emptyList(), rolloverTime)) + .putRolloverInfo(new RolloverInfo(dataStreamName, List.of(), rolloverTime)) .settings(settings(IndexVersion.current())) .numberOfShards(randomIntBetween(1, 5)) .numberOfReplicas(randomIntBetween(0, 5)) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForDataTierStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForDataTierStepTests.java index 00012575ea5de..2635e14b52eb4 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForDataTierStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForDataTierStepTests.java @@ -64,9 +64,7 @@ protected WaitForDataTierStep copyInstance(WaitForDataTierStep instance) { public void testConditionMet() { String notIncludedTier = randomFrom(DataTier.ALL_DATA_TIERS); - List otherTiers = DataTier.ALL_DATA_TIERS.stream() - .filter(tier -> notIncludedTier.equals(tier) == false) - .collect(Collectors.toList()); + List otherTiers = DataTier.ALL_DATA_TIERS.stream().filter(tier -> notIncludedTier.equals(tier) == false).toList(); List includedTiers = randomSubsetOf(between(1, otherTiers.size()), otherTiers); String tierPreference = String.join(",", includedTiers); WaitForDataTierStep step = new WaitForDataTierStep(randomStepKey(), randomStepKey(), tierPreference); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStepTests.java index 4ac5511a247c9..ba94508667776 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForFollowShardTasksStepTests.java @@ -16,9 +16,9 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import org.mockito.Mockito; -import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import static org.elasticsearch.xpack.core.ilm.UnfollowAction.CCR_METADATA_KEY; import static org.hamcrest.Matchers.equalTo; @@ -57,11 +57,11 @@ protected WaitForFollowShardTasksStep copyInstance(WaitForFollowShardTasksStep i public void testConditionMet() { IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true")) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(2) .numberOfReplicas(0) .build(); - List statsResponses = Arrays.asList( + List statsResponses = List.of( new FollowStatsAction.StatsResponse(createShardFollowTaskStatus(0, 9, 9)), new FollowStatsAction.StatsResponse(createShardFollowTaskStatus(1, 3, 3)) ); @@ -96,11 +96,11 @@ public void onFailure(Exception e) { public void testConditionNotMetShardsNotInSync() { IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true")) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(2) .numberOfReplicas(0) .build(); - List statsResponses = Arrays.asList( + List statsResponses = List.of( new FollowStatsAction.StatsResponse(createShardFollowTaskStatus(0, 9, 9)), new FollowStatsAction.StatsResponse(createShardFollowTaskStatus(1, 8, 3)) ); @@ -214,7 +214,7 @@ private void mockFollowStatsCall(String expectedIndexName, List listener = (ActionListener) invocationOnMock .getArguments()[2]; - listener.onResponse(new FollowStatsAction.StatsResponses(Collections.emptyList(), Collections.emptyList(), statsResponses)); + listener.onResponse(new FollowStatsAction.StatsResponses(List.of(), List.of(), statsResponses)); return null; }).when(client).execute(Mockito.eq(FollowStatsAction.INSTANCE), Mockito.any(), Mockito.any()); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexingCompleteStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexingCompleteStepTests.java index 2f91393b451d7..a0982e72b11af 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexingCompleteStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForIndexingCompleteStepTests.java @@ -15,7 +15,7 @@ import org.elasticsearch.index.IndexVersion; import org.elasticsearch.xpack.core.ilm.Step.StepKey; -import java.util.Collections; +import java.util.Map; import static org.elasticsearch.xpack.core.ilm.UnfollowAction.CCR_METADATA_KEY; import static org.hamcrest.Matchers.equalTo; @@ -54,7 +54,7 @@ protected WaitForIndexingCompleteStep copyInstance(WaitForIndexingCompleteStep i public void testConditionMet() { IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_INDEXING_COMPLETE, "true")) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); @@ -93,7 +93,7 @@ public void testConditionNotMet() { } IndexMetadata indexMetadata = IndexMetadata.builder("follower-index") .settings(indexSettings) - .putCustom(CCR_METADATA_KEY, Collections.emptyMap()) + .putCustom(CCR_METADATA_KEY, Map.of()) .numberOfShards(1) .numberOfReplicas(0) .build(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForRolloverReadyStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForRolloverReadyStepTests.java index 0264f7b09c6fd..db0c2957b3ccb 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForRolloverReadyStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/WaitForRolloverReadyStepTests.java @@ -38,7 +38,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -396,11 +395,7 @@ public void testEvaluateDoesntTriggerRolloverForIndexManuallyRolledOnLifecycleRo .putAlias(AliasMetadata.builder(rolloverAlias)) .settings(settings(IndexVersion.current()).put(RolloverAction.LIFECYCLE_ROLLOVER_ALIAS, rolloverAlias)) .putRolloverInfo( - new RolloverInfo( - rolloverAlias, - Collections.singletonList(new MaxSizeCondition(ByteSizeValue.ofBytes(2L))), - System.currentTimeMillis() - ) + new RolloverInfo(rolloverAlias, List.of(new MaxSizeCondition(ByteSizeValue.ofBytes(2L))), System.currentTimeMillis()) ) .numberOfShards(randomIntBetween(1, 5)) .numberOfReplicas(randomIntBetween(0, 5)) @@ -432,7 +427,7 @@ public void testEvaluateTriggersRolloverForIndexManuallyRolledOnDifferentAlias() .putRolloverInfo( new RolloverInfo( randomAlphaOfLength(5), - Collections.singletonList(new MaxSizeCondition(ByteSizeValue.ofBytes(2L))), + List.of(new MaxSizeCondition(ByteSizeValue.ofBytes(2L))), System.currentTimeMillis() ) ) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/GetLifecycleResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/GetLifecycleResponseTests.java index 05c637a3a66c9..1dc8b24c3231d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/GetLifecycleResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/GetLifecycleResponseTests.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; @@ -90,7 +89,7 @@ protected Writeable.Reader instanceReader() { protected NamedWriteableRegistry getNamedWriteableRegistry() { return new NamedWriteableRegistry( - Arrays.asList( + List.of( new NamedWriteableRegistry.Entry(LifecycleAction.class, MockAction.NAME, MockAction::new), new NamedWriteableRegistry.Entry(LifecycleType.class, TestLifecycleType.TYPE, in -> TestLifecycleType.INSTANCE) ) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/PutLifecycleRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/PutLifecycleRequestTests.java index feb5ca24a021d..b87a4e41258b8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/PutLifecycleRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/PutLifecycleRequestTests.java @@ -34,7 +34,6 @@ import org.junit.Before; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; public class PutLifecycleRequestTests extends AbstractXContentSerializingTestCase { @@ -78,7 +77,7 @@ public String getPolicyName() { @Override protected NamedWriteableRegistry getNamedWriteableRegistry() { return new NamedWriteableRegistry( - Arrays.asList( + List.of( new NamedWriteableRegistry.Entry( LifecycleType.class, TimeseriesLifecycleType.TYPE, @@ -105,7 +104,7 @@ protected NamedWriteableRegistry getNamedWriteableRegistry() { protected NamedXContentRegistry xContentRegistry() { List entries = new ArrayList<>(ClusterModule.getNamedXWriteables()); entries.addAll( - Arrays.asList( + List.of( new NamedXContentRegistry.Entry( LifecycleType.class, new ParseField(TimeseriesLifecycleType.TYPE), diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/RemoveIndexLifecyclePolicyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/RemoveIndexLifecyclePolicyResponseTests.java index 76f4d732f4ae7..44fed3d4b488b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/RemoveIndexLifecyclePolicyResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/action/RemoveIndexLifecyclePolicyResponseTests.java @@ -14,15 +14,13 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; public class RemoveIndexLifecyclePolicyResponseTests extends AbstractXContentSerializingTestCase { @Override protected Response createTestInstance() { - List failedIndexes = Arrays.asList(generateRandomStringArray(20, 20, false)); + List failedIndexes = List.of(generateRandomStringArray(20, 20, false)); return new Response(failedIndexes); } @@ -35,7 +33,7 @@ protected Writeable.Reader instanceReader() { protected Response mutateInstance(Response instance) { List failedIndices = randomValueOtherThan( instance.getFailedIndexes(), - () -> Arrays.asList(generateRandomStringArray(20, 20, false)) + () -> List.of(generateRandomStringArray(20, 20, false)) ); return new Response(failedIndices); } @@ -53,7 +51,7 @@ public void testNullFailedIndices() { public void testHasFailures() { Response response = new Response(new ArrayList<>()); assertFalse(response.hasFailures()); - assertEquals(Collections.emptyList(), response.getFailedIndexes()); + assertEquals(List.of(), response.getFailedIndexes()); int size = randomIntBetween(1, 10); List failedIndexes = new ArrayList<>(size); diff --git a/x-pack/plugin/ilm/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ilm/CCRIndexLifecycleIT.java b/x-pack/plugin/ilm/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ilm/CCRIndexLifecycleIT.java index 5168cd11eb172..a5d966873dda1 100644 --- a/x-pack/plugin/ilm/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ilm/CCRIndexLifecycleIT.java +++ b/x-pack/plugin/ilm/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ilm/CCRIndexLifecycleIT.java @@ -38,7 +38,6 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; -import static java.util.Collections.singletonMap; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.core.ilm.ShrinkIndexNameSupplier.SHRUNKEN_INDEX_PREFIX; import static org.hamcrest.Matchers.equalTo; @@ -762,8 +761,8 @@ private void assertDocumentExists(RestClient client, String index, String id) th } private void createNewSingletonPolicy(String policyName, String phaseName, LifecycleAction action, TimeValue after) throws IOException { - Phase phase = new Phase(phaseName, after, singletonMap(action.getWriteableName(), action)); - LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policyName, singletonMap(phase.getName(), phase)); + Phase phase = new Phase(phaseName, after, Map.of(action.getWriteableName(), action)); + LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policyName, Map.of(phase.getName(), phase)); XContentBuilder builder = jsonBuilder(); lifecyclePolicy.toXContent(builder, null); final StringEntity entity = new StringEntity("{ \"policy\":" + Strings.toString(builder) + "}", ContentType.APPLICATION_JSON); diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/MigrateToDataTiersIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/MigrateToDataTiersIT.java index 60e71b095039e..811d07a436677 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/MigrateToDataTiersIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/MigrateToDataTiersIT.java @@ -46,7 +46,6 @@ import java.util.Map; import java.util.concurrent.TimeUnit; -import static java.util.Collections.singletonMap; import static org.elasticsearch.xpack.TimeSeriesRestDriver.createIndexWithSettings; import static org.elasticsearch.xpack.TimeSeriesRestDriver.createNewSingletonPolicy; import static org.elasticsearch.xpack.TimeSeriesRestDriver.createPolicy; @@ -101,11 +100,11 @@ public void testMigrateToDataTiersAction() throws Exception { Map warmActions = new HashMap<>(); warmActions.put(SetPriorityAction.NAME, new SetPriorityAction(50)); warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1, null)); - warmActions.put(AllocateAction.NAME, new AllocateAction(null, null, singletonMap("data", "warm"), null, null)); + warmActions.put(AllocateAction.NAME, new AllocateAction(null, null, Map.of("data", "warm"), null, null)); warmActions.put(ShrinkAction.NAME, new ShrinkAction(1, null, false)); Map coldActions = new HashMap<>(); coldActions.put(SetPriorityAction.NAME, new SetPriorityAction(0)); - coldActions.put(AllocateAction.NAME, new AllocateAction(0, null, null, null, singletonMap("data", "cold"))); + coldActions.put(AllocateAction.NAME, new AllocateAction(0, null, null, null, Map.of("data", "cold"))); createPolicy( client(), @@ -114,7 +113,7 @@ public void testMigrateToDataTiersAction() throws Exception { new Phase("warm", TimeValue.ZERO, warmActions), new Phase("cold", TimeValue.timeValueDays(100), coldActions), null, - new Phase("delete", TimeValue.ZERO, singletonMap(DeleteAction.NAME, DeleteAction.WITH_SNAPSHOT_DELETE)) + new Phase("delete", TimeValue.ZERO, Map.of(DeleteAction.NAME, DeleteAction.WITH_SNAPSHOT_DELETE)) ); createIndexWithSettings( @@ -377,11 +376,11 @@ public void testMigrationDryRun() throws Exception { Map warmActions = new HashMap<>(); warmActions.put(SetPriorityAction.NAME, new SetPriorityAction(50)); warmActions.put(ForceMergeAction.NAME, new ForceMergeAction(1, null)); - warmActions.put(AllocateAction.NAME, new AllocateAction(null, null, singletonMap("data", "warm"), null, null)); + warmActions.put(AllocateAction.NAME, new AllocateAction(null, null, Map.of("data", "warm"), null, null)); warmActions.put(ShrinkAction.NAME, new ShrinkAction(1, null, false)); Map coldActions = new HashMap<>(); coldActions.put(SetPriorityAction.NAME, new SetPriorityAction(0)); - coldActions.put(AllocateAction.NAME, new AllocateAction(0, null, null, null, singletonMap("data", "cold"))); + coldActions.put(AllocateAction.NAME, new AllocateAction(0, null, null, null, Map.of("data", "cold"))); createPolicy( client(), @@ -390,7 +389,7 @@ public void testMigrationDryRun() throws Exception { new Phase("warm", TimeValue.ZERO, warmActions), new Phase("cold", TimeValue.timeValueDays(100), coldActions), null, - new Phase("delete", TimeValue.ZERO, singletonMap(DeleteAction.NAME, DeleteAction.WITH_SNAPSHOT_DELETE)) + new Phase("delete", TimeValue.ZERO, Map.of(DeleteAction.NAME, DeleteAction.WITH_SNAPSHOT_DELETE)) ); createIndexWithSettings( diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/TimeSeriesRestDriver.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/TimeSeriesRestDriver.java index 3949139db033b..a1c7ebc2d8b2c 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/TimeSeriesRestDriver.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/TimeSeriesRestDriver.java @@ -41,7 +41,6 @@ import java.io.IOException; import java.io.InputStream; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -50,7 +49,6 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static java.util.Collections.singletonMap; import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_SHARDS; import static org.elasticsearch.test.ESTestCase.randomAlphaOfLengthBetween; import static org.elasticsearch.test.ESTestCase.randomBoolean; @@ -154,8 +152,8 @@ public static void createNewSingletonPolicy( LifecycleAction action, TimeValue after ) throws IOException { - Phase phase = new Phase(phaseName, after, singletonMap(action.getWriteableName(), action)); - LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policyName, singletonMap(phase.getName(), phase)); + Phase phase = new Phase(phaseName, after, Map.of(action.getWriteableName(), action)); + LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policyName, Map.of(phase.getName(), phase)); XContentBuilder builder = jsonBuilder(); lifecyclePolicy.toXContent(builder, null); final StringEntity entity = new StringEntity("{ \"policy\":" + Strings.toString(builder) + "}", ContentType.APPLICATION_JSON); @@ -202,7 +200,7 @@ public static void createFullPolicy(RestClient client, String policyName, TimeVa new AllocateAction( 1, null, - singletonMap("_name", "javaRestTest-0,javaRestTest-1," + "javaRestTest-2," + "javaRestTest-3"), + Map.of("_name", "javaRestTest-0,javaRestTest-1," + "javaRestTest-2," + "javaRestTest-3"), null, null ) @@ -215,7 +213,7 @@ public static void createFullPolicy(RestClient client, String policyName, TimeVa new AllocateAction( 0, null, - singletonMap("_name", "javaRestTest-0,javaRestTest-1," + "javaRestTest-2," + "javaRestTest-3"), + Map.of("_name", "javaRestTest-0,javaRestTest-1," + "javaRestTest-2," + "javaRestTest-3"), null, null ) @@ -224,7 +222,7 @@ public static void createFullPolicy(RestClient client, String policyName, TimeVa phases.put("hot", new Phase("hot", hotTime, hotActions)); phases.put("warm", new Phase("warm", TimeValue.ZERO, warmActions)); phases.put("cold", new Phase("cold", TimeValue.ZERO, coldActions)); - phases.put("delete", new Phase("delete", TimeValue.ZERO, singletonMap(DeleteAction.NAME, DeleteAction.WITH_SNAPSHOT_DELETE))); + phases.put("delete", new Phase("delete", TimeValue.ZERO, Map.of(DeleteAction.NAME, DeleteAction.WITH_SNAPSHOT_DELETE))); LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policyName, phases); // PUT policy XContentBuilder builder = jsonBuilder(); @@ -300,7 +298,7 @@ public static Map getOnlyIndexSettings(RestClient client, String Map responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); Map indexSettings = (Map) responseMap.get(index); if (indexSettings == null) { - return Collections.emptyMap(); + return Map.of(); } return (Map) indexSettings.get("settings"); } diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/ChangePolicyForIndexIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/ChangePolicyForIndexIT.java index 7f75b010346ad..370e00785e843 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/ChangePolicyForIndexIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/ChangePolicyForIndexIT.java @@ -32,7 +32,6 @@ import java.util.Map; import java.util.concurrent.TimeUnit; -import static java.util.Collections.singletonMap; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.TimeSeriesRestDriver.createIndexWithSettings; import static org.elasticsearch.xpack.TimeSeriesRestDriver.createNewSingletonPolicy; @@ -67,7 +66,7 @@ public void testChangePolicyForIndex() throws Exception { new Phase( "hot", TimeValue.ZERO, - singletonMap(RolloverAction.NAME, new RolloverAction(null, null, null, 1L, null, null, null, null, null, null)) + Map.of(RolloverAction.NAME, new RolloverAction(null, null, null, 1L, null, null, null, null, null, null)) ) ); phases1.put( @@ -75,7 +74,7 @@ public void testChangePolicyForIndex() throws Exception { new Phase( "warm", TimeValue.ZERO, - singletonMap(AllocateAction.NAME, new AllocateAction(1, null, singletonMap("_name", "foobarbaz"), null, null)) + Map.of(AllocateAction.NAME, new AllocateAction(1, null, Map.of("_name", "foobarbaz"), null, null)) ) ); LifecyclePolicy lifecyclePolicy1 = new LifecyclePolicy("policy_1", phases1); @@ -85,7 +84,7 @@ public void testChangePolicyForIndex() throws Exception { new Phase( "hot", TimeValue.ZERO, - singletonMap(RolloverAction.NAME, new RolloverAction(null, null, null, 1000L, null, null, null, null, null, null)) + Map.of(RolloverAction.NAME, new RolloverAction(null, null, null, 1000L, null, null, null, null, null, null)) ) ); phases2.put( @@ -93,15 +92,9 @@ public void testChangePolicyForIndex() throws Exception { new Phase( "warm", TimeValue.ZERO, - singletonMap( + Map.of( AllocateAction.NAME, - new AllocateAction( - 1, - null, - singletonMap("_name", "javaRestTest-0,javaRestTest-1,javaRestTest-2,javaRestTest-3"), - null, - null - ) + new AllocateAction(1, null, Map.of("_name", "javaRestTest-0,javaRestTest-1,javaRestTest-2,javaRestTest-3"), null, null) ) ) ); diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java index 2b722a6555a08..4c53d711ffdef 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesLifecycleActionsIT.java @@ -58,7 +58,6 @@ import java.util.Map; import java.util.concurrent.TimeUnit; -import static java.util.Collections.singletonMap; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.TimeSeriesRestDriver.createFullPolicy; import static org.elasticsearch.xpack.TimeSeriesRestDriver.createIndexWithSettings; @@ -219,7 +218,7 @@ public void testAllocateOnlyAllocation() throws Exception { Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 2).put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) ); String allocateNodeName = "javaRestTest-0,javaRestTest-1,javaRestTest-2,javaRestTest-3"; - AllocateAction allocateAction = new AllocateAction(null, null, singletonMap("_name", allocateNodeName), null, null); + AllocateAction allocateAction = new AllocateAction(null, null, Map.of("_name", allocateNodeName), null, null); String endPhase = randomFrom("warm", "cold"); createNewSingletonPolicy(client(), policy, endPhase, allocateAction); updatePolicy(client(), index, policy); @@ -978,7 +977,7 @@ public void testHaltAtEndOfPhase() throws Exception { hotActions.put(SetPriorityAction.NAME, new SetPriorityAction(100)); Map phases = new HashMap<>(); phases.put("hot", new Phase("hot", TimeValue.ZERO, hotActions)); - phases.put("delete", new Phase("delete", TimeValue.ZERO, singletonMap(DeleteAction.NAME, DeleteAction.WITH_SNAPSHOT_DELETE))); + phases.put("delete", new Phase("delete", TimeValue.ZERO, Map.of(DeleteAction.NAME, DeleteAction.WITH_SNAPSHOT_DELETE))); LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, phases); // PUT policy XContentBuilder builder = jsonBuilder(); @@ -1004,7 +1003,7 @@ public void testDeleteActionDoesntDeleteSearchableSnapshot() throws Exception { phases.put("cold", new Phase("cold", TimeValue.ZERO, coldActions)); phases.put( "delete", - new Phase("delete", TimeValue.timeValueMillis(10000), singletonMap(DeleteAction.NAME, DeleteAction.NO_SNAPSHOT_DELETE)) + new Phase("delete", TimeValue.timeValueMillis(10000), Map.of(DeleteAction.NAME, DeleteAction.NO_SNAPSHOT_DELETE)) ); LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, phases); // PUT policy diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java index f00b5b566c156..15e59a1593337 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/SearchableSnapshotActionIT.java @@ -47,7 +47,6 @@ import java.util.Map; import java.util.concurrent.TimeUnit; -import static java.util.Collections.singletonMap; import static org.elasticsearch.cluster.routing.allocation.decider.ShardsLimitAllocationDecider.INDEX_TOTAL_SHARDS_PER_NODE_SETTING; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.TimeSeriesRestDriver.createComposableTemplate; @@ -185,7 +184,7 @@ public void testDeleteActionDeletesSearchableSnapshot() throws Exception { Map coldActions = Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo)); Map phases = new HashMap<>(); phases.put("cold", new Phase("cold", TimeValue.ZERO, coldActions)); - phases.put("delete", new Phase("delete", TimeValue.timeValueMillis(10000), singletonMap(DeleteAction.NAME, WITH_SNAPSHOT_DELETE))); + phases.put("delete", new Phase("delete", TimeValue.timeValueMillis(10000), Map.of(DeleteAction.NAME, WITH_SNAPSHOT_DELETE))); LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, phases); // PUT policy XContentBuilder builder = jsonBuilder(); @@ -455,7 +454,7 @@ public void testIdenticalSearchableSnapshotActionIsNoop() throws Exception { new Phase( "cold", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), null ); @@ -516,12 +515,12 @@ public void testConvertingSearchableSnapshotFromFullToPartial() throws Exception new Phase( "cold", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), new Phase( "frozen", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), null ); @@ -586,7 +585,7 @@ public void testResumingSearchableSnapshotFromFullToPartial() throws Exception { new Phase( "cold", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), null, null @@ -600,12 +599,12 @@ public void testResumingSearchableSnapshotFromFullToPartial() throws Exception { new Phase( "cold", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), new Phase( "frozen", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), null ); @@ -664,14 +663,14 @@ public void testResumingSearchableSnapshotFromFullToPartial() throws Exception { new Phase( "cold", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), new Phase( "frozen", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), - new Phase("delete", TimeValue.ZERO, singletonMap(DeleteAction.NAME, WITH_SNAPSHOT_DELETE)) + new Phase("delete", TimeValue.ZERO, Map.of(DeleteAction.NAME, WITH_SNAPSHOT_DELETE)) ); assertBusy(() -> { logger.info("--> waiting for [{}] to be deleted...", partiallyMountedIndexName); @@ -695,7 +694,7 @@ public void testResumingSearchableSnapshotFromPartialToFull() throws Exception { new Phase( "cold", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), null, null @@ -710,12 +709,12 @@ public void testResumingSearchableSnapshotFromPartialToFull() throws Exception { new Phase( "cold", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), new Phase( "frozen", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), null ); @@ -775,10 +774,10 @@ public void testResumingSearchableSnapshotFromPartialToFull() throws Exception { new Phase( "cold", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), null, - new Phase("delete", TimeValue.ZERO, singletonMap(DeleteAction.NAME, WITH_SNAPSHOT_DELETE)) + new Phase("delete", TimeValue.ZERO, Map.of(DeleteAction.NAME, WITH_SNAPSHOT_DELETE)) ); assertBusy(() -> { logger.info("--> waiting for [{}] to be deleted...", restoredPartiallyMountedIndexName); @@ -803,12 +802,12 @@ public void testSecondSearchableSnapshotUsingDifferentRepoThrows() throws Except new Phase( "cold", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), new Phase( "frozen", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(secondRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(secondRepo, randomBoolean())) ), null ) @@ -934,12 +933,12 @@ public void testSearchableSnapshotTotalShardsPerNode() throws Exception { new Phase( "cold", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean())) ), new Phase( "frozen", TimeValue.ZERO, - singletonMap(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean(), totalShardsPerNode)) + Map.of(SearchableSnapshotAction.NAME, new SearchableSnapshotAction(snapshotRepo, randomBoolean(), totalShardsPerNode)) ), null ); diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/ShrinkActionIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/ShrinkActionIT.java index d2f2dbbd0c9fb..2fecf3c617ccd 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/ShrinkActionIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/ShrinkActionIT.java @@ -39,7 +39,6 @@ import java.util.Map; import java.util.concurrent.TimeUnit; -import static java.util.Collections.singletonMap; import static org.elasticsearch.cluster.metadata.IndexMetadata.SETTING_NUMBER_OF_SHARDS; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.TimeSeriesRestDriver.createIndexWithSettings; @@ -286,7 +285,7 @@ public void testSetSingleNodeAllocationRetriesUntilItSucceeds() throws Exception TimeValue.ZERO, Map.of(migrateAction.getWriteableName(), migrateAction, shrinkAction.getWriteableName(), shrinkAction) ); - LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, singletonMap(phase.getName(), phase)); + LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, Map.of(phase.getName(), phase)); XContentBuilder builder = jsonBuilder(); lifecyclePolicy.toXContent(builder, null); final StringEntity entity = new StringEntity("{ \"policy\":" + Strings.toString(builder) + "}", ContentType.APPLICATION_JSON); diff --git a/x-pack/plugin/ilm/qa/with-security/src/javaRestTest/java/org/elasticsearch/xpack/security/PermissionsIT.java b/x-pack/plugin/ilm/qa/with-security/src/javaRestTest/java/org/elasticsearch/xpack/security/PermissionsIT.java index 9460500177616..12dede7067b03 100644 --- a/x-pack/plugin/ilm/qa/with-security/src/javaRestTest/java/org/elasticsearch/xpack/security/PermissionsIT.java +++ b/x-pack/plugin/ilm/qa/with-security/src/javaRestTest/java/org/elasticsearch/xpack/security/PermissionsIT.java @@ -45,7 +45,6 @@ import java.util.Map; import java.util.concurrent.TimeUnit; -import static java.util.Collections.singletonMap; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -225,8 +224,8 @@ public void testWhenUserLimitedByOnlyAliasOfIndexCanWriteToIndexWhichWasRolledov } private void createNewSingletonPolicy(RestClient client, String policy, String phaseName, LifecycleAction action) throws IOException { - Phase phase = new Phase(phaseName, TimeValue.ZERO, singletonMap(action.getWriteableName(), action)); - LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, singletonMap(phase.getName(), phase)); + Phase phase = new Phase(phaseName, TimeValue.ZERO, Map.of(action.getWriteableName(), action)); + LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, Map.of(phase.getName(), phase)); XContentBuilder builder = jsonBuilder(); lifecyclePolicy.toXContent(builder, null); final StringEntity entity = new StringEntity("{ \"policy\":" + Strings.toString(builder) + "}", ContentType.APPLICATION_JSON); diff --git a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ClusterStateWaitThresholdBreachTests.java b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ClusterStateWaitThresholdBreachTests.java index 55daa8104c12a..f25028824b56e 100644 --- a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ClusterStateWaitThresholdBreachTests.java +++ b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ClusterStateWaitThresholdBreachTests.java @@ -35,7 +35,6 @@ import org.elasticsearch.xpack.core.ilm.action.PutLifecycleRequest; import org.junit.Before; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; @@ -65,7 +64,7 @@ public void refreshDataStreamAndPolicy() { @Override protected Collection> nodePlugins() { - return Arrays.asList(LocalStateCompositeXPackPlugin.class, IndexLifecycle.class, Ccr.class); + return List.of(LocalStateCompositeXPackPlugin.class, IndexLifecycle.class, Ccr.class); } @Override diff --git a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/DataTiersMigrationsTests.java b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/DataTiersMigrationsTests.java index 7a0e00e5c4147..6d409bf474cfc 100644 --- a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/DataTiersMigrationsTests.java +++ b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/DataTiersMigrationsTests.java @@ -30,9 +30,8 @@ import org.elasticsearch.xpack.core.ilm.action.PutLifecycleRequest; import org.junit.Before; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -57,7 +56,7 @@ public void refreshDataStreamAndPolicy() { @Override protected Collection> nodePlugins() { - return Arrays.asList(LocalStateCompositeXPackPlugin.class, IndexLifecycle.class); + return List.of(LocalStateCompositeXPackPlugin.class, IndexLifecycle.class); } @Override @@ -100,9 +99,9 @@ public void testIndexDataTierMigration() throws Exception { logger.info("starting a cold data node"); internalCluster().startNode(coldNode(Settings.EMPTY)); - Phase hotPhase = new Phase("hot", TimeValue.ZERO, Collections.emptyMap()); - Phase warmPhase = new Phase("warm", TimeValue.ZERO, Collections.emptyMap()); - Phase coldPhase = new Phase("cold", TimeValue.ZERO, Collections.emptyMap()); + Phase hotPhase = new Phase("hot", TimeValue.ZERO, Map.of()); + Phase warmPhase = new Phase("warm", TimeValue.ZERO, Map.of()); + Phase coldPhase = new Phase("cold", TimeValue.ZERO, Map.of()); LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, Map.of("hot", hotPhase, "warm", warmPhase, "cold", coldPhase)); PutLifecycleRequest putLifecycleRequest = new PutLifecycleRequest(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, lifecyclePolicy); assertAcked(client().execute(ILMActions.PUT, putLifecycleRequest).get()); @@ -161,9 +160,9 @@ public void testUserOptsOutOfTierMigration() throws Exception { logger.info("starting a cold data node"); internalCluster().startNode(coldNode(Settings.EMPTY)); - Phase hotPhase = new Phase("hot", TimeValue.ZERO, Collections.emptyMap()); - Phase warmPhase = new Phase("warm", TimeValue.ZERO, Collections.emptyMap()); - Phase coldPhase = new Phase("cold", TimeValue.ZERO, Collections.emptyMap()); + Phase hotPhase = new Phase("hot", TimeValue.ZERO, Map.of()); + Phase warmPhase = new Phase("warm", TimeValue.ZERO, Map.of()); + Phase coldPhase = new Phase("cold", TimeValue.ZERO, Map.of()); LifecyclePolicy lifecyclePolicy = new LifecyclePolicy(policy, Map.of("hot", hotPhase, "warm", warmPhase, "cold", coldPhase)); PutLifecycleRequest putLifecycleRequest = new PutLifecycleRequest(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, lifecyclePolicy); assertAcked(client().execute(ILMActions.PUT, putLifecycleRequest).get()); diff --git a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ILMMultiNodeIT.java b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ILMMultiNodeIT.java index b443c769407c5..2c4c1c9e20bb6 100644 --- a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ILMMultiNodeIT.java +++ b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ILMMultiNodeIT.java @@ -33,10 +33,9 @@ import org.elasticsearch.xpack.core.ilm.action.ILMActions; import org.elasticsearch.xpack.core.ilm.action.PutLifecycleRequest; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -49,7 +48,7 @@ public class ILMMultiNodeIT extends ESIntegTestCase { @Override protected Collection> nodePlugins() { - return Arrays.asList(LocalStateCompositeXPackPlugin.class, DataStreamsPlugin.class, IndexLifecycle.class, Ccr.class); + return List.of(LocalStateCompositeXPackPlugin.class, DataStreamsPlugin.class, IndexLifecycle.class, Ccr.class); } @Override @@ -69,9 +68,9 @@ public void testShrinkOnTiers() throws Exception { ensureGreen(); RolloverAction rolloverAction = new RolloverAction(null, null, null, 1L, null, null, null, null, null, null); - Phase hotPhase = new Phase("hot", TimeValue.ZERO, Collections.singletonMap(rolloverAction.getWriteableName(), rolloverAction)); + Phase hotPhase = new Phase("hot", TimeValue.ZERO, Map.of(rolloverAction.getWriteableName(), rolloverAction)); ShrinkAction shrinkAction = new ShrinkAction(1, null, false); - Phase warmPhase = new Phase("warm", TimeValue.ZERO, Collections.singletonMap(shrinkAction.getWriteableName(), shrinkAction)); + Phase warmPhase = new Phase("warm", TimeValue.ZERO, Map.of(shrinkAction.getWriteableName(), shrinkAction)); Map phases = new HashMap<>(); phases.put(hotPhase.getName(), hotPhase); phases.put(warmPhase.getName(), warmPhase); @@ -89,7 +88,7 @@ public void testShrinkOnTiers() throws Exception { ); ComposableIndexTemplate template = ComposableIndexTemplate.builder() - .indexPatterns(Collections.singletonList(index)) + .indexPatterns(List.of(index)) .template(t) .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) .build(); @@ -121,12 +120,12 @@ public void testShrinkOnTiers() throws Exception { } public void startHotOnlyNode() { - Settings nodeSettings = Settings.builder().putList("node.roles", Arrays.asList("master", "data_hot", "ingest")).build(); + Settings nodeSettings = Settings.builder().putList("node.roles", List.of("master", "data_hot", "ingest")).build(); internalCluster().startNode(nodeSettings); } public void startWarmOnlyNode() { - Settings nodeSettings = Settings.builder().putList("node.roles", Arrays.asList("master", "data_warm", "ingest")).build(); + Settings nodeSettings = Settings.builder().putList("node.roles", List.of("master", "data_warm", "ingest")).build(); internalCluster().startNode(nodeSettings); } } diff --git a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ILMMultiNodeWithCCRDisabledIT.java b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ILMMultiNodeWithCCRDisabledIT.java index e02dd5fe45676..b91a309a23ae5 100644 --- a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ILMMultiNodeWithCCRDisabledIT.java +++ b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/ILMMultiNodeWithCCRDisabledIT.java @@ -34,10 +34,9 @@ import org.elasticsearch.xpack.core.ilm.action.ILMActions; import org.elasticsearch.xpack.core.ilm.action.PutLifecycleRequest; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -50,7 +49,7 @@ public class ILMMultiNodeWithCCRDisabledIT extends ESIntegTestCase { @Override protected Collection> nodePlugins() { - return Arrays.asList(LocalStateCompositeXPackPlugin.class, DataStreamsPlugin.class, IndexLifecycle.class, Ccr.class); + return List.of(LocalStateCompositeXPackPlugin.class, DataStreamsPlugin.class, IndexLifecycle.class, Ccr.class); } @Override @@ -75,7 +74,7 @@ public void testShrinkOnTiers() throws Exception { actions.put(shrinkAction.getWriteableName(), shrinkAction); Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); - LifecyclePolicy lifecyclePolicy = new LifecyclePolicy("shrink-policy", Collections.singletonMap(hotPhase.getName(), hotPhase)); + LifecyclePolicy lifecyclePolicy = new LifecyclePolicy("shrink-policy", Map.of(hotPhase.getName(), hotPhase)); client().execute(ILMActions.PUT, new PutLifecycleRequest(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, lifecyclePolicy)).get(); Template t = new Template( @@ -89,7 +88,7 @@ public void testShrinkOnTiers() throws Exception { ); ComposableIndexTemplate template = ComposableIndexTemplate.builder() - .indexPatterns(Collections.singletonList(index)) + .indexPatterns(List.of(index)) .template(t) .dataStreamTemplate(new ComposableIndexTemplate.DataStreamTemplate()) .build(); @@ -121,12 +120,12 @@ public void testShrinkOnTiers() throws Exception { } public void startHotOnlyNode() { - Settings nodeSettings = Settings.builder().putList("node.roles", Arrays.asList("master", "data_hot", "ingest")).build(); + Settings nodeSettings = Settings.builder().putList("node.roles", List.of("master", "data_hot", "ingest")).build(); internalCluster().startNode(nodeSettings); } public void startWarmOnlyNode() { - Settings nodeSettings = Settings.builder().putList("node.roles", Arrays.asList("master", "data_warm", "ingest")).build(); + Settings nodeSettings = Settings.builder().putList("node.roles", List.of("master", "data_warm", "ingest")).build(); internalCluster().startNode(nodeSettings); } } diff --git a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/IndexLifecycleInitialisationTests.java b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/IndexLifecycleInitialisationTests.java index d06a9f9cc19b1..644f88dc533b9 100644 --- a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/IndexLifecycleInitialisationTests.java +++ b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/IndexLifecycleInitialisationTests.java @@ -56,9 +56,7 @@ import java.io.IOException; import java.time.Instant; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -112,7 +110,7 @@ protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { @Override protected Collection> nodePlugins() { - return Arrays.asList(LocalStateCompositeXPackPlugin.class, IndexLifecycle.class, TestILMPlugin.class); + return List.of(LocalStateCompositeXPackPlugin.class, IndexLifecycle.class, TestILMPlugin.class); } @Before @@ -128,9 +126,9 @@ public void init() { Step.StepKey compKey = new Step.StepKey("mock", "complete", "complete"); steps.add(new ObservableClusterStateWaitStep(key, compKey)); steps.add(new PhaseCompleteStep(compKey, null)); - Map actions = Collections.singletonMap(ObservableAction.NAME, OBSERVABLE_ACTION); + Map actions = Map.of(ObservableAction.NAME, OBSERVABLE_ACTION); mockPhase = new Phase("mock", TimeValue.timeValueSeconds(0), actions); - Map phases = Collections.singletonMap("mock", mockPhase); + Map phases = Map.of("mock", mockPhase); lifecyclePolicy = newLockableLifecyclePolicy("test", phases); } @@ -311,7 +309,7 @@ public void testExplainExecution() throws Exception { updateIndexSettings(Settings.builder().put("index.lifecycle.test.complete", true), "test"); { - Phase phase = new Phase("mock", TimeValue.ZERO, Collections.singletonMap("TEST_ACTION", OBSERVABLE_ACTION)); + Phase phase = new Phase("mock", TimeValue.ZERO, Map.of("TEST_ACTION", OBSERVABLE_ACTION)); PhaseExecutionInfo expectedExecutionInfo = new PhaseExecutionInfo(lifecyclePolicy.getName(), phase, 1L, actualModifiedDate); assertBusy(() -> { IndexLifecycleExplainResponse indexResponse = executeExplainRequestAndGetTestIndexResponse("test"); @@ -526,12 +524,12 @@ public List> getSettings() { Setting.Property.Dynamic, Setting.Property.IndexScope ); - return Collections.singletonList(COMPLETE_SETTING); + return List.of(COMPLETE_SETTING); } @Override public List getNamedXContent() { - return Arrays.asList(new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ObservableAction.NAME), (p) -> { + return List.of(new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ObservableAction.NAME), (p) -> { MockAction.parse(p); return OBSERVABLE_ACTION; })); @@ -539,7 +537,7 @@ public List getNamedXContent() { @Override public List getNamedWriteables() { - return Arrays.asList( + return List.of( new NamedWriteableRegistry.Entry(LifecycleType.class, LockableLifecycleType.TYPE, (in) -> LockableLifecycleType.INSTANCE), new NamedWriteableRegistry.Entry(LifecycleAction.class, ObservableAction.NAME, ObservableAction::readObservableAction), new NamedWriteableRegistry.Entry( diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java index 9efe46402428c..a36b74d9932d9 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingService.java @@ -251,7 +251,7 @@ static List migrateIlmPolicies( ) { IndexLifecycleMetadata currentLifecycleMetadata = currentState.metadata().custom(IndexLifecycleMetadata.TYPE); if (currentLifecycleMetadata == null) { - return Collections.emptyList(); + return List.of(); } List migratedPolicies = new ArrayList<>(); @@ -827,7 +827,6 @@ public MigratedEntities( this.migratedPolicies = Collections.unmodifiableList(migratedPolicies); this.migratedTemplates = migratedTemplates; } - } /** diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java index 42d1955f0d453..c5d367804db42 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorService.java @@ -41,7 +41,6 @@ import org.elasticsearch.xpack.core.ilm.WaitForRolloverReadyStep; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -219,8 +218,8 @@ public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResources GREEN, "No Index Lifecycle Management policies configured", createDetails(verbose, ilmMetadata, currentMode), - Collections.emptyList(), - Collections.emptyList() + List.of(), + List.of() ); } else if (currentMode != OperationMode.RUNNING) { return createIndicator( @@ -238,8 +237,8 @@ public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResources GREEN, "Index Lifecycle Management is running", createDetails(verbose, ilmMetadata, currentMode), - Collections.emptyList(), - Collections.emptyList() + List.of(), + List.of() ); } else { return createIndicator( diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java index e59bde7253051..71d61caa5fe31 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleService.java @@ -54,7 +54,6 @@ import java.io.Closeable; import java.time.Clock; import java.util.Collection; -import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.function.LongSupplier; @@ -500,7 +499,7 @@ static Set indicesOnShuttingDownNodesInDangerousStep(ClusterState state, SingleNodeShutdownMetadata.Type.REPLACE ); if (shutdownNodes.isEmpty()) { - return Collections.emptySet(); + return Set.of(); } Set indicesPreventingShutdown = state.metadata() diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportGetLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportGetLifecycleAction.java index f4598727d6123..5fa0f881559fb 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportGetLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportGetLifecycleAction.java @@ -32,7 +32,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -71,7 +70,7 @@ protected void masterOperation(Task task, Request request, ClusterState state, A IndexLifecycleMetadata metadata = clusterService.state().metadata().custom(IndexLifecycleMetadata.TYPE); if (metadata == null) { if (request.getPolicyNames().length == 0) { - listener.onResponse(new Response(Collections.emptyList())); + listener.onResponse(new Response(List.of())); } else { listener.onFailure( new ResourceNotFoundException("Lifecycle policy not found: {}", Arrays.toString(request.getPolicyNames())) diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryItem.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryItem.java index 977887a0487f3..efd54e05cb153 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryItem.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/history/ILMHistoryItem.java @@ -18,7 +18,7 @@ import org.elasticsearch.xcontent.json.JsonXContent; import java.io.IOException; -import java.util.Collections; +import java.util.Map; import java.util.Objects; import static org.elasticsearch.ElasticsearchException.REST_EXCEPTION_SKIP_STACK_TRACE; @@ -110,7 +110,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } private static String exceptionToString(Exception exception) { - Params stacktraceParams = new MapParams(Collections.singletonMap(REST_EXCEPTION_SKIP_STACK_TRACE, "false")); + Params stacktraceParams = new MapParams(Map.of(REST_EXCEPTION_SKIP_STACK_TRACE, "false")); String exceptionString; try (XContentBuilder causeXContentBuilder = JsonXContent.contentBuilder()) { causeXContentBuilder.startObject(); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java index 570c2f5231acf..2ee133b6292bd 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/cluster/metadata/MetadataMigrateToDataTiersRoutingServiceTests.java @@ -48,7 +48,6 @@ import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -118,10 +117,7 @@ public void testMigrateIlmPolicyForIndexWithoutILMMetadata() { Metadata.builder() .putCustom( IndexLifecycleMetadata.TYPE, - new IndexLifecycleMetadata( - Collections.singletonMap(policyMetadata.getName(), policyMetadata), - OperationMode.STOPPED - ) + new IndexLifecycleMetadata(Map.of(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED) ) .put(IndexMetadata.builder(indexName).settings(getBaseIndexSettings())) .build() @@ -176,7 +172,7 @@ public void testMigrateIlmPolicyForPhaseWithDeactivatedMigrateAction() { ); LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata( policy, - Collections.emptyMap(), + Map.of(), randomNonNegativeLong(), randomNonNegativeLong() ); @@ -186,10 +182,7 @@ public void testMigrateIlmPolicyForPhaseWithDeactivatedMigrateAction() { Metadata.builder() .putCustom( IndexLifecycleMetadata.TYPE, - new IndexLifecycleMetadata( - Collections.singletonMap(policyMetadata.getName(), policyMetadata), - OperationMode.STOPPED - ) + new IndexLifecycleMetadata(Map.of(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED) ) .put(IndexMetadata.builder(indexName).settings(getBaseIndexSettings())) .build() @@ -245,10 +238,7 @@ public void testMigrateIlmPolicyRefreshesCachedPhase() { Metadata.builder() .putCustom( IndexLifecycleMetadata.TYPE, - new IndexLifecycleMetadata( - Collections.singletonMap(policyMetadata.getName(), policyMetadata), - OperationMode.STOPPED - ) + new IndexLifecycleMetadata(Map.of(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED) ) .put(indexMetadata) .build() @@ -302,10 +292,7 @@ public void testMigrateIlmPolicyRefreshesCachedPhase() { .putCustom( IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata( - Collections.singletonMap( - policyMetadataWithTotalShardsPerNode.getName(), - policyMetadataWithTotalShardsPerNode - ), + Map.of(policyMetadataWithTotalShardsPerNode.getName(), policyMetadataWithTotalShardsPerNode), OperationMode.STOPPED ) ) @@ -352,10 +339,7 @@ public void testMigrateIlmPolicyRefreshesCachedPhase() { Metadata.builder() .putCustom( IndexLifecycleMetadata.TYPE, - new IndexLifecycleMetadata( - Collections.singletonMap(policyMetadata.getName(), policyMetadata), - OperationMode.STOPPED - ) + new IndexLifecycleMetadata(Map.of(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED) ) .put(indexMetadata) .build() @@ -406,10 +390,7 @@ public void testMigrateIlmPolicyRefreshesCachedPhase() { Metadata.builder() .putCustom( IndexLifecycleMetadata.TYPE, - new IndexLifecycleMetadata( - Collections.singletonMap(policyMetadata.getName(), policyMetadata), - OperationMode.STOPPED - ) + new IndexLifecycleMetadata(Map.of(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED) ) .put(indexMetadata) .build() @@ -456,10 +437,7 @@ public void testMigrateIlmPolicyRefreshesCachedPhase() { Metadata.builder() .putCustom( IndexLifecycleMetadata.TYPE, - new IndexLifecycleMetadata( - Collections.singletonMap(policyMetadata.getName(), policyMetadata), - OperationMode.STOPPED - ) + new IndexLifecycleMetadata(Map.of(policyMetadata.getName(), policyMetadata), OperationMode.STOPPED) ) .put(indexMetadata) .build() @@ -1008,7 +986,7 @@ public void testMigrateToDataTiersRouting() { ); LifecyclePolicyMetadata policyWithDataAttribute = new LifecyclePolicyMetadata( policyToMigrate, - Collections.emptyMap(), + Map.of(), randomNonNegativeLong(), randomNonNegativeLong() ); @@ -1026,7 +1004,7 @@ public void testMigrateToDataTiersRouting() { ); LifecyclePolicyMetadata policyWithOtherAttribute = new LifecyclePolicyMetadata( shouldntBeMigratedPolicy, - Collections.emptyMap(), + Map.of(), randomNonNegativeLong(), randomNonNegativeLong() ); @@ -1215,7 +1193,7 @@ public void testDryRunDoesntRequireILMStopped() { public void testMigrationDoesNotRemoveComposableTemplates() { ComposableIndexTemplate composableIndexTemplate = ComposableIndexTemplate.builder() - .indexPatterns(Collections.singletonList("*")) + .indexPatterns(List.of("*")) .template(new Template(Settings.builder().put(DATA_ROUTING_REQUIRE_SETTING, "hot").build(), null, null)) .build(); @@ -1285,7 +1263,7 @@ private LifecyclePolicyMetadata getWarmColdPolicyMeta( new Phase("cold", TimeValue.ZERO, Map.of(coldAllocateAction.getWriteableName(), coldAllocateAction)) ) ); - return new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()); + return new LifecyclePolicyMetadata(policy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong()); } public void testMigrateLegacyIndexTemplates() { diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTaskTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTaskTests.java index b3146e81d08fc..06d11bff069fd 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTaskTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/ExecuteStepsUpdateTaskTests.java @@ -42,9 +42,8 @@ import org.mockito.Mockito; import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.elasticsearch.cluster.metadata.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; @@ -91,42 +90,33 @@ public void prepareState() throws IOException { Phase mixedPhase = new Phase( "first_phase", TimeValue.ZERO, - Collections.singletonMap(MockAction.NAME, new MockAction(Arrays.asList(firstStep, secondStep, thirdStep))) + Map.of(MockAction.NAME, new MockAction(List.of(firstStep, secondStep, thirdStep))) ); Phase allClusterPhase = new Phase( "first_phase", TimeValue.ZERO, - Collections.singletonMap(MockAction.NAME, new MockAction(Arrays.asList(firstStep, allClusterSecondStep))) + Map.of(MockAction.NAME, new MockAction(List.of(firstStep, allClusterSecondStep))) ); Phase invalidPhase = new Phase( "invalid_phase", TimeValue.ZERO, - Collections.singletonMap( - MockAction.NAME, - new MockAction(Arrays.asList(new MockClusterStateActionStep(firstStepKey, invalidStepKey))) - ) - ); - LifecyclePolicy mixedPolicy = newTestLifecyclePolicy(mixedPolicyName, Collections.singletonMap(mixedPhase.getName(), mixedPhase)); - LifecyclePolicy allClusterPolicy = newTestLifecyclePolicy( - allClusterPolicyName, - Collections.singletonMap(allClusterPhase.getName(), allClusterPhase) - ); - LifecyclePolicy invalidPolicy = newTestLifecyclePolicy( - invalidPolicyName, - Collections.singletonMap(invalidPhase.getName(), invalidPhase) + Map.of(MockAction.NAME, new MockAction(List.of(new MockClusterStateActionStep(firstStepKey, invalidStepKey)))) ); + LifecyclePolicy mixedPolicy = newTestLifecyclePolicy(mixedPolicyName, Map.of(mixedPhase.getName(), mixedPhase)); + LifecyclePolicy allClusterPolicy = newTestLifecyclePolicy(allClusterPolicyName, Map.of(allClusterPhase.getName(), allClusterPhase)); + LifecyclePolicy invalidPolicy = newTestLifecyclePolicy(invalidPolicyName, Map.of(invalidPhase.getName(), invalidPhase)); Map policyMap = new HashMap<>(); policyMap.put( mixedPolicyName, - new LifecyclePolicyMetadata(mixedPolicy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) + new LifecyclePolicyMetadata(mixedPolicy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong()) ); policyMap.put( allClusterPolicyName, - new LifecyclePolicyMetadata(allClusterPolicy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) + new LifecyclePolicyMetadata(allClusterPolicy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong()) ); policyMap.put( invalidPolicyName, - new LifecyclePolicyMetadata(invalidPolicy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) + new LifecyclePolicyMetadata(invalidPolicy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong()) ); policyStepsRegistry = new PolicyStepsRegistry(NamedXContentRegistry.EMPTY, client, null); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorServiceTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorServiceTests.java index 9e2a67caac253..7a37aaba96c18 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorServiceTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IlmHealthIndicatorServiceTests.java @@ -36,7 +36,6 @@ import org.elasticsearch.xpack.core.ilm.LifecycleSettings; import java.io.IOException; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -177,7 +176,7 @@ public void testIsYellowWhenNotRunningAndPoliciesConfigured() { YELLOW, "Index Lifecycle Management is not running", new SimpleHealthIndicatorDetails(Map.of("ilm_status", status, "policies", 1, "stagnating_indices", 0)), - Collections.singletonList( + List.of( new HealthIndicatorImpact( NAME, IlmHealthIndicatorService.AUTOMATION_DISABLED_IMPACT_ID, @@ -251,7 +250,7 @@ public void testSkippingFieldsWhenVerboseIsFalse() { YELLOW, "Index Lifecycle Management is not running", HealthIndicatorDetails.EMPTY, - Collections.singletonList( + List.of( new HealthIndicatorImpact( NAME, IlmHealthIndicatorService.AUTOMATION_DISABLED_IMPACT_ID, diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleInfoTransportActionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleInfoTransportActionTests.java index d81faf6a398d7..4e8d7440eb773 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleInfoTransportActionTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleInfoTransportActionTests.java @@ -33,7 +33,6 @@ import org.mockito.Mockito; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -75,18 +74,18 @@ public void testUsageStats() throws Exception { indexPolicies.put("index_3", policy1Name); indexPolicies.put("index_4", policy1Name); indexPolicies.put("index_5", policy3Name); - LifecyclePolicy policy1 = new LifecyclePolicy(policy1Name, Collections.emptyMap()); + LifecyclePolicy policy1 = new LifecyclePolicy(policy1Name, Map.of()); policies.add(policy1); - PolicyStats policy1Stats = new PolicyStats(Collections.emptyMap(), 4); + PolicyStats policy1Stats = new PolicyStats(Map.of(), 4); Map phases1 = new HashMap<>(); LifecyclePolicy policy2 = new LifecyclePolicy(policy2Name, phases1); policies.add(policy2); - PolicyStats policy2Stats = new PolicyStats(Collections.emptyMap(), 0); + PolicyStats policy2Stats = new PolicyStats(Map.of(), 0); - LifecyclePolicy policy3 = new LifecyclePolicy(policy3Name, Collections.emptyMap()); + LifecyclePolicy policy3 = new LifecyclePolicy(policy3Name, Map.of()); policies.add(policy3); - PolicyStats policy3Stats = new PolicyStats(Collections.emptyMap(), 1); + PolicyStats policy3Stats = new PolicyStats(Map.of(), 1); ClusterState clusterState = buildClusterState(policies, indexPolicies); Mockito.when(clusterService.state()).thenReturn(clusterState); @@ -110,7 +109,7 @@ public void testUsageStats() throws Exception { private ClusterState buildClusterState(List lifecyclePolicies, Map indexPolicies) { Map lifecyclePolicyMetadatasMap = lifecyclePolicies.stream() - .map(p -> new LifecyclePolicyMetadata(p, Collections.emptyMap(), 1, 0L)) + .map(p -> new LifecyclePolicyMetadata(p, Map.of(), 1, 0L)) .collect(Collectors.toMap(LifecyclePolicyMetadata::getName, Function.identity())); IndexLifecycleMetadata indexLifecycleMetadata = new IndexLifecycleMetadata(lifecyclePolicyMetadatasMap, OperationMode.RUNNING); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleMetadataTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleMetadataTests.java index e757488c2690e..ece83fe6bc437 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleMetadataTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleMetadataTests.java @@ -44,8 +44,6 @@ import org.elasticsearch.xpack.core.ilm.WaitForSnapshotAction; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.SortedMap; @@ -63,10 +61,7 @@ protected IndexLifecycleMetadata createTestInstance() { Map policies = Maps.newMapWithExpectedSize(numPolicies); for (int i = 0; i < numPolicies; i++) { LifecyclePolicy policy = randomTimeseriesLifecyclePolicy(randomAlphaOfLength(4) + i); - policies.put( - policy.getName(), - new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ); + policies.put(policy.getName(), new LifecyclePolicyMetadata(policy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())); } return new IndexLifecycleMetadata(policies, randomFrom(OperationMode.values())); } @@ -84,7 +79,7 @@ protected Reader instanceReader() { @Override protected NamedWriteableRegistry getNamedWriteableRegistry() { return new NamedWriteableRegistry( - Arrays.asList( + List.of( new NamedWriteableRegistry.Entry( LifecycleType.class, TimeseriesLifecycleType.TYPE, @@ -111,7 +106,7 @@ protected NamedWriteableRegistry getNamedWriteableRegistry() { protected NamedXContentRegistry xContentRegistry() { List entries = new ArrayList<>(ClusterModule.getNamedXWriteables()); entries.addAll( - Arrays.asList( + List.of( new NamedXContentRegistry.Entry( LifecycleType.class, new ParseField(TimeseriesLifecycleType.TYPE), @@ -155,7 +150,7 @@ protected Metadata.Custom mutateInstance(Custom instance) { policyName, new LifecyclePolicyMetadata( randomTimeseriesLifecyclePolicy(policyName), - Collections.emptyMap(), + Map.of(), randomNonNegativeLong(), randomNonNegativeLong() ) @@ -192,9 +187,9 @@ public static IndexLifecycleMetadata createTestInstance(int numPolicies, Operati Map phases = Maps.newMapWithExpectedSize(numberPhases); for (int j = 0; j < numberPhases; j++) { TimeValue after = randomTimeValue(0, 1_000_000_000, TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS); - Map actions = Collections.emptyMap(); + Map actions = Map.of(); if (randomBoolean()) { - actions = Collections.singletonMap(DeleteAction.NAME, DeleteAction.WITH_SNAPSHOT_DELETE); + actions = Map.of(DeleteAction.NAME, DeleteAction.WITH_SNAPSHOT_DELETE); } String phaseName = randomAlphaOfLength(10); phases.put(phaseName, new Phase(phaseName, after, actions)); @@ -204,7 +199,7 @@ public static IndexLifecycleMetadata createTestInstance(int numPolicies, Operati policyName, new LifecyclePolicyMetadata( newTestLifecyclePolicy(policyName, phases), - Collections.emptyMap(), + Map.of(), randomNonNegativeLong(), randomNonNegativeLong() ) diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java index 8a4859fcd8b77..374f10b604f18 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunnerTests.java @@ -73,8 +73,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -89,7 +87,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; -import static java.util.stream.Collectors.toList; import static org.elasticsearch.cluster.metadata.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.awaitLatch; import static org.elasticsearch.xpack.core.ilm.LifecycleSettings.LIFECYCLE_HISTORY_INDEX_ENABLED_SETTING; @@ -248,7 +245,7 @@ public void testRunPolicyErrorStepOnRetryableFailedStep() { List waitForRolloverStepList = action.toSteps(client, phaseName, null) .stream() .filter(s -> s.getKey().name().equals(WaitForRolloverReadyStep.NAME)) - .collect(toList()); + .toList(); assertThat(waitForRolloverStepList.size(), is(1)); Step waitForRolloverStep = waitForRolloverStepList.get(0); StepKey stepKey = waitForRolloverStep.getKey(); @@ -288,7 +285,7 @@ public void testRunStateChangePolicyWithNoNextStep() throws Exception { .build(); ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool); DiscoveryNode node = clusterService.localNode(); - IndexLifecycleMetadata ilm = new IndexLifecycleMetadata(Collections.emptyMap(), OperationMode.RUNNING); + IndexLifecycleMetadata ilm = new IndexLifecycleMetadata(Map.of(), OperationMode.RUNNING); ClusterState state = ClusterState.builder(new ClusterName("cluster")) .metadata(Metadata.builder().put(indexMetadata, true).putCustom(IndexLifecycleMetadata.TYPE, ilm)) .nodes(DiscoveryNodes.builder().add(node).masterNodeId(node.getId()).localNodeId(node.getId())) @@ -317,7 +314,7 @@ public void testRunStateChangePolicyWithNextStep() throws Exception { StepKey nextStepKey = new StepKey("phase", "action", "next_cluster_state_action_step"); MockClusterStateActionStep step = new MockClusterStateActionStep(stepKey, nextStepKey); MockClusterStateActionStep nextStep = new MockClusterStateActionStep(nextStepKey, null); - MockPolicyStepsRegistry stepRegistry = createMultiStepPolicyStepRegistry(policyName, Arrays.asList(step, nextStep)); + MockPolicyStepsRegistry stepRegistry = createMultiStepPolicyStepRegistry(policyName, List.of(step, nextStep)); stepRegistry.setResolver((i, k) -> { if (stepKey.equals(k)) { return step; @@ -340,7 +337,7 @@ public void testRunStateChangePolicyWithNextStep() throws Exception { .build(); ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool); DiscoveryNode node = clusterService.localNode(); - IndexLifecycleMetadata ilm = new IndexLifecycleMetadata(Collections.emptyMap(), OperationMode.RUNNING); + IndexLifecycleMetadata ilm = new IndexLifecycleMetadata(Map.of(), OperationMode.RUNNING); ClusterState state = ClusterState.builder(new ClusterName("cluster")) .metadata(Metadata.builder().put(indexMetadata, true).putCustom(IndexLifecycleMetadata.TYPE, ilm)) .nodes(DiscoveryNodes.builder().add(node).masterNodeId(node.getId()).localNodeId(node.getId())) @@ -427,7 +424,7 @@ public void doTestRunPolicyWithFailureToReadPolicy(boolean asyncAction, boolean .build(); ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool); DiscoveryNode node = clusterService.localNode(); - IndexLifecycleMetadata ilm = new IndexLifecycleMetadata(Collections.emptyMap(), OperationMode.RUNNING); + IndexLifecycleMetadata ilm = new IndexLifecycleMetadata(Map.of(), OperationMode.RUNNING); ClusterState state = ClusterState.builder(new ClusterName("cluster")) .metadata(Metadata.builder().put(indexMetadata, true).putCustom(IndexLifecycleMetadata.TYPE, ilm)) .nodes(DiscoveryNodes.builder().add(node).masterNodeId(node.getId()).localNodeId(node.getId())) @@ -476,7 +473,7 @@ public void testRunAsyncActionDoesNotRun() { .build(); ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool); DiscoveryNode node = clusterService.localNode(); - IndexLifecycleMetadata ilm = new IndexLifecycleMetadata(Collections.emptyMap(), OperationMode.RUNNING); + IndexLifecycleMetadata ilm = new IndexLifecycleMetadata(Map.of(), OperationMode.RUNNING); ClusterState state = ClusterState.builder(new ClusterName("cluster")) .metadata(Metadata.builder().put(indexMetadata, true).putCustom(IndexLifecycleMetadata.TYPE, ilm)) .nodes(DiscoveryNodes.builder().add(node).masterNodeId(node.getId()).localNodeId(node.getId())) @@ -503,7 +500,7 @@ public void testRunStateChangePolicyWithAsyncActionNextStep() throws Exception { StepKey nextStepKey = new StepKey("phase", "action", "async_action_step"); MockClusterStateActionStep step = new MockClusterStateActionStep(stepKey, nextStepKey); MockAsyncActionStep nextStep = new MockAsyncActionStep(nextStepKey, null); - MockPolicyStepsRegistry stepRegistry = createMultiStepPolicyStepRegistry(policyName, Arrays.asList(step, nextStep)); + MockPolicyStepsRegistry stepRegistry = createMultiStepPolicyStepRegistry(policyName, List.of(step, nextStep)); stepRegistry.setResolver((i, k) -> { if (stepKey.equals(k)) { return step; @@ -526,7 +523,7 @@ public void testRunStateChangePolicyWithAsyncActionNextStep() throws Exception { .build(); ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool); DiscoveryNode node = clusterService.localNode(); - IndexLifecycleMetadata ilm = new IndexLifecycleMetadata(Collections.emptyMap(), OperationMode.RUNNING); + IndexLifecycleMetadata ilm = new IndexLifecycleMetadata(Map.of(), OperationMode.RUNNING); ClusterState state = ClusterState.builder(new ClusterName("cluster")) .metadata(Metadata.builder().put(indexMetadata, true).putCustom(IndexLifecycleMetadata.TYPE, ilm)) .nodes(DiscoveryNodes.builder().add(node).masterNodeId(node.getId()).localNodeId(node.getId())) @@ -603,7 +600,7 @@ public void testRunPeriodicStep() throws Exception { .build(); ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool); DiscoveryNode node = clusterService.localNode(); - IndexLifecycleMetadata ilm = new IndexLifecycleMetadata(Collections.emptyMap(), OperationMode.RUNNING); + IndexLifecycleMetadata ilm = new IndexLifecycleMetadata(Map.of(), OperationMode.RUNNING); ClusterState state = ClusterState.builder(new ClusterName("cluster")) .metadata(Metadata.builder().put(indexMetadata, true).putCustom(IndexLifecycleMetadata.TYPE, ilm)) .nodes(DiscoveryNodes.builder().add(node).masterNodeId(node.getId()).localNodeId(node.getId())) @@ -785,7 +782,7 @@ public void testGetCurrentStep() { Client client = mock(Client.class); when(client.settings()).thenReturn(Settings.EMPTY); LifecyclePolicy policy = LifecyclePolicyTests.randomTimeseriesLifecyclePolicyWithAllPhases(policyName); - LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Collections.emptyMap(), 1, randomNonNegativeLong()); + LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Map.of(), 1, randomNonNegativeLong()); String phaseName = randomFrom(policy.getPhases().keySet()); Phase phase = policy.getPhases().get(phaseName); PhaseExecutionInfo pei = new PhaseExecutionInfo(policy.getName(), phase, 1, randomNonNegativeLong()); @@ -824,7 +821,7 @@ public void testIsReadyToTransition() { StepKey stepKey = new StepKey("phase", MockAction.NAME, MockAction.NAME); MockAsyncActionStep step = new MockAsyncActionStep(stepKey, null); SortedMap lifecyclePolicyMap = new TreeMap<>( - Collections.singletonMap( + Map.of( policyName, new LifecyclePolicyMetadata( createPolicy(policyName, null, step.getKey()), @@ -834,9 +831,9 @@ public void testIsReadyToTransition() { ) ) ); - Map firstStepMap = Collections.singletonMap(policyName, step); - Map policySteps = Collections.singletonMap(step.getKey(), step); - Map> stepMap = Collections.singletonMap(policyName, policySteps); + Map firstStepMap = Map.of(policyName, step); + Map policySteps = Map.of(step.getKey(), step); + Map> stepMap = Map.of(policyName, policySteps); PolicyStepsRegistry policyStepsRegistry = new PolicyStepsRegistry( lifecyclePolicyMap, firstStepMap, @@ -897,7 +894,7 @@ private static LifecyclePolicy createPolicy(String policyName, StepKey safeStep, assert unsafeStep == null || safeStep.phase().equals(unsafeStep.phase()) == false : "safe and unsafe actions must be in different phases"; Map actions = new HashMap<>(); - List steps = Collections.singletonList(new MockStep(safeStep, null)); + List steps = List.of(new MockStep(safeStep, null)); MockAction safeAction = new MockAction(steps, true); actions.put(safeAction.getWriteableName(), safeAction); Phase phase = new Phase(safeStep.phase(), TimeValue.timeValueMillis(0), actions); @@ -906,7 +903,7 @@ private static LifecyclePolicy createPolicy(String policyName, StepKey safeStep, if (unsafeStep != null) { assert MockAction.NAME.equals(unsafeStep.action()) : "The unsafe action needs to be MockAction.NAME"; Map actions = new HashMap<>(); - List steps = Collections.singletonList(new MockStep(unsafeStep, null)); + List steps = List.of(new MockStep(unsafeStep, null)); MockAction unsafeAction = new MockAction(steps, false); actions.put(unsafeAction.getWriteableName(), unsafeAction); Phase phase = new Phase(unsafeStep.phase(), TimeValue.timeValueMillis(0), actions); @@ -1233,7 +1230,7 @@ public Step getStep(IndexMetadata indexMetadata, StepKey stepKey) { } public static MockPolicyStepsRegistry createOneStepPolicyStepRegistry(String policyName, Step step) { - return createMultiStepPolicyStepRegistry(policyName, Collections.singletonList(step)); + return createMultiStepPolicyStepRegistry(policyName, List.of(step)); } public static MockPolicyStepsRegistry createMultiStepPolicyStepRegistry(String policyName, List steps) { diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java index eceb81542377a..b77e643bc2853 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleServiceTests.java @@ -58,9 +58,9 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneId; -import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.UUID; @@ -114,7 +114,7 @@ public void prepareServices() { }).when(executorService).execute(any()); Settings settings = Settings.builder().put(LifecycleSettings.LIFECYCLE_POLL_INTERVAL, "1s").build(); when(clusterService.getClusterSettings()).thenReturn( - new ClusterSettings(settings, Collections.singleton(LifecycleSettings.LIFECYCLE_POLL_INTERVAL_SETTING)) + new ClusterSettings(settings, Set.of(LifecycleSettings.LIFECYCLE_POLL_INTERVAL_SETTING)) ); when(clusterService.lifecycleState()).thenReturn(State.STARTED); @@ -154,14 +154,11 @@ public void testStoppedModeSkip() { randomStepKey(), randomStepKey() ); - MockAction mockAction = new MockAction(Collections.singletonList(mockStep)); - Phase phase = new Phase("phase", TimeValue.ZERO, Collections.singletonMap("action", mockAction)); - LifecyclePolicy policy = newTestLifecyclePolicy(policyName, Collections.singletonMap(phase.getName(), phase)); + MockAction mockAction = new MockAction(List.of(mockStep)); + Phase phase = new Phase("phase", TimeValue.ZERO, Map.of("action", mockAction)); + LifecyclePolicy policy = newTestLifecyclePolicy(policyName, Map.of(phase.getName(), phase)); SortedMap policyMap = new TreeMap<>(); - policyMap.put( - policyName, - new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ); + policyMap.put(policyName, new LifecyclePolicyMetadata(policy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())); Index index = new Index(randomAlphaOfLengthBetween(1, 20), randomAlphaOfLengthBetween(1, 20)); IndexMetadata indexMetadata = IndexMetadata.builder(index.getName()) .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_NAME, policyName)) @@ -191,14 +188,11 @@ public void testRequestedStopOnShrink() { mockShrinkStep, randomStepKey() ); - MockAction mockAction = new MockAction(Collections.singletonList(mockStep)); - Phase phase = new Phase("phase", TimeValue.ZERO, Collections.singletonMap("action", mockAction)); - LifecyclePolicy policy = newTestLifecyclePolicy(policyName, Collections.singletonMap(phase.getName(), phase)); + MockAction mockAction = new MockAction(List.of(mockStep)); + Phase phase = new Phase("phase", TimeValue.ZERO, Map.of("action", mockAction)); + LifecyclePolicy policy = newTestLifecyclePolicy(policyName, Map.of(phase.getName(), phase)); SortedMap policyMap = new TreeMap<>(); - policyMap.put( - policyName, - new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ); + policyMap.put(policyName, new LifecyclePolicyMetadata(policy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())); Index index = new Index(randomAlphaOfLengthBetween(1, 20), randomAlphaOfLengthBetween(1, 20)); LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); lifecycleState.setPhase(mockShrinkStep.phase()); @@ -250,14 +244,11 @@ private void verifyCanStopWithStep(String stoppableStep) { mockShrinkStep, randomStepKey() ); - MockAction mockAction = new MockAction(Collections.singletonList(mockStep)); - Phase phase = new Phase("phase", TimeValue.ZERO, Collections.singletonMap("action", mockAction)); - LifecyclePolicy policy = newTestLifecyclePolicy(policyName, Collections.singletonMap(phase.getName(), phase)); + MockAction mockAction = new MockAction(List.of(mockStep)); + Phase phase = new Phase("phase", TimeValue.ZERO, Map.of("action", mockAction)); + LifecyclePolicy policy = newTestLifecyclePolicy(policyName, Map.of(phase.getName(), phase)); SortedMap policyMap = new TreeMap<>(); - policyMap.put( - policyName, - new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ); + policyMap.put(policyName, new LifecyclePolicyMetadata(policy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())); Index index = new Index(randomAlphaOfLengthBetween(1, 20), randomAlphaOfLengthBetween(1, 20)); LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); lifecycleState.setPhase(mockShrinkStep.phase()); @@ -301,14 +292,11 @@ public void testRequestedStopOnSafeAction() { currentStepKey, randomStepKey() ); - MockAction mockAction = new MockAction(Collections.singletonList(mockStep)); - Phase phase = new Phase("phase", TimeValue.ZERO, Collections.singletonMap("action", mockAction)); - LifecyclePolicy policy = newTestLifecyclePolicy(policyName, Collections.singletonMap(phase.getName(), phase)); + MockAction mockAction = new MockAction(List.of(mockStep)); + Phase phase = new Phase("phase", TimeValue.ZERO, Map.of("action", mockAction)); + LifecyclePolicy policy = newTestLifecyclePolicy(policyName, Map.of(phase.getName(), phase)); SortedMap policyMap = new TreeMap<>(); - policyMap.put( - policyName, - new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ); + policyMap.put(policyName, new LifecyclePolicyMetadata(policy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())); Index index = new Index(randomAlphaOfLengthBetween(1, 20), randomAlphaOfLengthBetween(1, 20)); LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); lifecycleState.setPhase(currentStepKey.phase()); @@ -370,9 +358,9 @@ public void doTestExceptionStillProcessesOtherIndices(boolean useOnMaster) { } else { i1mockStep = new IndexLifecycleRunnerTests.MockClusterStateActionStep(i1currentStepKey, randomStepKey()); } - MockAction i1mockAction = new MockAction(Collections.singletonList(i1mockStep)); - Phase i1phase = new Phase("phase", TimeValue.ZERO, Collections.singletonMap("action", i1mockAction)); - LifecyclePolicy i1policy = newTestLifecyclePolicy(policy1, Collections.singletonMap(i1phase.getName(), i1phase)); + MockAction i1mockAction = new MockAction(List.of(i1mockStep)); + Phase i1phase = new Phase("phase", TimeValue.ZERO, Map.of("action", i1mockAction)); + LifecyclePolicy i1policy = newTestLifecyclePolicy(policy1, Map.of(i1phase.getName(), i1phase)); Index index1 = new Index(randomAlphaOfLengthBetween(1, 20), randomAlphaOfLengthBetween(1, 20)); LifecycleExecutionState.Builder i1lifecycleState = LifecycleExecutionState.builder(); i1lifecycleState.setPhase(i1currentStepKey.phase()); @@ -387,9 +375,9 @@ public void doTestExceptionStillProcessesOtherIndices(boolean useOnMaster) { } else { i2mockStep = new IndexLifecycleRunnerTests.MockClusterStateActionStep(i2currentStepKey, randomStepKey()); } - MockAction mockAction = new MockAction(Collections.singletonList(i2mockStep)); - Phase i2phase = new Phase("phase", TimeValue.ZERO, Collections.singletonMap("action", mockAction)); - LifecyclePolicy i2policy = newTestLifecyclePolicy(policy1, Collections.singletonMap(i2phase.getName(), i1phase)); + MockAction mockAction = new MockAction(List.of(i2mockStep)); + Phase i2phase = new Phase("phase", TimeValue.ZERO, Map.of("action", mockAction)); + LifecyclePolicy i2policy = newTestLifecyclePolicy(policy1, Map.of(i2phase.getName(), i1phase)); Index index2 = new Index( randomValueOtherThan(index1.getName(), () -> randomAlphaOfLengthBetween(1, 20)), randomAlphaOfLengthBetween(1, 20) @@ -422,14 +410,8 @@ public void doTestExceptionStillProcessesOtherIndices(boolean useOnMaster) { } SortedMap policyMap = new TreeMap<>(); - policyMap.put( - policy1, - new LifecyclePolicyMetadata(i1policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ); - policyMap.put( - policy2, - new LifecyclePolicyMetadata(i2policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ); + policyMap.put(policy1, new LifecyclePolicyMetadata(i1policy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())); + policyMap.put(policy2, new LifecyclePolicyMetadata(i2policy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())); IndexMetadata i1indexMetadata = IndexMetadata.builder(index1.getName()) .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_NAME, policy1)) @@ -533,14 +515,8 @@ public void testIndicesOnShuttingDownNodesInDangerousStep() { SingleNodeShutdownMetadata.Type.REPLACE )) { ClusterState state = ClusterState.builder(ClusterName.DEFAULT).build(); - assertThat( - IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "regular_node"), - equalTo(Collections.emptySet()) - ); - assertThat( - IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "shutdown_node"), - equalTo(Collections.emptySet()) - ); + assertThat(IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "regular_node"), equalTo(Set.of())); + assertThat(IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "shutdown_node"), equalTo(Set.of())); IndexMetadata nonDangerousIndex = IndexMetadata.builder("no_danger") .settings(settings(IndexVersion.current()).put(LifecycleSettings.LIFECYCLE_NAME, "mypolicy")) @@ -583,7 +559,7 @@ public void testIndicesOnShuttingDownNodesInDangerousStep() { Map indices = Map.of("no_danger", nonDangerousIndex, "danger", dangerousIndex); Metadata metadata = Metadata.builder() - .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata(Collections.emptyMap(), OperationMode.RUNNING)) + .putCustom(IndexLifecycleMetadata.TYPE, new IndexLifecycleMetadata(Map.of(), OperationMode.RUNNING)) .indices(indices) .persistentSettings(settings(IndexVersion.current()).build()) .build(); @@ -612,14 +588,8 @@ public void testIndicesOnShuttingDownNodesInDangerousStep() { .build(); // No danger yet, because no node is shutting down - assertThat( - IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "regular_node"), - equalTo(Collections.emptySet()) - ); - assertThat( - IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "shutdown_node"), - equalTo(Collections.emptySet()) - ); + assertThat(IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "regular_node"), equalTo(Set.of())); + assertThat(IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "shutdown_node"), equalTo(Set.of())); state = ClusterState.builder(state) .metadata( @@ -627,7 +597,7 @@ public void testIndicesOnShuttingDownNodesInDangerousStep() { .putCustom( NodesShutdownMetadata.TYPE, new NodesShutdownMetadata( - Collections.singletonMap( + Map.of( "shutdown_node", SingleNodeShutdownMetadata.builder() .setNodeId("shutdown_node") @@ -642,15 +612,12 @@ public void testIndicesOnShuttingDownNodesInDangerousStep() { ) .build(); - assertThat( - IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "regular_node"), - equalTo(Collections.emptySet()) - ); + assertThat(IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "regular_node"), equalTo(Set.of())); // No danger, because this is a "RESTART" type shutdown assertThat( "restart type shutdowns are not considered dangerous", IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "shutdown_node"), - equalTo(Collections.emptySet()) + equalTo(Set.of()) ); final String targetNodeName = type == SingleNodeShutdownMetadata.Type.REPLACE ? randomAlphaOfLengthBetween(10, 20) : null; @@ -661,7 +628,7 @@ public void testIndicesOnShuttingDownNodesInDangerousStep() { .putCustom( NodesShutdownMetadata.TYPE, new NodesShutdownMetadata( - Collections.singletonMap( + Map.of( "shutdown_node", SingleNodeShutdownMetadata.builder() .setNodeId("shutdown_node") @@ -679,10 +646,7 @@ public void testIndicesOnShuttingDownNodesInDangerousStep() { .build(); // The dangerous index should be calculated as being in danger now - assertThat( - IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "shutdown_node"), - equalTo(Collections.singleton("danger")) - ); + assertThat(IndexLifecycleService.indicesOnShuttingDownNodesInDangerousStep(state, "shutdown_node"), equalTo(Set.of("danger"))); } } } diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java index 49aa0a65a5704..a1f51f1fae90f 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/IndexLifecycleTransitionTests.java @@ -48,7 +48,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -80,8 +79,8 @@ public void testMoveClusterStateToNextStep() { .stream() .findFirst() .orElseThrow(() -> new AssertionError("expected next phase to be present")); - List policyMetadatas = Collections.singletonList( - new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) + List policyMetadatas = List.of( + new LifecyclePolicyMetadata(policy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong()) ); Step.StepKey currentStep = new Step.StepKey("current_phase", "current_action", "current_step"); Step.StepKey nextStep = new Step.StepKey(nextPhase.getName(), "next_action", "next_step"); @@ -128,8 +127,8 @@ public void testMoveClusterStateToNextStepSamePhase() { p -> p.getPhases().isEmpty(), () -> LifecyclePolicyTests.randomTestLifecyclePolicy("policy") ); - List policyMetadatas = Collections.singletonList( - new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) + List policyMetadatas = List.of( + new LifecyclePolicyMetadata(policy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong()) ); Step.StepKey currentStep = new Step.StepKey("current_phase", "current_action", "current_step"); Step.StepKey nextStep = new Step.StepKey("current_phase", "next_action", "next_step"); @@ -179,8 +178,8 @@ public void testMoveClusterStateToNextStepSameAction() { p -> p.getPhases().isEmpty(), () -> LifecyclePolicyTests.randomTestLifecyclePolicy("policy") ); - List policyMetadatas = Collections.singletonList( - new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) + List policyMetadatas = List.of( + new LifecyclePolicyMetadata(policy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong()) ); Step.StepKey currentStep = new Step.StepKey("current_phase", "current_action", "current_step"); Step.StepKey nextStep = new Step.StepKey("current_phase", "current_action", "next_step"); @@ -236,8 +235,8 @@ public void testSuccessfulValidatedMoveClusterStateToNextStep() { .stream() .findFirst() .orElseThrow(() -> new AssertionError("expected next phase to be present")); - List policyMetadatas = Collections.singletonList( - new LifecyclePolicyMetadata(policy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) + List policyMetadatas = List.of( + new LifecyclePolicyMetadata(policy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong()) ); Step.StepKey currentStepKey = new Step.StepKey("current_phase", "current_action", "current_step"); Step.StepKey nextStepKey = new Step.StepKey(nextPhase.getName(), "next_action", "next_step"); @@ -279,7 +278,7 @@ public void testValidatedMoveClusterStateToNextStepWithoutPolicy() { lifecycleState.setAction(currentStepKey.action()); lifecycleState.setStep(currentStepKey.name()); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), List.of()); Index index = clusterState.metadata().index(indexName).getIndex(); IllegalArgumentException exception = expectThrows( IllegalArgumentException.class, @@ -303,7 +302,7 @@ public void testValidatedMoveClusterStateToNextStepInvalidNextStep() { lifecycleState.setAction(currentStepKey.action()); lifecycleState.setStep(currentStepKey.name()); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), List.of()); Index index = clusterState.metadata().index(indexName).getIndex(); IllegalArgumentException exception = expectThrows( IllegalArgumentException.class, @@ -325,7 +324,7 @@ public void testMoveClusterStateToErrorStep() throws IOException { lifecycleState.setPhase(currentStep.phase()); lifecycleState.setAction(currentStep.action()); lifecycleState.setStep(currentStep.name()); - ClusterState clusterState = buildClusterState(indexName, Settings.builder(), lifecycleState.build(), Collections.emptyList()); + ClusterState clusterState = buildClusterState(indexName, Settings.builder(), lifecycleState.build(), List.of()); Index index = clusterState.metadata().index(indexName).getIndex(); ClusterState newClusterState = IndexLifecycleTransition.moveClusterStateToErrorStep( @@ -359,7 +358,7 @@ public void testAddStepInfoToClusterState() throws IOException { lifecycleState.setPhase(currentStep.phase()); lifecycleState.setAction(currentStep.action()); lifecycleState.setStep(currentStep.name()); - ClusterState clusterState = buildClusterState(indexName, Settings.builder(), lifecycleState.build(), Collections.emptyList()); + ClusterState clusterState = buildClusterState(indexName, Settings.builder(), lifecycleState.build(), List.of()); Index index = clusterState.metadata().index(indexName).getIndex(); ClusterState newClusterState = IndexLifecycleTransition.addStepInfoToClusterState(index, clusterState, stepInfo); assertClusterStateStepInfo(clusterState, index, currentStep, newClusterState, stepInfo); @@ -378,9 +377,7 @@ public void testRemovePolicyForIndex() { lifecycleState.setAction(currentStep.action()); lifecycleState.setStep(currentStep.name()); List policyMetadatas = new ArrayList<>(); - policyMetadatas.add( - new LifecyclePolicyMetadata(oldPolicy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ); + policyMetadatas.add(new LifecyclePolicyMetadata(oldPolicy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())); ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); Index index = clusterState.metadata().index(indexName).getIndex(); Index[] indices = new Index[] { index }; @@ -399,7 +396,7 @@ public void testRemovePolicyForIndexNoCurrentPolicy() { indexName, indexSettingsBuilder, LifecycleExecutionState.builder().build(), - Collections.emptyList() + List.of() ); Index index = clusterState.metadata().index(indexName).getIndex(); Index[] indices = new Index[] { index }; @@ -414,7 +411,7 @@ public void testRemovePolicyForIndexNoCurrentPolicy() { public void testRemovePolicyForIndexIndexDoesntExist() { String indexName = randomAlphaOfLength(10); String oldPolicyName = "old_policy"; - LifecyclePolicy oldPolicy = newTestLifecyclePolicy(oldPolicyName, Collections.emptyMap()); + LifecyclePolicy oldPolicy = newTestLifecyclePolicy(oldPolicyName, Map.of()); Step.StepKey currentStep = AbstractStepTestCase.randomStepKey(); Settings.Builder indexSettingsBuilder = Settings.builder().put(LifecycleSettings.LIFECYCLE_NAME, oldPolicyName); LifecycleExecutionState.Builder lifecycleState = LifecycleExecutionState.builder(); @@ -422,9 +419,7 @@ public void testRemovePolicyForIndexIndexDoesntExist() { lifecycleState.setAction(currentStep.action()); lifecycleState.setStep(currentStep.name()); List policyMetadatas = new ArrayList<>(); - policyMetadatas.add( - new LifecyclePolicyMetadata(oldPolicy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ); + policyMetadatas.add(new LifecyclePolicyMetadata(oldPolicy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())); ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); Index index = new Index("doesnt_exist", "im_not_here"); Index[] indices = new Index[] { index }; @@ -448,9 +443,7 @@ public void testRemovePolicyForIndexIndexInUnsafe() { lifecycleState.setAction(currentStep.action()); lifecycleState.setStep(currentStep.name()); List policyMetadatas = new ArrayList<>(); - policyMetadatas.add( - new LifecyclePolicyMetadata(oldPolicy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ); + policyMetadatas.add(new LifecyclePolicyMetadata(oldPolicy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())); ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); Index index = clusterState.metadata().index(indexName).getIndex(); Index[] indices = new Index[] { index }; @@ -475,9 +468,7 @@ public void testRemovePolicyWithIndexingComplete() { lifecycleState.setAction(currentStep.action()); lifecycleState.setStep(currentStep.name()); List policyMetadatas = new ArrayList<>(); - policyMetadatas.add( - new LifecyclePolicyMetadata(oldPolicy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ); + policyMetadatas.add(new LifecyclePolicyMetadata(oldPolicy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())); ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), policyMetadatas); Index index = clusterState.metadata().index(indexName).getIndex(); Index[] indices = new Index[] { index }; @@ -756,7 +747,7 @@ public void testMoveClusterStateToFailedStep() { LifecyclePolicy policy = createPolicy(policyName, failedStepKey, null); LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata( policy, - Collections.emptyMap(), + Map.of(), randomNonNegativeLong(), randomNonNegativeLong() ); @@ -771,12 +762,7 @@ public void testMoveClusterStateToFailedStep() { lifecycleState.setStep(errorStepKey.name()); lifecycleState.setStepTime(now); lifecycleState.setFailedStep(failedStepKey.name()); - ClusterState clusterState = buildClusterState( - indexName, - indexSettingsBuilder, - lifecycleState.build(), - Collections.singletonList(policyMetadata) - ); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), List.of(policyMetadata)); Index index = clusterState.metadata().index(indexName).getIndex(); ClusterState nextClusterState = IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep( clusterState, @@ -802,7 +788,7 @@ public void testMoveClusterStateToFailedStepWithUnknownStep() { LifecyclePolicy policy = createPolicy(policyName, failedStepKey, null); LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata( policy, - Collections.emptyMap(), + Map.of(), randomNonNegativeLong(), randomNonNegativeLong() ); @@ -817,12 +803,7 @@ public void testMoveClusterStateToFailedStepWithUnknownStep() { lifecycleState.setStep(errorStepKey.name()); lifecycleState.setStepTime(now); lifecycleState.setFailedStep(failedStepKey.name()); - ClusterState clusterState = buildClusterState( - indexName, - indexSettingsBuilder, - lifecycleState.build(), - Collections.singletonList(policyMetadata) - ); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), List.of(policyMetadata)); IllegalArgumentException exception = expectThrows( IllegalArgumentException.class, () -> IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep(clusterState, indexName, () -> now, policyRegistry, false) @@ -840,7 +821,7 @@ public void testMoveClusterStateToFailedStepIndexNotFound() { existingIndexName, Settings.builder(), LifecycleExecutionState.builder().build(), - Collections.emptyList() + List.of() ); IllegalArgumentException exception = expectThrows( IllegalArgumentException.class, @@ -863,7 +844,7 @@ public void testMoveClusterStateToFailedStepInvalidPolicySetting() { lifecycleState.setAction(errorStepKey.action()); lifecycleState.setStep(errorStepKey.name()); lifecycleState.setFailedStep(failedStepKey.name()); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), List.of()); IllegalArgumentException exception = expectThrows( IllegalArgumentException.class, () -> IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep(clusterState, indexName, () -> now, policyRegistry, false) @@ -883,7 +864,7 @@ public void testMoveClusterStateToFailedNotOnError() { lifecycleState.setPhase(failedStepKey.phase()); lifecycleState.setAction(failedStepKey.action()); lifecycleState.setStep(failedStepKey.name()); - ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), Collections.emptyList()); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), List.of()); IllegalArgumentException exception = expectThrows( IllegalArgumentException.class, () -> IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep(clusterState, indexName, () -> now, policyRegistry, false) @@ -906,7 +887,7 @@ public void testMoveClusterStateToPreviouslyFailedStepAsAutomaticRetryAndSetsPre LifecyclePolicy policy = createPolicy(policyName, failedStepKey, null); LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata( policy, - Collections.emptyMap(), + Map.of(), randomNonNegativeLong(), randomNonNegativeLong() ); @@ -923,12 +904,7 @@ public void testMoveClusterStateToPreviouslyFailedStepAsAutomaticRetryAndSetsPre lifecycleState.setFailedStep(failedStepKey.name()); String initialStepInfo = randomAlphaOfLengthBetween(10, 50); lifecycleState.setStepInfo(initialStepInfo); - ClusterState clusterState = buildClusterState( - indexName, - indexSettingsBuilder, - lifecycleState.build(), - Collections.singletonList(policyMetadata) - ); + ClusterState clusterState = buildClusterState(indexName, indexSettingsBuilder, lifecycleState.build(), List.of(policyMetadata)); Index index = clusterState.metadata().index(indexName).getIndex(); ClusterState nextClusterState = IndexLifecycleTransition.moveClusterStateToPreviouslyFailedStep( clusterState, @@ -976,13 +952,11 @@ public void testMoveToFailedStepDoesntRefreshCachedPhaseWhenUnsafe() { Map actions = new HashMap<>(); actions.put("set_priority", new SetPriorityAction(100)); Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); - Map phases = Collections.singletonMap("hot", hotPhase); + Map phases = Map.of("hot", hotPhase); LifecyclePolicy currentPolicy = new LifecyclePolicy("my-policy", phases); List policyMetadatas = new ArrayList<>(); - policyMetadatas.add( - new LifecyclePolicyMetadata(currentPolicy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ); + policyMetadatas.add(new LifecyclePolicyMetadata(currentPolicy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())); Step.StepKey errorStepKey = new Step.StepKey("hot", RolloverAction.NAME, ErrorStep.NAME); PolicyStepsRegistry stepsRegistry = createOneStepPolicyStepRegistry("my-policy", new ErrorStep(errorStepKey)); @@ -1040,9 +1014,9 @@ public void testRefreshPhaseJson() throws IOException { actions.put("rollover", new RolloverAction(null, null, null, 1L, null, null, null, null, null, null)); actions.put("set_priority", new SetPriorityAction(100)); Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); - Map phases = Collections.singletonMap("hot", hotPhase); + Map phases = Map.of("hot", hotPhase); LifecyclePolicy newPolicy = new LifecyclePolicy("my-policy", phases); - LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(newPolicy, Collections.emptyMap(), 2L, 2L); + LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(newPolicy, Map.of(), 2L, 2L); ClusterState existingState = ClusterState.builder(ClusterState.EMPTY_STATE) .metadata(Metadata.builder(Metadata.EMPTY_METADATA).put(meta, false).build()) @@ -1185,7 +1159,7 @@ public void testMoveStateToNextActionAndUpdateCachedPhase() { actions.put("rollover", new RolloverAction(null, null, null, 1L, null, null, null, null, null, null)); actions.put("set_priority", new SetPriorityAction(100)); Phase hotPhase = new Phase("hot", TimeValue.ZERO, actions); - Map phases = Collections.singletonMap("hot", hotPhase); + Map phases = Map.of("hot", hotPhase); LifecyclePolicy currentPolicy = new LifecyclePolicy("my-policy", phases); { @@ -1195,10 +1169,10 @@ public void testMoveStateToNextActionAndUpdateCachedPhase() { Map actionsWithoutRollover = new HashMap<>(); actionsWithoutRollover.put("set_priority", new SetPriorityAction(100)); Phase hotPhaseNoRollover = new Phase("hot", TimeValue.ZERO, actionsWithoutRollover); - Map phasesNoRollover = Collections.singletonMap("hot", hotPhaseNoRollover); + Map phasesNoRollover = Map.of("hot", hotPhaseNoRollover); LifecyclePolicyMetadata updatedPolicyMetadata = new LifecyclePolicyMetadata( new LifecyclePolicy("my-policy", phasesNoRollover), - Collections.emptyMap(), + Map.of(), 2L, 2L ); @@ -1233,10 +1207,10 @@ public void testMoveStateToNextActionAndUpdateCachedPhase() { Map actionsWitoutSetPriority = new HashMap<>(); actionsWitoutSetPriority.put("rollover", new RolloverAction(null, null, null, 1L, null, null, null, null, null, null)); Phase hotPhaseNoSetPriority = new Phase("hot", TimeValue.ZERO, actionsWitoutSetPriority); - Map phasesWithoutSetPriority = Collections.singletonMap("hot", hotPhaseNoSetPriority); + Map phasesWithoutSetPriority = Map.of("hot", hotPhaseNoSetPriority); LifecyclePolicyMetadata updatedPolicyMetadata = new LifecyclePolicyMetadata( new LifecyclePolicy("my-policy", phasesWithoutSetPriority), - Collections.emptyMap(), + Map.of(), 2L, 2L ); @@ -1275,7 +1249,7 @@ private static LifecyclePolicy createPolicy(String policyName, Step.StepKey safe assert unsafeStep == null || safeStep.phase().equals(unsafeStep.phase()) == false : "safe and unsafe actions must be in different phases"; Map actions = new HashMap<>(); - List steps = Collections.singletonList(new MockStep(safeStep, null)); + List steps = List.of(new MockStep(safeStep, null)); MockAction safeAction = new MockAction(steps, true); actions.put(safeAction.getWriteableName(), safeAction); Phase phase = new Phase(safeStep.phase(), TimeValue.timeValueMillis(0), actions); @@ -1284,7 +1258,7 @@ private static LifecyclePolicy createPolicy(String policyName, Step.StepKey safe if (unsafeStep != null) { assert MockAction.NAME.equals(unsafeStep.action()) : "The unsafe action needs to be MockAction.NAME"; Map actions = new HashMap<>(); - List steps = Collections.singletonList(new MockStep(unsafeStep, null)); + List steps = List.of(new MockStep(unsafeStep, null)); MockAction unsafeAction = new MockAction(steps, false); actions.put(unsafeAction.getWriteableName(), unsafeAction); Phase phase = new Phase(unsafeStep.phase(), TimeValue.timeValueMillis(0), actions); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTaskTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTaskTests.java index eee3fe3ce53c2..81688ec1503cd 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTaskTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToErrorStepUpdateTaskTests.java @@ -28,7 +28,7 @@ import org.elasticsearch.xpack.core.ilm.Step.StepKey; import org.junit.Before; -import java.util.Collections; +import java.util.Map; import static org.elasticsearch.cluster.metadata.LifecycleExecutionState.ILM_CUSTOM_METADATA_KEY; import static org.hamcrest.Matchers.containsString; @@ -53,10 +53,7 @@ public void setupClusterState() { .build(); index = indexMetadata.getIndex(); IndexLifecycleMetadata ilmMeta = new IndexLifecycleMetadata( - Collections.singletonMap( - policy, - new LifecyclePolicyMetadata(lifecyclePolicy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ), + Map.of(policy, new LifecyclePolicyMetadata(lifecyclePolicy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())), OperationMode.RUNNING ); Metadata metadata = Metadata.builder() diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToNextStepUpdateTaskTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToNextStepUpdateTaskTests.java index f9a8d4a2ab486..554e9a48c625e 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToNextStepUpdateTaskTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/MoveToNextStepUpdateTaskTests.java @@ -29,7 +29,6 @@ import org.junit.Before; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -67,10 +66,7 @@ public void setupClusterState() { index = indexMetadata.getIndex(); lifecyclePolicy = LifecyclePolicyTests.randomTestLifecyclePolicy(policy); IndexLifecycleMetadata ilmMeta = new IndexLifecycleMetadata( - Collections.singletonMap( - policy, - new LifecyclePolicyMetadata(lifecyclePolicy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ), + Map.of(policy, new LifecyclePolicyMetadata(lifecyclePolicy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())), OperationMode.RUNNING ); Metadata metadata = Metadata.builder() @@ -95,7 +91,7 @@ public void testExecuteSuccessfullyMoved() throws Exception { AlwaysExistingStepRegistry stepRegistry = new AlwaysExistingStepRegistry(client); stepRegistry.update( new IndexLifecycleMetadata( - Map.of(policy, new LifecyclePolicyMetadata(lifecyclePolicy, Collections.emptyMap(), 2L, 2L)), + Map.of(policy, new LifecyclePolicyMetadata(lifecyclePolicy, Map.of(), 2L, 2L)), OperationMode.RUNNING ) ); @@ -169,7 +165,7 @@ public void testExecuteSuccessfulMoveWithInvalidNextStep() throws Exception { AlwaysExistingStepRegistry stepRegistry = new AlwaysExistingStepRegistry(client); stepRegistry.update( new IndexLifecycleMetadata( - Map.of(policy, new LifecyclePolicyMetadata(lifecyclePolicy, Collections.emptyMap(), 2L, 2L)), + Map.of(policy, new LifecyclePolicyMetadata(lifecyclePolicy, Map.of(), 2L, 2L)), OperationMode.RUNNING ) ); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/PolicyStepsRegistryTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/PolicyStepsRegistryTests.java index 36d537a57382c..f61267d40a513 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/PolicyStepsRegistryTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/PolicyStepsRegistryTests.java @@ -46,7 +46,6 @@ import org.elasticsearch.xpack.core.ilm.Step; import org.mockito.Mockito; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -76,7 +75,7 @@ private IndexMetadata emptyMetadata(Index index) { public void testGetFirstStep() { String policyName = randomAlphaOfLengthBetween(2, 10); Step expectedFirstStep = new MockStep(MOCK_STEP_KEY, null); - Map firstStepMap = Collections.singletonMap(policyName, expectedFirstStep); + Map firstStepMap = Map.of(policyName, expectedFirstStep); PolicyStepsRegistry registry = new PolicyStepsRegistry(null, firstStepMap, null, NamedXContentRegistry.EMPTY, null, null); Step actualFirstStep = registry.getFirstStep(policyName); assertThat(actualFirstStep, sameInstance(expectedFirstStep)); @@ -85,7 +84,7 @@ public void testGetFirstStep() { public void testGetFirstStepUnknownPolicy() { String policyName = randomAlphaOfLengthBetween(2, 10); Step expectedFirstStep = new MockStep(MOCK_STEP_KEY, null); - Map firstStepMap = Collections.singletonMap(policyName, expectedFirstStep); + Map firstStepMap = Map.of(policyName, expectedFirstStep); PolicyStepsRegistry registry = new PolicyStepsRegistry(null, firstStepMap, null, NamedXContentRegistry.EMPTY, null, null); Step actualFirstStep = registry.getFirstStep(policyName + "unknown"); assertNull(actualFirstStep); @@ -95,7 +94,7 @@ public void testGetStep() { Client client = mock(Client.class); Mockito.when(client.settings()).thenReturn(Settings.EMPTY); LifecyclePolicy policy = LifecyclePolicyTests.randomTimeseriesLifecyclePolicyWithAllPhases("policy"); - LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Collections.emptyMap(), 1, randomNonNegativeLong()); + LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Map.of(), 1, randomNonNegativeLong()); String phaseName = randomFrom(policy.getPhases().keySet()); Phase phase = policy.getPhases().get(phaseName); PhaseExecutionInfo pei = new PhaseExecutionInfo(policy.getName(), phase, 1, randomNonNegativeLong()); @@ -119,7 +118,7 @@ public void testGetStepErrorStep() { Step.StepKey errorStepKey = new Step.StepKey(randomAlphaOfLengthBetween(1, 10), randomAlphaOfLengthBetween(1, 10), ErrorStep.NAME); Step expectedStep = new ErrorStep(errorStepKey); Index index = new Index("test", "uuid"); - Map> indexSteps = Collections.singletonMap(index, Collections.singletonList(expectedStep)); + Map> indexSteps = Map.of(index, List.of(expectedStep)); PolicyStepsRegistry registry = new PolicyStepsRegistry(null, null, null, NamedXContentRegistry.EMPTY, null, null); Step actualStep = registry.getStep(emptyMetadata(index), errorStepKey); assertThat(actualStep, equalTo(expectedStep)); @@ -143,7 +142,7 @@ public void testGetStepForIndexWithNoPhaseGetsInitializationStep() { Client client = mock(Client.class); Mockito.when(client.settings()).thenReturn(Settings.EMPTY); LifecyclePolicy policy = LifecyclePolicyTests.randomTimeseriesLifecyclePolicy("policy"); - LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Collections.emptyMap(), 1, randomNonNegativeLong()); + LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Map.of(), 1, randomNonNegativeLong()); IndexMetadata indexMetadata = IndexMetadata.builder("test") .settings(indexSettings(IndexVersion.current(), 1, 0).put(LifecycleSettings.LIFECYCLE_NAME, "policy").build()) .build(); @@ -158,7 +157,7 @@ public void testGetStepUnknownStepKey() { Client client = mock(Client.class); Mockito.when(client.settings()).thenReturn(Settings.EMPTY); LifecyclePolicy policy = LifecyclePolicyTests.randomTimeseriesLifecyclePolicyWithAllPhases("policy"); - LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Collections.emptyMap(), 1, randomNonNegativeLong()); + LifecyclePolicyMetadata policyMetadata = new LifecyclePolicyMetadata(policy, Map.of(), 1, randomNonNegativeLong()); String phaseName = randomFrom(policy.getPhases().keySet()); Phase phase = policy.getPhases().get(phaseName); PhaseExecutionInfo pei = new PhaseExecutionInfo(policy.getName(), phase, 1, randomNonNegativeLong()); @@ -193,7 +192,7 @@ public void testUpdateFromNothingToSomethingToNothing() throws Exception { headers.put(randomAlphaOfLength(10), randomAlphaOfLength(10)); headers.put(randomAlphaOfLength(10), randomAlphaOfLength(10)); } - Map policyMap = Collections.singletonMap( + Map policyMap = Map.of( newPolicy.getName(), new LifecyclePolicyMetadata(newPolicy, headers, randomNonNegativeLong(), randomNonNegativeLong()) ); @@ -271,7 +270,7 @@ public void testUpdateFromNothingToSomethingToNothing() throws Exception { assertThat(registry.getStepMap(), equalTo(registryStepMap)); // remove policy - lifecycleMetadata = new IndexLifecycleMetadata(Collections.emptyMap(), OperationMode.RUNNING); + lifecycleMetadata = new IndexLifecycleMetadata(Map.of(), OperationMode.RUNNING); currentState = ClusterState.builder(currentState) .metadata(Metadata.builder(metadata).putCustom(IndexLifecycleMetadata.TYPE, lifecycleMetadata)) .build(); @@ -291,7 +290,7 @@ public void testUpdateChangedPolicy() { headers.put(randomAlphaOfLength(10), randomAlphaOfLength(10)); headers.put(randomAlphaOfLength(10), randomAlphaOfLength(10)); } - Map policyMap = Collections.singletonMap( + Map policyMap = Map.of( newPolicy.getName(), new LifecyclePolicyMetadata(newPolicy, headers, randomNonNegativeLong(), randomNonNegativeLong()) ); @@ -316,10 +315,7 @@ public void testUpdateChangedPolicy() { // swap out policy newPolicy = LifecyclePolicyTests.randomTestLifecyclePolicy(policyName); lifecycleMetadata = new IndexLifecycleMetadata( - Collections.singletonMap( - policyName, - new LifecyclePolicyMetadata(newPolicy, Collections.emptyMap(), randomNonNegativeLong(), randomNonNegativeLong()) - ), + Map.of(policyName, new LifecyclePolicyMetadata(newPolicy, Map.of(), randomNonNegativeLong(), randomNonNegativeLong())), OperationMode.RUNNING ); currentState = ClusterState.builder(currentState) @@ -356,7 +352,7 @@ public void testUpdatePolicyButNoPhaseChangeIndexStepsDontChange() throws Except headers.put(randomAlphaOfLength(10), randomAlphaOfLength(10)); headers.put(randomAlphaOfLength(10), randomAlphaOfLength(10)); } - Map policyMap = Collections.singletonMap( + Map policyMap = Map.of( newPolicy.getName(), new LifecyclePolicyMetadata(newPolicy, headers, randomNonNegativeLong(), randomNonNegativeLong()) ); @@ -411,7 +407,7 @@ public void testUpdatePolicyButNoPhaseChangeIndexStepsDontChange() throws Except assertThat(((ShrinkStep) gotStep).getNumberOfShards(), equalTo(1)); // Update the policy with the new policy, but keep the phase the same - policyMap = Collections.singletonMap( + policyMap = Map.of( updatedPolicy.getName(), new LifecyclePolicyMetadata(updatedPolicy, headers, randomNonNegativeLong(), randomNonNegativeLong()) ); @@ -457,7 +453,7 @@ public void testGetStepMultithreaded() throws Exception { .build(); SortedMap metas = new TreeMap<>(); - metas.put("policy", new LifecyclePolicyMetadata(policy, Collections.emptyMap(), 1, randomNonNegativeLong())); + metas.put("policy", new LifecyclePolicyMetadata(policy, Map.of(), 1, randomNonNegativeLong())); IndexLifecycleMetadata meta = new IndexLifecycleMetadata(metas, OperationMode.RUNNING); PolicyStepsRegistry registry = new PolicyStepsRegistry(REGISTRY, client, null); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/StagnatingIndicesFinderTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/StagnatingIndicesFinderTests.java index be2d449353242..95412f92b6156 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/StagnatingIndicesFinderTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/StagnatingIndicesFinderTests.java @@ -28,7 +28,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.LongSupplier; -import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -61,7 +60,7 @@ public void testStagnatingIndicesFinder() { assertEquals(expectedMaxTimeOnStep, maxTimeOnStep); assertEquals(expectedMaxRetriesPerStep, maxRetriesPerStep); return rc; - }).collect(Collectors.toList()); + }).toList(); // Per the evaluator, the timeSupplier _must_ be called only twice when(mockedTimeSupplier.getAsLong()).thenReturn(instant, instant); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportStopILMActionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportStopILMActionTests.java index 8c0fede4c11dc..bd0d63ebb0f3d 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportStopILMActionTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportStopILMActionTests.java @@ -24,7 +24,8 @@ import org.elasticsearch.xpack.core.ilm.action.ILMActions; import org.mockito.ArgumentMatcher; -import static java.util.Collections.emptyMap; +import java.util.Map; + import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -50,7 +51,7 @@ public void testStopILMClusterStatePriorityIsImmediate() { ILMActions.STOP.name(), "description", new TaskId(randomLong() + ":" + randomLong()), - emptyMap() + Map.of() ); StopILMRequest request = new StopILMRequest(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT); transportStopILMAction.masterOperation(task, request, ClusterState.EMPTY_STATE, ActionListener.noop()); From b460f081c24dca466fd60b5105900282ef3d3017 Mon Sep 17 00:00:00 2001 From: Mayya Sharipova Date: Thu, 12 Dec 2024 10:24:55 -0500 Subject: [PATCH 52/77] [DOCS] _index_prefix for highligh matched_fields (#118569) Enhance documenation to explain that "_index_prefix" subfield must be added to `matched_fields` param for highlighting a main field. When doing prefix queries on fields that are indexed with prefixes, "_index_prefix" subfield is used. If we try to highlight the main field, we may not get any results. "_index_prefix" subfield must be added to `matched_fields` which instructs ES to use matches from "_index_prefix" to highlight the main field. --- .../mapping/params/index-prefixes.asciidoc | 27 +++++++++++++++++++ .../mapping/types/search-as-you-type.asciidoc | 15 +++++++++++ 2 files changed, 42 insertions(+) diff --git a/docs/reference/mapping/params/index-prefixes.asciidoc b/docs/reference/mapping/params/index-prefixes.asciidoc index a143c5531c81b..1d5e844467b6f 100644 --- a/docs/reference/mapping/params/index-prefixes.asciidoc +++ b/docs/reference/mapping/params/index-prefixes.asciidoc @@ -54,3 +54,30 @@ PUT my-index-000001 } } -------------------------------- + +`index_prefixes` parameter instructs {ES} to create a subfield "._index_prefix". This +field will be used to do fast prefix queries. When doing highlighting, add "._index_prefix" +subfield to the `matched_fields` parameter to highlight the main field based on the +found matches of the prefix field, like in the request below: + +[source,console] +-------------------------------- +GET my-index-000001/_search +{ + "query": { + "prefix": { + "full_name": { + "value": "ki" + } + } + }, + "highlight": { + "fields": { + "full_name": { + "matched_fields": ["full_name._index_prefix"] + } + } + } +} +-------------------------------- +// TEST[continued] diff --git a/docs/reference/mapping/types/search-as-you-type.asciidoc b/docs/reference/mapping/types/search-as-you-type.asciidoc index 3c71389f4cebb..c2673a614c265 100644 --- a/docs/reference/mapping/types/search-as-you-type.asciidoc +++ b/docs/reference/mapping/types/search-as-you-type.asciidoc @@ -97,11 +97,21 @@ GET my-index-000001/_search "my_field._3gram" ] } + }, + "highlight": { + "fields": { + "my_field": { + "matched_fields": ["my_field._index_prefix"] <1> + } + } } } -------------------------------------------------- // TEST[continued] +<1> Adding "my_field._index_prefix" to the `matched_fields` allows to highlight + "my_field" also based on matches from "my_field._index_prefix" field. + [source,console-result] -------------------------------------------------- { @@ -126,6 +136,11 @@ GET my-index-000001/_search "_score" : 0.8630463, "_source" : { "my_field" : "quick brown fox jump lazy dog" + }, + "highlight": { + "my_field": [ + "quick brown fox jump lazy dog" + ] } } ] From 65a2342e28d7645768bac8e4f397e44da42790b0 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Thu, 12 Dec 2024 16:29:12 +0100 Subject: [PATCH 53/77] Legacy index version to target indices created before N-2 (#118443) Legacy index versions can only be read via the archive indices functionality. The supported versions are currently 5.x and 6.x. For 9.0, we are not going to add support for indices created in 7.x as archive indices. Rather we will allow reading from N - 2 directly from Lucene, without relying on archive indices. IndexVersion#isLegacyIndexVersion is tied to the archive indices functionality and identifies index versions that can only be present in the cluster as archive. This commit aligns isLegacyIndexVersion to only match versions before N - 2. --- .../org/elasticsearch/cluster/metadata/IndexMetadata.java | 8 ++++---- .../main/java/org/elasticsearch/env/NodeEnvironment.java | 3 +-- .../main/java/org/elasticsearch/index/IndexVersion.java | 7 ++++++- .../main/java/org/elasticsearch/index/IndexVersions.java | 3 ++- .../elasticsearch/repositories/RepositoriesModule.java | 4 +++- .../xpack/lucene/bwc/AbstractArchiveTestCase.java | 6 +----- 6 files changed, 17 insertions(+), 14 deletions(-) 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 952789e1bf746..c74275991c899 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexMetadata.java @@ -1116,10 +1116,10 @@ public IndexVersion getCreationVersion() { /** * Return the {@link IndexVersion} that this index provides compatibility for. - * This is typically compared to the {@link IndexVersions#MINIMUM_COMPATIBLE} to figure out whether the index can be handled - * by the cluster. - * By default, this is equal to the {@link #getCreationVersion()}, but can also be a newer version if the index has been imported as - * a legacy index from an older snapshot, and its metadata has been converted to be handled by newer version nodes. + * This is typically compared to the {@link IndexVersions#MINIMUM_COMPATIBLE} or {@link IndexVersions#MINIMUM_READONLY_COMPATIBLE} + * to figure out whether the index can be handled by the cluster. + * By default, this is equal to the {@link #getCreationVersion()}, but can also be a newer version if the index has been created by + * a legacy version, and imported archive, in which case its metadata has been converted to be handled by newer version nodes. */ public IndexVersion getCompatibilityVersion() { return indexCompatibilityVersion; diff --git a/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java b/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java index f3a3fc9f771d4..afadb8f5b3011 100644 --- a/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java +++ b/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java @@ -524,8 +524,7 @@ static void checkForIndexCompatibility(Logger logger, DataPath... dataPaths) thr logger.info("oldest index version recorded in NodeMetadata {}", metadata.oldestIndexVersion()); - if (metadata.oldestIndexVersion().isLegacyIndexVersion()) { - + if (metadata.oldestIndexVersion().before(IndexVersions.MINIMUM_COMPATIBLE)) { String bestDowngradeVersion = getBestDowngradeVersion(metadata.previousNodeVersion().toString()); throw new IllegalStateException( "Cannot start this node because it holds metadata for indices with version [" diff --git a/server/src/main/java/org/elasticsearch/index/IndexVersion.java b/server/src/main/java/org/elasticsearch/index/IndexVersion.java index cfb51cc3b5aef..22deee1325625 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexVersion.java +++ b/server/src/main/java/org/elasticsearch/index/IndexVersion.java @@ -123,8 +123,13 @@ public static IndexVersion current() { return CurrentHolder.CURRENT; } + /** + * Returns whether this index version is supported by this node version out-of-the-box. + * This is used to distinguish between ordinary indices and archive indices that may be + * imported into the cluster in read-only mode, and with limited functionality. + */ public boolean isLegacyIndexVersion() { - return before(IndexVersions.MINIMUM_COMPATIBLE); + return before(IndexVersions.MINIMUM_READONLY_COMPATIBLE); } public static IndexVersion getMinimumCompatibleIndexVersion(int versionId) { diff --git a/server/src/main/java/org/elasticsearch/index/IndexVersions.java b/server/src/main/java/org/elasticsearch/index/IndexVersions.java index 5589508507aec..0aaae2104576a 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexVersions.java +++ b/server/src/main/java/org/elasticsearch/index/IndexVersions.java @@ -192,6 +192,7 @@ private static Version parseUnchecked(String version) { */ public static final IndexVersion MINIMUM_COMPATIBLE = V_8_0_0; + public static final IndexVersion MINIMUM_READONLY_COMPATIBLE = V_7_0_0; static final NavigableMap VERSION_IDS = getAllVersionIds(IndexVersions.class); static final IndexVersion LATEST_DEFINED; @@ -207,7 +208,7 @@ static NavigableMap getAllVersionIds(Class cls) { Map versionIdFields = new HashMap<>(); NavigableMap builder = new TreeMap<>(); - Set ignore = Set.of("ZERO", "MINIMUM_COMPATIBLE"); + Set ignore = Set.of("ZERO", "MINIMUM_COMPATIBLE", "MINIMUM_READONLY_COMPATIBLE"); for (Field declaredField : cls.getFields()) { if (declaredField.getType().equals(IndexVersion.class)) { diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesModule.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesModule.java index e767bf5e3723b..b236b1fa730f1 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesModule.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesModule.java @@ -102,7 +102,9 @@ public RepositoriesModule( } if (preRestoreChecks.isEmpty()) { preRestoreChecks.add((snapshot, version) -> { - if (version.isLegacyIndexVersion()) { + // pre-restore checks will be run against the version in which the snapshot was created as well as + // the version in which the restored index was created + if (version.before(IndexVersions.MINIMUM_COMPATIBLE)) { throw new SnapshotRestoreException( snapshot, "the snapshot was created with Elasticsearch version [" diff --git a/x-pack/plugin/old-lucene-versions/src/internalClusterTest/java/org/elasticsearch/xpack/lucene/bwc/AbstractArchiveTestCase.java b/x-pack/plugin/old-lucene-versions/src/internalClusterTest/java/org/elasticsearch/xpack/lucene/bwc/AbstractArchiveTestCase.java index 71f788727aa23..a24c673c1aef8 100644 --- a/x-pack/plugin/old-lucene-versions/src/internalClusterTest/java/org/elasticsearch/xpack/lucene/bwc/AbstractArchiveTestCase.java +++ b/x-pack/plugin/old-lucene-versions/src/internalClusterTest/java/org/elasticsearch/xpack/lucene/bwc/AbstractArchiveTestCase.java @@ -94,11 +94,7 @@ public IndexMetadata getSnapshotIndexMetaData(RepositoryData repositoryData, Sna .put( IndexMetadata.SETTING_INDEX_VERSION_CREATED.getKey(), metadata.settings() - .getAsVersionId( - "version", - IndexVersion::fromId, - IndexVersion.fromId(randomFrom(5000099, 6000099, 7000099)) - ) + .getAsVersionId("version", IndexVersion::fromId, IndexVersion.fromId(randomFrom(5000099, 6000099))) ) ) .build(); From 78488c70f4062764d9d8e078cfffe059047a9945 Mon Sep 17 00:00:00 2001 From: Dan Rubinstein Date: Thu, 12 Dec 2024 10:38:03 -0500 Subject: [PATCH 54/77] Retry on ClusterBlockException on transform destination index (#118194) * Retry on ClusterBlockException on transform destination index * Update docs/changelog/118194.yaml * Cleaning up tests * Fixing tests --------- Co-authored-by: Elastic Machine --- docs/changelog/118194.yaml | 5 + .../transforms/TransformFailureHandler.java | 45 +++- .../TransformFailureHandlerTests.java | 231 +++++++++++++----- 3 files changed, 220 insertions(+), 61 deletions(-) create mode 100644 docs/changelog/118194.yaml diff --git a/docs/changelog/118194.yaml b/docs/changelog/118194.yaml new file mode 100644 index 0000000000000..0e5eca55d597c --- /dev/null +++ b/docs/changelog/118194.yaml @@ -0,0 +1,5 @@ +pr: 118194 +summary: Retry on `ClusterBlockException` on transform destination index +area: Machine Learning +type: enhancement +issues: [] diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformFailureHandler.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformFailureHandler.java index 337d3c5820c07..24586e5f36337 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformFailureHandler.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformFailureHandler.java @@ -169,7 +169,14 @@ private void handleScriptException(ScriptException scriptException, boolean unat * @param numFailureRetries the number of configured retries */ private void handleBulkIndexingException(BulkIndexingException bulkIndexingException, boolean unattended, int numFailureRetries) { - if (unattended == false && bulkIndexingException.isIrrecoverable()) { + if (bulkIndexingException.getCause() instanceof ClusterBlockException) { + retryWithoutIncrementingFailureCount( + bulkIndexingException, + bulkIndexingException.getDetailedMessage(), + unattended, + numFailureRetries + ); + } else if (unattended == false && bulkIndexingException.isIrrecoverable()) { String message = TransformMessages.getMessage( TransformMessages.LOG_TRANSFORM_PIVOT_IRRECOVERABLE_BULK_INDEXING_ERROR, bulkIndexingException.getDetailedMessage() @@ -232,12 +239,46 @@ private void retry(Throwable unwrappedException, String message, boolean unatten && unwrappedException.getClass().equals(context.getLastFailure().getClass()); final int failureCount = context.incrementAndGetFailureCount(unwrappedException); - if (unattended == false && numFailureRetries != -1 && failureCount > numFailureRetries) { fail(unwrappedException, "task encountered more than " + numFailureRetries + " failures; latest failure: " + message); return; } + logRetry(unwrappedException, message, unattended, numFailureRetries, failureCount, repeatedFailure); + } + + /** + * Terminate failure handling without incrementing the retries used + *

+ * This is used when there is an ongoing recoverable issue and we want to retain + * retries for any issues that may occur after the issue is resolved + * + * @param unwrappedException The exception caught + * @param message error message to log/audit + * @param unattended whether the transform runs in unattended mode + * @param numFailureRetries the number of configured retries + */ + private void retryWithoutIncrementingFailureCount( + Throwable unwrappedException, + String message, + boolean unattended, + int numFailureRetries + ) { + // group failures to decide whether to report it below + final boolean repeatedFailure = context.getLastFailure() != null + && unwrappedException.getClass().equals(context.getLastFailure().getClass()); + + logRetry(unwrappedException, message, unattended, numFailureRetries, context.getFailureCount(), repeatedFailure); + } + + private void logRetry( + Throwable unwrappedException, + String message, + boolean unattended, + int numFailureRetries, + int failureCount, + boolean repeatedFailure + ) { // Since our schedule fires again very quickly after failures it is possible to run into the same failure numerous // times in a row, very quickly. We do not want to spam the audit log with repeated failures, so only record the first one // and if the number of retries is about to exceed diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformFailureHandlerTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformFailureHandlerTests.java index 84c8d4e140408..3894ff3043ccd 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformFailureHandlerTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformFailureHandlerTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.xpack.core.transform.transforms.TransformTaskState; import org.elasticsearch.xpack.transform.notifications.MockTransformAuditor; +import java.util.List; import java.util.Map; import java.util.Set; @@ -63,9 +64,121 @@ public int getFailureCountChangedCounter() { } } - public void testUnattended() { + public void testHandleIndexerFailure_CircuitBreakingExceptionNewPageSizeLessThanMinimumPageSize() { + var e = new CircuitBreakingException(randomAlphaOfLength(10), 1, 0, randomFrom(CircuitBreaker.Durability.values())); + assertRetryIfUnattendedOtherwiseFail(e); + } + + public void testHandleIndexerFailure_CircuitBreakingExceptionNewPageSizeNotLessThanMinimumPageSize() { + var e = new CircuitBreakingException(randomAlphaOfLength(10), 1, 1, randomFrom(CircuitBreaker.Durability.values())); + + List.of(true, false).forEach((unattended) -> { assertNoFailureAndContextPageSizeSet(e, unattended, 365); }); + } + + public void testHandleIndexerFailure_ScriptException() { + var e = new ScriptException( + randomAlphaOfLength(10), + new ArithmeticException(randomAlphaOfLength(10)), + singletonList(randomAlphaOfLength(10)), + randomAlphaOfLength(10), + randomAlphaOfLength(10) + ); + assertRetryIfUnattendedOtherwiseFail(e); + } + + public void testHandleIndexerFailure_BulkIndexExceptionWrappingClusterBlockException() { + final BulkIndexingException bulkIndexingException = new BulkIndexingException( + randomAlphaOfLength(10), + new ClusterBlockException(Map.of("test-index", Set.of(MetadataIndexStateService.INDEX_CLOSED_BLOCK))), + randomBoolean() + ); + + List.of(true, false).forEach((unattended) -> { assertRetryFailureCountNotIncremented(bulkIndexingException, unattended); }); + } + + public void testHandleIndexerFailure_IrrecoverableBulkIndexException() { + final BulkIndexingException e = new BulkIndexingException( + randomAlphaOfLength(10), + new ElasticsearchStatusException(randomAlphaOfLength(10), RestStatus.INTERNAL_SERVER_ERROR), + true + ); + assertRetryIfUnattendedOtherwiseFail(e); + } + + public void testHandleIndexerFailure_RecoverableBulkIndexException() { + final BulkIndexingException bulkIndexingException = new BulkIndexingException( + randomAlphaOfLength(10), + new ElasticsearchStatusException(randomAlphaOfLength(10), RestStatus.INTERNAL_SERVER_ERROR), + false + ); + + List.of(true, false).forEach((unattended) -> { assertRetry(bulkIndexingException, unattended); }); + } + + public void testHandleIndexerFailure_ClusterBlockException() { + List.of(true, false).forEach((unattended) -> { + assertRetry( + new ClusterBlockException(Map.of(randomAlphaOfLength(10), Set.of(MetadataIndexStateService.INDEX_CLOSED_BLOCK))), + unattended + ); + }); + } + + public void testHandleIndexerFailure_SearchPhaseExecutionExceptionWithNoShardSearchFailures() { + List.of(true, false).forEach((unattended) -> { + assertRetry( + new SearchPhaseExecutionException(randomAlphaOfLength(10), randomAlphaOfLength(10), ShardSearchFailure.EMPTY_ARRAY), + unattended + ); + }); + } + + public void testHandleIndexerFailure_SearchPhaseExecutionExceptionWithShardSearchFailures() { + List.of(true, false).forEach((unattended) -> { + assertRetry( + new SearchPhaseExecutionException( + randomAlphaOfLength(10), + randomAlphaOfLength(10), + new ShardSearchFailure[] { new ShardSearchFailure(new Exception()) } + ), + unattended + ); + }); + } + + public void testHandleIndexerFailure_RecoverableElasticsearchException() { + List.of(true, false).forEach((unattended) -> { + assertRetry(new ElasticsearchStatusException(randomAlphaOfLength(10), RestStatus.INTERNAL_SERVER_ERROR), unattended); + }); + } + + public void testHandleIndexerFailure_IrrecoverableElasticsearchException() { + var e = new ElasticsearchStatusException(randomAlphaOfLength(10), RestStatus.NOT_FOUND); + assertRetryIfUnattendedOtherwiseFail(e); + } + + public void testHandleIndexerFailure_IllegalArgumentException() { + var e = new IllegalArgumentException(randomAlphaOfLength(10)); + assertRetryIfUnattendedOtherwiseFail(e); + } + + public void testHandleIndexerFailure_UnexpectedException() { + List.of(true, false).forEach((unattended) -> { assertRetry(new Exception(), unattended); }); + } + + private void assertRetryIfUnattendedOtherwiseFail(Exception e) { + List.of(true, false).forEach((unattended) -> { + if (unattended) { + assertRetry(e, unattended); + } else { + assertFailure(e); + } + }); + } + + private void assertRetry(Exception e, boolean unattended) { String transformId = randomAlphaOfLength(10); - SettingsConfig settings = new SettingsConfig.Builder().setUnattended(true).build(); + SettingsConfig settings = new SettingsConfig.Builder().setNumFailureRetries(2).setUnattended(unattended).build(); MockTransformAuditor auditor = MockTransformAuditor.createMockAuditor(); MockTransformContextListener contextListener = new MockTransformContextListener(); @@ -74,51 +187,33 @@ public void testUnattended() { TransformFailureHandler handler = new TransformFailureHandler(auditor, context, transformId); - handler.handleIndexerFailure( - new SearchPhaseExecutionException( - "query", - "Partial shards failure", - new ShardSearchFailure[] { - new ShardSearchFailure(new CircuitBreakingException("to much memory", 110, 100, CircuitBreaker.Durability.TRANSIENT)) } - ), - settings - ); + assertNoFailure(handler, e, contextListener, settings, true); + assertNoFailure(handler, e, contextListener, settings, true); + if (unattended) { + assertNoFailure(handler, e, contextListener, settings, true); + } else { + // fail after max retry attempts reached + assertFailure(handler, e, contextListener, settings, true); + } + } - // CBE isn't a failure, but it only affects page size(which we don't test here) - assertFalse(contextListener.getFailed()); - assertEquals(0, contextListener.getFailureCountChangedCounter()); + private void assertRetryFailureCountNotIncremented(Exception e, boolean unattended) { + String transformId = randomAlphaOfLength(10); + SettingsConfig settings = new SettingsConfig.Builder().setNumFailureRetries(2).setUnattended(unattended).build(); - assertNoFailure( - handler, - new SearchPhaseExecutionException( - "query", - "Partial shards failure", - new ShardSearchFailure[] { - new ShardSearchFailure( - new ScriptException( - "runtime error", - new ArithmeticException("/ by zero"), - singletonList("stack"), - "test", - "painless" - ) - ) } - ), - contextListener, - settings - ); - assertNoFailure( - handler, - new ElasticsearchStatusException("something really bad happened", RestStatus.INTERNAL_SERVER_ERROR), - contextListener, - settings - ); - assertNoFailure(handler, new IllegalArgumentException("expected apples not oranges"), contextListener, settings); - assertNoFailure(handler, new RuntimeException("the s*** hit the fan"), contextListener, settings); - assertNoFailure(handler, new NullPointerException("NPE"), contextListener, settings); + MockTransformAuditor auditor = MockTransformAuditor.createMockAuditor(); + MockTransformContextListener contextListener = new MockTransformContextListener(); + TransformContext context = new TransformContext(TransformTaskState.STARTED, "", 0, contextListener); + context.setPageSize(500); + + TransformFailureHandler handler = new TransformFailureHandler(auditor, context, transformId); + + assertNoFailure(handler, e, contextListener, settings, false); + assertNoFailure(handler, e, contextListener, settings, false); + assertNoFailure(handler, e, contextListener, settings, false); } - public void testClusterBlock() { + private void assertFailure(Exception e) { String transformId = randomAlphaOfLength(10); SettingsConfig settings = new SettingsConfig.Builder().setNumFailureRetries(2).build(); @@ -129,32 +224,50 @@ public void testClusterBlock() { TransformFailureHandler handler = new TransformFailureHandler(auditor, context, transformId); - final ClusterBlockException clusterBlock = new ClusterBlockException( - Map.of("test-index", Set.of(MetadataIndexStateService.INDEX_CLOSED_BLOCK)) - ); + assertFailure(handler, e, contextListener, settings, false); + } - handler.handleIndexerFailure(clusterBlock, settings); - assertFalse(contextListener.getFailed()); - assertEquals(1, contextListener.getFailureCountChangedCounter()); + private void assertNoFailure( + TransformFailureHandler handler, + Exception e, + MockTransformContextListener mockTransformContextListener, + SettingsConfig settings, + boolean failureCountIncremented + ) { + handler.handleIndexerFailure(e, settings); + assertFalse(mockTransformContextListener.getFailed()); + assertEquals(failureCountIncremented ? 1 : 0, mockTransformContextListener.getFailureCountChangedCounter()); + mockTransformContextListener.reset(); + } - handler.handleIndexerFailure(clusterBlock, settings); - assertFalse(contextListener.getFailed()); - assertEquals(2, contextListener.getFailureCountChangedCounter()); + private void assertNoFailureAndContextPageSizeSet(Exception e, boolean unattended, int newPageSize) { + String transformId = randomAlphaOfLength(10); + SettingsConfig settings = new SettingsConfig.Builder().setNumFailureRetries(2).setUnattended(unattended).build(); - handler.handleIndexerFailure(clusterBlock, settings); - assertTrue(contextListener.getFailed()); - assertEquals(3, contextListener.getFailureCountChangedCounter()); + MockTransformAuditor auditor = MockTransformAuditor.createMockAuditor(); + MockTransformContextListener contextListener = new MockTransformContextListener(); + TransformContext context = new TransformContext(TransformTaskState.STARTED, "", 0, contextListener); + context.setPageSize(500); + + TransformFailureHandler handler = new TransformFailureHandler(auditor, context, transformId); + + handler.handleIndexerFailure(e, settings); + assertFalse(contextListener.getFailed()); + assertEquals(0, contextListener.getFailureCountChangedCounter()); + assertEquals(newPageSize, context.getPageSize()); + contextListener.reset(); } - private void assertNoFailure( + private void assertFailure( TransformFailureHandler handler, Exception e, MockTransformContextListener mockTransformContextListener, - SettingsConfig settings + SettingsConfig settings, + boolean failureCountChanged ) { handler.handleIndexerFailure(e, settings); - assertFalse(mockTransformContextListener.getFailed()); - assertEquals(1, mockTransformContextListener.getFailureCountChangedCounter()); + assertTrue(mockTransformContextListener.getFailed()); + assertEquals(failureCountChanged ? 1 : 0, mockTransformContextListener.getFailureCountChangedCounter()); mockTransformContextListener.reset(); } From d53ccafce47ddbe17a53aa3340fc93ffb5c33ac3 Mon Sep 17 00:00:00 2001 From: Craig Taverner Date: Thu, 12 Dec 2024 16:43:55 +0100 Subject: [PATCH 55/77] Minimize code diff after Re-enable LOOKUP JOIN tests in 8.18 (#118373) When we back-ported the LOOKUP JOIN PRs to 8.x (see #117967), we found it necessary to disable all csv-spec tests since they create indices with mode:lookup, which is illegal in the cluster state of mixed clusters where other nodes do not understand the new index mode. We need to re-enable the tests, and make sure the tests are only disabled in mixed clusters with node versions too old to handle the new mode. --- .../esql/qa/server/mixed-cluster/build.gradle | 1 + .../esql/qa/mixed/MixedClusterEsqlSpecIT.java | 7 ++++ .../xpack/esql/ccq/MultiClusterSpecIT.java | 7 ++++ .../xpack/esql/qa/rest/EsqlSpecTestCase.java | 41 +++++++++++++------ .../rest/generative/GenerativeRestTest.java | 2 +- .../xpack/esql/CsvTestsDataLoader.java | 14 ++++--- 6 files changed, 52 insertions(+), 20 deletions(-) diff --git a/x-pack/plugin/esql/qa/server/mixed-cluster/build.gradle b/x-pack/plugin/esql/qa/server/mixed-cluster/build.gradle index eac5d5764d4b2..971eed1c6795d 100644 --- a/x-pack/plugin/esql/qa/server/mixed-cluster/build.gradle +++ b/x-pack/plugin/esql/qa/server/mixed-cluster/build.gradle @@ -27,6 +27,7 @@ restResources { dependencies { javaRestTestImplementation project(xpackModule('esql:qa:testFixtures')) javaRestTestImplementation project(xpackModule('esql:qa:server')) + javaRestTestImplementation project(xpackModule('esql')) } GradleUtils.extendSourceSet(project, "javaRestTest", "yamlRestTest") diff --git a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java index 801e1d12b1d4a..81070b3155f2e 100644 --- a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java +++ b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java @@ -18,8 +18,10 @@ import org.junit.ClassRule; import java.io.IOException; +import java.util.List; import static org.elasticsearch.xpack.esql.CsvTestUtils.isEnabled; +import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V4; import static org.elasticsearch.xpack.esql.qa.rest.EsqlSpecTestCase.Mode.ASYNC; public class MixedClusterEsqlSpecIT extends EsqlSpecTestCase { @@ -92,6 +94,11 @@ protected boolean supportsInferenceTestService() { return false; } + @Override + protected boolean supportsIndexModeLookup() throws IOException { + return hasCapabilities(List.of(JOIN_LOOKUP_V4.capabilityName())); + } + @Override protected boolean deduplicateExactWarnings() { /* diff --git a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java index e658d169cbce8..2ec75683ab149 100644 --- a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java +++ b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java @@ -280,4 +280,11 @@ protected boolean enableRoundingDoubleValuesOnAsserting() { protected boolean supportsInferenceTestService() { return false; } + + @Override + protected boolean supportsIndexModeLookup() throws IOException { + // CCS does not yet support JOIN_LOOKUP_V4 and clusters falsely report they have this capability + // return hasCapabilities(List.of(JOIN_LOOKUP_V4.capabilityName())); + return false; + } } diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/EsqlSpecTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/EsqlSpecTestCase.java index 2484a428c4b03..e7bce73e21cdd 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/EsqlSpecTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/EsqlSpecTestCase.java @@ -135,8 +135,8 @@ public void setup() throws IOException { createInferenceEndpoint(client()); } - if (indexExists(availableDatasetsForEs(client()).iterator().next().indexName()) == false) { - loadDataSetIntoEs(client()); + if (indexExists(availableDatasetsForEs(client(), supportsIndexModeLookup()).iterator().next().indexName()) == false) { + loadDataSetIntoEs(client(), supportsIndexModeLookup()); } } @@ -182,12 +182,30 @@ protected void shouldSkipTest(String testName) throws IOException { protected static void checkCapabilities(RestClient client, TestFeatureService testFeatureService, String testName, CsvTestCase testCase) throws IOException { - if (testCase.requiredCapabilities.isEmpty()) { + if (hasCapabilities(client, testCase.requiredCapabilities)) { return; } + + var features = new EsqlFeatures().getFeatures().stream().map(NodeFeature::id).collect(Collectors.toSet()); + + for (String feature : testCase.requiredCapabilities) { + var esqlFeature = "esql." + feature; + assumeTrue("Requested capability " + feature + " is an ESQL cluster feature", features.contains(esqlFeature)); + assumeTrue("Test " + testName + " requires " + feature, testFeatureService.clusterHasFeature(esqlFeature)); + } + } + + protected static boolean hasCapabilities(List requiredCapabilities) throws IOException { + return hasCapabilities(adminClient(), requiredCapabilities); + } + + protected static boolean hasCapabilities(RestClient client, List requiredCapabilities) throws IOException { + if (requiredCapabilities.isEmpty()) { + return true; + } try { - if (clusterHasCapability(client, "POST", "/_query", List.of(), testCase.requiredCapabilities).orElse(false)) { - return; + if (clusterHasCapability(client, "POST", "/_query", List.of(), requiredCapabilities).orElse(false)) { + return true; } LOGGER.info("capabilities API returned false, we might be in a mixed version cluster so falling back to cluster features"); } catch (ResponseException e) { @@ -206,20 +224,17 @@ protected static void checkCapabilities(RestClient client, TestFeatureService te throw e; } } - - var features = new EsqlFeatures().getFeatures().stream().map(NodeFeature::id).collect(Collectors.toSet()); - - for (String feature : testCase.requiredCapabilities) { - var esqlFeature = "esql." + feature; - assumeTrue("Requested capability " + feature + " is an ESQL cluster feature", features.contains(esqlFeature)); - assumeTrue("Test " + testName + " requires " + feature, testFeatureService.clusterHasFeature(esqlFeature)); - } + return false; } protected boolean supportsInferenceTestService() { return true; } + protected boolean supportsIndexModeLookup() throws IOException { + return true; + } + protected final void doTest() throws Throwable { RequestObjectBuilder builder = new RequestObjectBuilder(randomFrom(XContentType.values())); diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java index 63c184e973cde..588d5870d89ec 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java @@ -46,7 +46,7 @@ public abstract class GenerativeRestTest extends ESRestTestCase { @Before public void setup() throws IOException { if (indexExists(CSV_DATASET_MAP.keySet().iterator().next()) == false) { - loadDataSetIntoEs(client()); + loadDataSetIntoEs(client(), true); } } diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java index 34af1edb9f99b..dbeb54996733a 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java @@ -235,7 +235,7 @@ public static void main(String[] args) throws IOException { } try (RestClient client = builder.build()) { - loadDataSetIntoEs(client, (restClient, indexName, indexMapping, indexSettings) -> { + loadDataSetIntoEs(client, true, (restClient, indexName, indexMapping, indexSettings) -> { // don't use ESRestTestCase methods here or, if you do, test running the main method before making the change StringBuilder jsonBody = new StringBuilder("{"); if (indexSettings != null && indexSettings.isEmpty() == false) { @@ -254,26 +254,28 @@ public static void main(String[] args) throws IOException { } } - public static Set availableDatasetsForEs(RestClient client) throws IOException { + public static Set availableDatasetsForEs(RestClient client, boolean supportsIndexModeLookup) throws IOException { boolean inferenceEnabled = clusterHasInferenceEndpoint(client); return CSV_DATASET_MAP.values() .stream() .filter(d -> d.requiresInferenceEndpoint == false || inferenceEnabled) + .filter(d -> supportsIndexModeLookup || d.indexName.endsWith("_lookup") == false) // TODO: use actual index settings .collect(Collectors.toCollection(HashSet::new)); } - public static void loadDataSetIntoEs(RestClient client) throws IOException { - loadDataSetIntoEs(client, (restClient, indexName, indexMapping, indexSettings) -> { + public static void loadDataSetIntoEs(RestClient client, boolean supportsIndexModeLookup) throws IOException { + loadDataSetIntoEs(client, supportsIndexModeLookup, (restClient, indexName, indexMapping, indexSettings) -> { ESRestTestCase.createIndex(restClient, indexName, indexSettings, indexMapping, null); }); } - private static void loadDataSetIntoEs(RestClient client, IndexCreator indexCreator) throws IOException { + private static void loadDataSetIntoEs(RestClient client, boolean supportsIndexModeLookup, IndexCreator indexCreator) + throws IOException { Logger logger = LogManager.getLogger(CsvTestsDataLoader.class); Set loadedDatasets = new HashSet<>(); - for (var dataset : availableDatasetsForEs(client)) { + for (var dataset : availableDatasetsForEs(client, supportsIndexModeLookup)) { load(client, dataset, logger, indexCreator); loadedDatasets.add(dataset.indexName); } From d270158c1189ec553ff1d56d03010ed478832007 Mon Sep 17 00:00:00 2001 From: Jan Kuipers <148754765+jan-elastic@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:50:53 +0100 Subject: [PATCH 56/77] ES|QL categorize with multiple groupings (#118173) * ES|QL categorize with multiple groupings. * Fix VerifierTests * Close stuff when constructing CategorizePackedValuesBlockHash fails * CategorizePackedValuesBlockHashTests * Improve categorize javadocs * Update docs/changelog/118173.yaml * Create CategorizePackedValuesBlockHash's deletegate page differently * Double check in BlockHash builder for single categorize * Reuse blocks array * More CSV tests * Remove assumeTrue categorize_v5 * Rename test * Two more verifier tests * more CSV tests * Add JavaDocs/comments * spotless * Refactor/unify recategorize * Better memory accounting * fix csv test * randomize CategorizePackedValuesBlockHashTests * Add TODO --- docs/changelog/118173.yaml | 5 + .../aggregation/blockhash/BlockHash.java | 13 +- .../blockhash/CategorizeBlockHash.java | 79 +++--- .../CategorizePackedValuesBlockHash.java | 170 ++++++++++++ .../operator/HashAggregationOperator.java | 8 +- .../blockhash/CategorizeBlockHashTests.java | 3 - .../CategorizePackedValuesBlockHashTests.java | 248 ++++++++++++++++++ .../src/main/resources/categorize.csv-spec | 169 ++++++++++++ .../xpack/esql/action/EsqlCapabilities.java | 4 + .../xpack/esql/analysis/Verifier.java | 8 +- .../function/grouping/Categorize.java | 3 +- .../xpack/esql/analysis/VerifierTests.java | 37 ++- .../optimizer/LogicalPlanOptimizerTests.java | 5 - 13 files changed, 676 insertions(+), 76 deletions(-) create mode 100644 docs/changelog/118173.yaml create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/CategorizePackedValuesBlockHash.java create mode 100644 x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/blockhash/CategorizePackedValuesBlockHashTests.java diff --git a/docs/changelog/118173.yaml b/docs/changelog/118173.yaml new file mode 100644 index 0000000000000..a3c9054674ba5 --- /dev/null +++ b/docs/changelog/118173.yaml @@ -0,0 +1,5 @@ +pr: 118173 +summary: ES|QL categorize with multiple groupings +area: Machine Learning +type: feature +issues: [] diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/BlockHash.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/BlockHash.java index 9b53e6558f4db..191d6443264ca 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/BlockHash.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/BlockHash.java @@ -180,13 +180,16 @@ public static BlockHash buildCategorizeBlockHash( List groups, AggregatorMode aggregatorMode, BlockFactory blockFactory, - AnalysisRegistry analysisRegistry + AnalysisRegistry analysisRegistry, + int emitBatchSize ) { - if (groups.size() != 1) { - throw new IllegalArgumentException("only a single CATEGORIZE group can used"); + if (groups.size() == 1) { + return new CategorizeBlockHash(blockFactory, groups.get(0).channel, aggregatorMode, analysisRegistry); + } else { + assert groups.get(0).isCategorize(); + assert groups.subList(1, groups.size()).stream().noneMatch(GroupSpec::isCategorize); + return new CategorizePackedValuesBlockHash(groups, blockFactory, aggregatorMode, analysisRegistry, emitBatchSize); } - - return new CategorizeBlockHash(blockFactory, groups.get(0).channel, aggregatorMode, analysisRegistry); } /** diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/CategorizeBlockHash.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/CategorizeBlockHash.java index 35c6faf84e623..f83776fbdbc85 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/CategorizeBlockHash.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/CategorizeBlockHash.java @@ -44,7 +44,7 @@ import java.util.Objects; /** - * Base BlockHash implementation for {@code Categorize} grouping function. + * BlockHash implementation for {@code Categorize} grouping function. */ public class CategorizeBlockHash extends BlockHash { @@ -53,11 +53,9 @@ public class CategorizeBlockHash extends BlockHash { ); private static final int NULL_ORD = 0; - // TODO: this should probably also take an emitBatchSize private final int channel; private final AggregatorMode aggregatorMode; private final TokenListCategorizer.CloseableTokenListCategorizer categorizer; - private final CategorizeEvaluator evaluator; /** @@ -95,12 +93,14 @@ public class CategorizeBlockHash extends BlockHash { } } + boolean seenNull() { + return seenNull; + } + @Override public void add(Page page, GroupingAggregatorFunction.AddInput addInput) { - if (aggregatorMode.isInputPartial() == false) { - addInitial(page, addInput); - } else { - addIntermediate(page, addInput); + try (IntBlock block = add(page)) { + addInput.add(0, block); } } @@ -129,50 +129,38 @@ public void close() { Releasables.close(evaluator, categorizer); } + private IntBlock add(Page page) { + return aggregatorMode.isInputPartial() == false ? addInitial(page) : addIntermediate(page); + } + /** * Adds initial (raw) input to the state. */ - private void addInitial(Page page, GroupingAggregatorFunction.AddInput addInput) { - try (IntBlock result = (IntBlock) evaluator.eval(page.getBlock(channel))) { - addInput.add(0, result); - } + IntBlock addInitial(Page page) { + return (IntBlock) evaluator.eval(page.getBlock(channel)); } /** * Adds intermediate state to the state. */ - private void addIntermediate(Page page, GroupingAggregatorFunction.AddInput addInput) { + private IntBlock addIntermediate(Page page) { if (page.getPositionCount() == 0) { - return; + return null; } BytesRefBlock categorizerState = page.getBlock(channel); if (categorizerState.areAllValuesNull()) { seenNull = true; - try (var newIds = blockFactory.newConstantIntVector(NULL_ORD, 1)) { - addInput.add(0, newIds); - } - return; - } - - Map idMap = readIntermediate(categorizerState.getBytesRef(0, new BytesRef())); - try (IntBlock.Builder newIdsBuilder = blockFactory.newIntBlockBuilder(idMap.size())) { - int fromId = idMap.containsKey(0) ? 0 : 1; - int toId = fromId + idMap.size(); - for (int i = fromId; i < toId; i++) { - newIdsBuilder.appendInt(idMap.get(i)); - } - try (IntBlock newIds = newIdsBuilder.build()) { - addInput.add(0, newIds); - } + return blockFactory.newConstantIntBlockWith(NULL_ORD, 1); } + return recategorize(categorizerState.getBytesRef(0, new BytesRef()), null).asBlock(); } /** - * Read intermediate state from a block. - * - * @return a map from the old category id to the new one. The old ids go from 0 to {@code size - 1}. + * Reads the intermediate state from a block and recategorizes the provided IDs. + * If no IDs are provided, the IDs are the IDs in the categorizer's state in order. + * (So 0...N-1 or 1...N, depending on whether null is present.) */ - private Map readIntermediate(BytesRef bytes) { + IntVector recategorize(BytesRef bytes, IntVector ids) { Map idMap = new HashMap<>(); try (StreamInput in = new BytesArray(bytes).streamInput()) { if (in.readBoolean()) { @@ -185,10 +173,22 @@ private Map readIntermediate(BytesRef bytes) { // +1 because the 0 ordinal is reserved for null idMap.put(oldCategoryId + 1, newCategoryId + 1); } - return idMap; } catch (IOException e) { throw new RuntimeException(e); } + try (IntVector.Builder newIdsBuilder = blockFactory.newIntVectorBuilder(idMap.size())) { + if (ids == null) { + int idOffset = idMap.containsKey(0) ? 0 : 1; + for (int i = 0; i < idMap.size(); i++) { + newIdsBuilder.appendInt(idMap.get(i + idOffset)); + } + } else { + for (int i = 0; i < ids.getPositionCount(); i++) { + newIdsBuilder.appendInt(idMap.get(ids.getInt(i))); + } + } + return newIdsBuilder.build(); + } } /** @@ -198,15 +198,20 @@ private Block buildIntermediateBlock() { if (categorizer.getCategoryCount() == 0) { return blockFactory.newConstantNullBlock(seenNull ? 1 : 0); } + int positionCount = categorizer.getCategoryCount() + (seenNull ? 1 : 0); + // We're returning a block with N positions just because the Page must have all blocks with the same position count! + return blockFactory.newConstantBytesRefBlockWith(serializeCategorizer(), positionCount); + } + + BytesRef serializeCategorizer() { + // TODO: This BytesStreamOutput is not accounted for by the circuit breaker. Fix that! try (BytesStreamOutput out = new BytesStreamOutput()) { out.writeBoolean(seenNull); out.writeVInt(categorizer.getCategoryCount()); for (SerializableTokenListCategory category : categorizer.toCategoriesById()) { category.writeTo(out); } - // We're returning a block with N positions just because the Page must have all blocks with the same position count! - int positionCount = categorizer.getCategoryCount() + (seenNull ? 1 : 0); - return blockFactory.newConstantBytesRefBlockWith(out.bytes().toBytesRef(), positionCount); + return out.bytes().toBytesRef(); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/CategorizePackedValuesBlockHash.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/CategorizePackedValuesBlockHash.java new file mode 100644 index 0000000000000..20874cb10ceb8 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/blockhash/CategorizePackedValuesBlockHash.java @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.blockhash; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.BitArray; +import org.elasticsearch.compute.aggregation.AggregatorMode; +import org.elasticsearch.compute.aggregation.GroupingAggregatorFunction; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BlockFactory; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.core.ReleasableIterator; +import org.elasticsearch.core.Releasables; +import org.elasticsearch.index.analysis.AnalysisRegistry; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * BlockHash implementation for {@code Categorize} grouping function as first + * grouping expression, followed by one or mode other grouping expressions. + *

+ * For the first grouping (the {@code Categorize} grouping function), a + * {@code CategorizeBlockHash} is used, which outputs integers (category IDs). + * Next, a {@code PackedValuesBlockHash} is used on the category IDs and the + * other groupings (which are not {@code Categorize}s). + */ +public class CategorizePackedValuesBlockHash extends BlockHash { + + private final List specs; + private final AggregatorMode aggregatorMode; + private final Block[] blocks; + private final CategorizeBlockHash categorizeBlockHash; + private final PackedValuesBlockHash packedValuesBlockHash; + + CategorizePackedValuesBlockHash( + List specs, + BlockFactory blockFactory, + AggregatorMode aggregatorMode, + AnalysisRegistry analysisRegistry, + int emitBatchSize + ) { + super(blockFactory); + this.specs = specs; + this.aggregatorMode = aggregatorMode; + blocks = new Block[specs.size()]; + + List delegateSpecs = new ArrayList<>(); + delegateSpecs.add(new GroupSpec(0, ElementType.INT)); + for (int i = 1; i < specs.size(); i++) { + delegateSpecs.add(new GroupSpec(i, specs.get(i).elementType())); + } + + boolean success = false; + try { + categorizeBlockHash = new CategorizeBlockHash(blockFactory, specs.get(0).channel(), aggregatorMode, analysisRegistry); + packedValuesBlockHash = new PackedValuesBlockHash(delegateSpecs, blockFactory, emitBatchSize); + success = true; + } finally { + if (success == false) { + close(); + } + } + } + + @Override + public void add(Page page, GroupingAggregatorFunction.AddInput addInput) { + try (IntBlock categories = getCategories(page)) { + blocks[0] = categories; + for (int i = 1; i < specs.size(); i++) { + blocks[i] = page.getBlock(specs.get(i).channel()); + } + packedValuesBlockHash.add(new Page(blocks), addInput); + } + } + + private IntBlock getCategories(Page page) { + if (aggregatorMode.isInputPartial() == false) { + return categorizeBlockHash.addInitial(page); + } else { + BytesRefBlock stateBlock = page.getBlock(0); + BytesRef stateBytes = stateBlock.getBytesRef(0, new BytesRef()); + try (StreamInput in = new BytesArray(stateBytes).streamInput()) { + BytesRef categorizerState = in.readBytesRef(); + try (IntVector ids = IntVector.readFrom(blockFactory, in)) { + return categorizeBlockHash.recategorize(categorizerState, ids).asBlock(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public Block[] getKeys() { + Block[] keys = packedValuesBlockHash.getKeys(); + if (aggregatorMode.isOutputPartial() == false) { + // For final output, the keys are the category regexes. + try ( + BytesRefBlock regexes = (BytesRefBlock) categorizeBlockHash.getKeys()[0]; + BytesRefBlock.Builder builder = blockFactory.newBytesRefBlockBuilder(keys[0].getPositionCount()) + ) { + IntVector idsVector = (IntVector) keys[0].asVector(); + int idsOffset = categorizeBlockHash.seenNull() ? 0 : -1; + BytesRef scratch = new BytesRef(); + for (int i = 0; i < idsVector.getPositionCount(); i++) { + int id = idsVector.getInt(i); + if (id == 0) { + builder.appendNull(); + } else { + builder.appendBytesRef(regexes.getBytesRef(id + idsOffset, scratch)); + } + } + keys[0].close(); + keys[0] = builder.build(); + } + } else { + // For intermediate output, the keys are the delegate PackedValuesBlockHash's + // keys, with the category IDs replaced by the categorizer's internal state + // together with the list of category IDs. + BytesRef state; + // TODO: This BytesStreamOutput is not accounted for by the circuit breaker. Fix that! + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.writeBytesRef(categorizeBlockHash.serializeCategorizer()); + ((IntVector) keys[0].asVector()).writeTo(out); + state = out.bytes().toBytesRef(); + } catch (IOException e) { + throw new RuntimeException(e); + } + keys[0].close(); + keys[0] = blockFactory.newConstantBytesRefBlockWith(state, keys[0].getPositionCount()); + } + return keys; + } + + @Override + public IntVector nonEmpty() { + return packedValuesBlockHash.nonEmpty(); + } + + @Override + public BitArray seenGroupIds(BigArrays bigArrays) { + return packedValuesBlockHash.seenGroupIds(bigArrays); + } + + @Override + public final ReleasableIterator lookup(Page page, ByteSizeValue targetBlockSize) { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + Releasables.close(categorizeBlockHash, packedValuesBlockHash); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/HashAggregationOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/HashAggregationOperator.java index 6f8386ec08de1..ccddfdf5cc74a 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/HashAggregationOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/HashAggregationOperator.java @@ -51,7 +51,13 @@ public Operator get(DriverContext driverContext) { if (groups.stream().anyMatch(BlockHash.GroupSpec::isCategorize)) { return new HashAggregationOperator( aggregators, - () -> BlockHash.buildCategorizeBlockHash(groups, aggregatorMode, driverContext.blockFactory(), analysisRegistry), + () -> BlockHash.buildCategorizeBlockHash( + groups, + aggregatorMode, + driverContext.blockFactory(), + analysisRegistry, + maxPageSize + ), driverContext ); } diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/blockhash/CategorizeBlockHashTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/blockhash/CategorizeBlockHashTests.java index f8428b7c33568..587deda650a23 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/blockhash/CategorizeBlockHashTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/blockhash/CategorizeBlockHashTests.java @@ -130,9 +130,6 @@ public void close() { } finally { page.releaseBlocks(); } - - // TODO: randomize values? May give wrong results - // TODO: assert the categorizer state after adding pages. } public void testCategorizeRawMultivalue() { diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/blockhash/CategorizePackedValuesBlockHashTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/blockhash/CategorizePackedValuesBlockHashTests.java new file mode 100644 index 0000000000000..cfa023af3d18a --- /dev/null +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/aggregation/blockhash/CategorizePackedValuesBlockHashTests.java @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.blockhash; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.analysis.common.CommonAnalysisPlugin; +import org.elasticsearch.common.breaker.CircuitBreaker; +import org.elasticsearch.common.collect.Iterators; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.MockBigArrays; +import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.compute.aggregation.AggregatorMode; +import org.elasticsearch.compute.aggregation.ValuesBytesRefAggregatorFunctionSupplier; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BlockFactory; +import org.elasticsearch.compute.data.BlockUtils; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.CannedSourceOperator; +import org.elasticsearch.compute.operator.Driver; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.compute.operator.HashAggregationOperator; +import org.elasticsearch.compute.operator.LocalSourceOperator; +import org.elasticsearch.compute.operator.PageConsumerOperator; +import org.elasticsearch.core.Releasables; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.index.analysis.AnalysisRegistry; +import org.elasticsearch.indices.analysis.AnalysisModule; +import org.elasticsearch.plugins.scanners.StablePluginsRegistry; +import org.elasticsearch.xpack.ml.MachineLearning; +import org.junit.Before; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.elasticsearch.compute.operator.OperatorTestCase.runDriver; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; + +public class CategorizePackedValuesBlockHashTests extends BlockHashTestCase { + + private AnalysisRegistry analysisRegistry; + + @Before + private void initAnalysisRegistry() throws IOException { + analysisRegistry = new AnalysisModule( + TestEnvironment.newEnvironment( + Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()).build() + ), + List.of(new MachineLearning(Settings.EMPTY), new CommonAnalysisPlugin()), + new StablePluginsRegistry() + ).getAnalysisRegistry(); + } + + public void testCategorize_withDriver() { + BigArrays bigArrays = new MockBigArrays(PageCacheRecycler.NON_RECYCLING_INSTANCE, ByteSizeValue.ofMb(256)).withCircuitBreaking(); + CircuitBreaker breaker = bigArrays.breakerService().getBreaker(CircuitBreaker.REQUEST); + DriverContext driverContext = new DriverContext(bigArrays, new BlockFactory(breaker, bigArrays)); + boolean withNull = randomBoolean(); + boolean withMultivalues = randomBoolean(); + + List groupSpecs = List.of( + new BlockHash.GroupSpec(0, ElementType.BYTES_REF, true), + new BlockHash.GroupSpec(1, ElementType.INT, false) + ); + + LocalSourceOperator.BlockSupplier input1 = () -> { + try ( + BytesRefBlock.Builder messagesBuilder = driverContext.blockFactory().newBytesRefBlockBuilder(10); + IntBlock.Builder idsBuilder = driverContext.blockFactory().newIntBlockBuilder(10) + ) { + if (withMultivalues) { + messagesBuilder.beginPositionEntry(); + } + messagesBuilder.appendBytesRef(new BytesRef("connected to 1.1.1")); + messagesBuilder.appendBytesRef(new BytesRef("connected to 1.1.2")); + if (withMultivalues) { + messagesBuilder.endPositionEntry(); + } + idsBuilder.appendInt(7); + if (withMultivalues == false) { + idsBuilder.appendInt(7); + } + + messagesBuilder.appendBytesRef(new BytesRef("connected to 1.1.3")); + messagesBuilder.appendBytesRef(new BytesRef("connection error")); + messagesBuilder.appendBytesRef(new BytesRef("connection error")); + messagesBuilder.appendBytesRef(new BytesRef("connected to 1.1.4")); + idsBuilder.appendInt(42); + idsBuilder.appendInt(7); + idsBuilder.appendInt(42); + idsBuilder.appendInt(7); + + if (withNull) { + messagesBuilder.appendNull(); + idsBuilder.appendInt(43); + } + return new Block[] { messagesBuilder.build(), idsBuilder.build() }; + } + }; + LocalSourceOperator.BlockSupplier input2 = () -> { + try ( + BytesRefBlock.Builder messagesBuilder = driverContext.blockFactory().newBytesRefBlockBuilder(10); + IntBlock.Builder idsBuilder = driverContext.blockFactory().newIntBlockBuilder(10) + ) { + messagesBuilder.appendBytesRef(new BytesRef("connected to 2.1.1")); + messagesBuilder.appendBytesRef(new BytesRef("connected to 2.1.2")); + messagesBuilder.appendBytesRef(new BytesRef("disconnected")); + messagesBuilder.appendBytesRef(new BytesRef("connection error")); + idsBuilder.appendInt(111); + idsBuilder.appendInt(7); + idsBuilder.appendInt(7); + idsBuilder.appendInt(42); + if (withNull) { + messagesBuilder.appendNull(); + idsBuilder.appendNull(); + } + return new Block[] { messagesBuilder.build(), idsBuilder.build() }; + } + }; + + List intermediateOutput = new ArrayList<>(); + + Driver driver = new Driver( + driverContext, + new LocalSourceOperator(input1), + List.of( + new HashAggregationOperator.HashAggregationOperatorFactory( + groupSpecs, + AggregatorMode.INITIAL, + List.of(new ValuesBytesRefAggregatorFunctionSupplier(List.of(0)).groupingAggregatorFactory(AggregatorMode.INITIAL)), + 16 * 1024, + analysisRegistry + ).get(driverContext) + ), + new PageConsumerOperator(intermediateOutput::add), + () -> {} + ); + runDriver(driver); + + driver = new Driver( + driverContext, + new LocalSourceOperator(input2), + List.of( + new HashAggregationOperator.HashAggregationOperatorFactory( + groupSpecs, + AggregatorMode.INITIAL, + List.of(new ValuesBytesRefAggregatorFunctionSupplier(List.of(0)).groupingAggregatorFactory(AggregatorMode.INITIAL)), + 16 * 1024, + analysisRegistry + ).get(driverContext) + ), + new PageConsumerOperator(intermediateOutput::add), + () -> {} + ); + runDriver(driver); + + List finalOutput = new ArrayList<>(); + + driver = new Driver( + driverContext, + new CannedSourceOperator(intermediateOutput.iterator()), + List.of( + new HashAggregationOperator.HashAggregationOperatorFactory( + groupSpecs, + AggregatorMode.FINAL, + List.of(new ValuesBytesRefAggregatorFunctionSupplier(List.of(2)).groupingAggregatorFactory(AggregatorMode.FINAL)), + 16 * 1024, + analysisRegistry + ).get(driverContext) + ), + new PageConsumerOperator(finalOutput::add), + () -> {} + ); + runDriver(driver); + + assertThat(finalOutput, hasSize(1)); + assertThat(finalOutput.get(0).getBlockCount(), equalTo(3)); + BytesRefBlock outputMessages = finalOutput.get(0).getBlock(0); + IntBlock outputIds = finalOutput.get(0).getBlock(1); + BytesRefBlock outputValues = finalOutput.get(0).getBlock(2); + assertThat(outputIds.getPositionCount(), equalTo(outputMessages.getPositionCount())); + assertThat(outputValues.getPositionCount(), equalTo(outputMessages.getPositionCount())); + Map>> result = new HashMap<>(); + for (int i = 0; i < outputMessages.getPositionCount(); i++) { + BytesRef messageBytesRef = ((BytesRef) BlockUtils.toJavaObject(outputMessages, i)); + String message = messageBytesRef == null ? null : messageBytesRef.utf8ToString(); + result.computeIfAbsent(message, key -> new HashMap<>()); + + Integer id = (Integer) BlockUtils.toJavaObject(outputIds, i); + result.get(message).computeIfAbsent(id, key -> new HashSet<>()); + + Object values = BlockUtils.toJavaObject(outputValues, i); + if (values == null) { + result.get(message).get(id).add(null); + } else { + if ((values instanceof List) == false) { + values = List.of(values); + } + for (Object valueObject : (List) values) { + BytesRef value = (BytesRef) valueObject; + result.get(message).get(id).add(value.utf8ToString()); + } + } + } + Releasables.close(() -> Iterators.map(finalOutput.iterator(), (Page p) -> p::releaseBlocks)); + + Map>> expectedResult = Map.of( + ".*?connected.+?to.*?", + Map.of( + 7, + Set.of("connected to 1.1.1", "connected to 1.1.2", "connected to 1.1.4", "connected to 2.1.2"), + 42, + Set.of("connected to 1.1.3"), + 111, + Set.of("connected to 2.1.1") + ), + ".*?connection.+?error.*?", + Map.of(7, Set.of("connection error"), 42, Set.of("connection error")), + ".*?disconnected.*?", + Map.of(7, Set.of("disconnected")) + ); + if (withNull) { + expectedResult = new HashMap<>(expectedResult); + expectedResult.put(null, new HashMap<>()); + expectedResult.get(null).put(null, new HashSet<>()); + expectedResult.get(null).get(null).add(null); + expectedResult.get(null).put(43, new HashSet<>()); + expectedResult.get(null).get(43).add(null); + } + assertThat(result, equalTo(expectedResult)); + } +} diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/categorize.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/categorize.csv-spec index 4ce43961a7077..5ad62dd7a21a8 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/categorize.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/categorize.csv-spec @@ -60,6 +60,19 @@ COUNT():long | VALUES(str):keyword | category:keyword 1 | [a, b, c] | .*?disconnected.*? ; +limit before stats +required_capability: categorize_v5 + +FROM sample_data | SORT message | LIMIT 4 + | STATS count=COUNT() BY category=CATEGORIZE(message) + | SORT category +; + +count:long | category:keyword + 3 | .*?Connected.+?to.*? + 1 | .*?Connection.+?error.*? +; + skips stopwords required_capability: categorize_v5 @@ -615,3 +628,159 @@ COUNT():long | x:keyword 3 | [.*?Connection.+?error.*?,.*?Connection.+?error.*?] 1 | [.*?Disconnected.*?,.*?Disconnected.*?] ; + +multiple groupings with categorize and ip +required_capability: categorize_multiple_groupings + +FROM sample_data + | STATS count=COUNT() BY category=CATEGORIZE(message), client_ip + | SORT category, client_ip +; + +count:long | category:keyword | client_ip:ip + 1 | .*?Connected.+?to.*? | 172.21.2.113 + 1 | .*?Connected.+?to.*? | 172.21.2.162 + 1 | .*?Connected.+?to.*? | 172.21.3.15 + 3 | .*?Connection.+?error.*? | 172.21.3.15 + 1 | .*?Disconnected.*? | 172.21.0.5 +; + +multiple groupings with categorize and bucketed timestamp +required_capability: categorize_multiple_groupings + +FROM sample_data + | STATS count=COUNT() BY category=CATEGORIZE(message), timestamp=BUCKET(@timestamp, 1 HOUR) + | SORT category, timestamp +; + +count:long | category:keyword | timestamp:datetime + 2 | .*?Connected.+?to.*? | 2023-10-23T12:00:00.000Z + 1 | .*?Connected.+?to.*? | 2023-10-23T13:00:00.000Z + 3 | .*?Connection.+?error.*? | 2023-10-23T13:00:00.000Z + 1 | .*?Disconnected.*? | 2023-10-23T13:00:00.000Z +; + + +multiple groupings with categorize and limit before stats +required_capability: categorize_multiple_groupings + +FROM sample_data | SORT message | LIMIT 5 + | STATS count=COUNT() BY category=CATEGORIZE(message), client_ip + | SORT category, client_ip +; + +count:long | category:keyword | client_ip:ip + 1 | .*?Connected.+?to.*? | 172.21.2.113 + 1 | .*?Connected.+?to.*? | 172.21.2.162 + 1 | .*?Connected.+?to.*? | 172.21.3.15 + 2 | .*?Connection.+?error.*? | 172.21.3.15 +; + +multiple groupings with categorize and nulls +required_capability: categorize_multiple_groupings + +FROM employees + | STATS SUM(languages) BY category=CATEGORIZE(job_positions), gender + | SORT category DESC, gender ASC + | LIMIT 5 +; + +SUM(languages):long | category:keyword | gender:keyword + 11 | null | F + 16 | null | M + 14 | .*?Tech.+?Lead.*? | F + 23 | .*?Tech.+?Lead.*? | M + 9 | .*?Tech.+?Lead.*? | null +; + +multiple groupings with categorize and a field that's always null +required_capability: categorize_multiple_groupings + +FROM sample_data + | EVAL nullfield = null + | STATS count=COUNT() BY category=CATEGORIZE(nullfield), client_ip + | SORT client_ip +; + +count:long | category:keyword | client_ip:ip + 1 | null | 172.21.0.5 + 1 | null | 172.21.2.113 + 1 | null | 172.21.2.162 + 4 | null | 172.21.3.15 +; + +multiple groupings with categorize and the same text field +required_capability: categorize_multiple_groupings + +FROM sample_data + | STATS count=COUNT() BY category=CATEGORIZE(message), message + | SORT message +; + +count:long | category:keyword | message:keyword + 1 | .*?Connected.+?to.*? | Connected to 10.1.0.1 + 1 | .*?Connected.+?to.*? | Connected to 10.1.0.2 + 1 | .*?Connected.+?to.*? | Connected to 10.1.0.3 + 3 | .*?Connection.+?error.*? | Connection error + 1 | .*?Disconnected.*? | Disconnected +; + +multiple additional complex groupings with categorize +required_capability: categorize_multiple_groupings + +FROM sample_data + | STATS count=COUNT(), duration=SUM(event_duration) BY category=CATEGORIZE(message), SUBSTRING(message, 1, 7), ip_part=TO_LONG(SUBSTRING(TO_STRING(client_ip), 8, 1)), hour=BUCKET(@timestamp, 1 HOUR) + | SORT ip_part, category +; + +count:long | duration:long | category:keyword | SUBSTRING(message, 1, 7):keyword | ip_part:long | hour:datetime + 1 | 1232382 | .*?Disconnected.*? | Disconn | 0 | 2023-10-23T13:00:00.000Z + 2 | 6215122 | .*?Connected.+?to.*? | Connect | 2 | 2023-10-23T12:00:00.000Z + 1 | 1756467 | .*?Connected.+?to.*? | Connect | 3 | 2023-10-23T13:00:00.000Z + 3 | 14027356 | .*?Connection.+?error.*? | Connect | 3 | 2023-10-23T13:00:00.000Z +; + +multiple groupings with categorize and some constants including null +required_capability: categorize_multiple_groupings + +FROM sample_data + | STATS count=MV_COUNT(VALUES(message)) BY category=CATEGORIZE(message), null, constant="constant" + | SORT category +; + +count:integer | category:keyword | null:null | constant:keyword + 3 | .*?Connected.+?to.*? | null | constant + 1 | .*?Connection.+?error.*? | null | constant + 1 | .*?Disconnected.*? | null | constant +; + +multiple groupings with categorize and aggregation filters +required_capability: categorize_multiple_groupings + +FROM employees + | STATS lang_low=AVG(languages) WHERE salary<=50000, lang_high=AVG(languages) WHERE salary>50000 BY category=CATEGORIZE(job_positions), gender + | SORT category, gender + | LIMIT 5 +; + +lang_low:double | lang_high:double | category:keyword | gender:keyword + 2.0 | 5.0 | .*?Accountant.*? | F + 3.0 | 2.5 | .*?Accountant.*? | M + 5.0 | 2.0 | .*?Accountant.*? | null + 3.0 | 3.25 | .*?Architect.*? | F + 3.75 | null | .*?Architect.*? | M +; + +multiple groupings with categorize on null row +required_capability: categorize_multiple_groupings + +ROW message = null, str = ["a", "b", "c"] + | STATS COUNT(), VALUES(str) BY category=CATEGORIZE(message), str + | SORT str +; + +COUNT():long | VALUES(str):keyword | category:keyword | str:keyword + 1 | [a, b, c] | null | a + 1 | [a, b, c] | null | b + 1 | [a, b, c] | null | c +; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 1aee1df3dbafb..6436e049c7dd8 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -419,6 +419,10 @@ public enum Cap { */ CATEGORIZE_V5, + /** + * Support for multiple groupings in "CATEGORIZE". + */ + CATEGORIZE_MULTIPLE_GROUPINGS, /** * QSTR function */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java index c805adf5d5a57..7a733d73941e4 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java @@ -316,11 +316,15 @@ private static void checkAggregate(LogicalPlan p, Set failures) { private static void checkCategorizeGrouping(Aggregate agg, Set failures) { // Forbid CATEGORIZE grouping function with other groupings if (agg.groupings().size() > 1) { - agg.groupings().forEach(g -> { + agg.groupings().subList(1, agg.groupings().size()).forEach(g -> { g.forEachDown( Categorize.class, categorize -> failures.add( - fail(categorize, "cannot use CATEGORIZE grouping function [{}] with multiple groupings", categorize.sourceText()) + fail( + categorize, + "CATEGORIZE grouping function [{}] can only be in the first grouping expression", + categorize.sourceText() + ) ) ); }); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Categorize.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Categorize.java index ded913a78bdf1..a100dd64915f1 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Categorize.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Categorize.java @@ -95,7 +95,8 @@ public boolean foldable() { @Override public Nullability nullable() { - // Both nulls and empty strings result in null values + // Null strings and strings that don't produce tokens after analysis lead to null values. + // This includes empty strings, only whitespace, (hexa)decimal numbers and stopwords. return Nullability.TRUE; } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index 84dcdbadef9f0..e20f0d8bbc8ff 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -1869,38 +1869,35 @@ public void testIntervalAsString() { ); } - public void testCategorizeSingleGrouping() { - assumeTrue("requires Categorize capability", EsqlCapabilities.Cap.CATEGORIZE_V5.isEnabled()); - - query("from test | STATS COUNT(*) BY CATEGORIZE(first_name)"); - query("from test | STATS COUNT(*) BY cat = CATEGORIZE(first_name)"); + public void testCategorizeOnlyFirstGrouping() { + query("FROM test | STATS COUNT(*) BY CATEGORIZE(first_name)"); + query("FROM test | STATS COUNT(*) BY cat = CATEGORIZE(first_name)"); + query("FROM test | STATS COUNT(*) BY CATEGORIZE(first_name), emp_no"); + query("FROM test | STATS COUNT(*) BY a = CATEGORIZE(first_name), b = emp_no"); assertEquals( - "1:31: cannot use CATEGORIZE grouping function [CATEGORIZE(first_name)] with multiple groupings", - error("from test | STATS COUNT(*) BY CATEGORIZE(first_name), emp_no") + "1:39: CATEGORIZE grouping function [CATEGORIZE(first_name)] can only be in the first grouping expression", + error("FROM test | STATS COUNT(*) BY emp_no, CATEGORIZE(first_name)") ); assertEquals( - "1:39: cannot use CATEGORIZE grouping function [CATEGORIZE(first_name)] with multiple groupings", - error("FROM test | STATS COUNT(*) BY emp_no, CATEGORIZE(first_name)") + "1:55: CATEGORIZE grouping function [CATEGORIZE(last_name)] can only be in the first grouping expression", + error("FROM test | STATS COUNT(*) BY CATEGORIZE(first_name), CATEGORIZE(last_name)") ); assertEquals( - "1:35: cannot use CATEGORIZE grouping function [CATEGORIZE(first_name)] with multiple groupings", - error("FROM test | STATS COUNT(*) BY a = CATEGORIZE(first_name), b = emp_no") + "1:55: CATEGORIZE grouping function [CATEGORIZE(first_name)] can only be in the first grouping expression", + error("FROM test | STATS COUNT(*) BY CATEGORIZE(first_name), CATEGORIZE(first_name)") ); assertEquals( - "1:31: cannot use CATEGORIZE grouping function [CATEGORIZE(first_name)] with multiple groupings\n" - + "line 1:55: cannot use CATEGORIZE grouping function [CATEGORIZE(last_name)] with multiple groupings", - error("FROM test | STATS COUNT(*) BY CATEGORIZE(first_name), CATEGORIZE(last_name)") + "1:63: CATEGORIZE grouping function [CATEGORIZE(last_name)] can only be in the first grouping expression", + error("FROM test | STATS COUNT(*) BY CATEGORIZE(first_name), emp_no, CATEGORIZE(last_name)") ); assertEquals( - "1:31: cannot use CATEGORIZE grouping function [CATEGORIZE(first_name)] with multiple groupings", - error("FROM test | STATS COUNT(*) BY CATEGORIZE(first_name), CATEGORIZE(first_name)") + "1:63: CATEGORIZE grouping function [CATEGORIZE(first_name)] can only be in the first grouping expression", + error("FROM test | STATS COUNT(*) BY CATEGORIZE(first_name), emp_no, CATEGORIZE(first_name)") ); } public void testCategorizeNestedGrouping() { - assumeTrue("requires Categorize capability", EsqlCapabilities.Cap.CATEGORIZE_V5.isEnabled()); - query("from test | STATS COUNT(*) BY CATEGORIZE(LENGTH(first_name)::string)"); assertEquals( @@ -1914,8 +1911,6 @@ public void testCategorizeNestedGrouping() { } public void testCategorizeWithinAggregations() { - assumeTrue("requires Categorize capability", EsqlCapabilities.Cap.CATEGORIZE_V5.isEnabled()); - query("from test | STATS MV_COUNT(cat), COUNT(*) BY cat = CATEGORIZE(first_name)"); query("from test | STATS MV_COUNT(CATEGORIZE(first_name)), COUNT(*) BY cat = CATEGORIZE(first_name)"); query("from test | STATS MV_COUNT(CATEGORIZE(first_name)), COUNT(*) BY CATEGORIZE(first_name)"); @@ -1944,8 +1939,6 @@ public void testCategorizeWithinAggregations() { } public void testCategorizeWithFilteredAggregations() { - assumeTrue("requires Categorize capability", EsqlCapabilities.Cap.CATEGORIZE_V5.isEnabled()); - query("FROM test | STATS COUNT(*) WHERE first_name == \"John\" BY CATEGORIZE(last_name)"); query("FROM test | STATS COUNT(*) WHERE last_name == \"Doe\" BY CATEGORIZE(last_name)"); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java index c2a26845d4e88..87bc11d8388bc 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java @@ -20,7 +20,6 @@ import org.elasticsearch.xpack.esql.EsqlTestUtils; import org.elasticsearch.xpack.esql.TestBlockFactory; import org.elasticsearch.xpack.esql.VerificationException; -import org.elasticsearch.xpack.esql.action.EsqlCapabilities; import org.elasticsearch.xpack.esql.analysis.Analyzer; import org.elasticsearch.xpack.esql.analysis.AnalyzerContext; import org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils; @@ -1212,8 +1211,6 @@ public void testCombineProjectionWithAggregationFirstAndAliasedGroupingUsedInAgg * \_EsRelation[test][_meta_field{f}#23, emp_no{f}#17, first_name{f}#18, ..] */ public void testCombineProjectionWithCategorizeGrouping() { - assumeTrue("requires Categorize capability", EsqlCapabilities.Cap.CATEGORIZE_V5.isEnabled()); - var plan = plan(""" from test | eval k = first_name, k1 = k @@ -3949,8 +3946,6 @@ public void testNestedExpressionsInGroups() { * \_EsRelation[test][_meta_field{f}#14, emp_no{f}#8, first_name{f}#9, ge..] */ public void testNestedExpressionsInGroupsWithCategorize() { - assumeTrue("requires Categorize capability", EsqlCapabilities.Cap.CATEGORIZE_V5.isEnabled()); - var plan = optimizedPlan(""" from test | stats c = count(salary) by CATEGORIZE(CONCAT(first_name, "abc")) From eac473151223362a52d7985efce5fd0d18bee10a Mon Sep 17 00:00:00 2001 From: Mary Gouseti Date: Thu, 12 Dec 2024 17:51:52 +0200 Subject: [PATCH 57/77] [9.0] Clean up leftover reference of `include_type_name` (#118535) The query param `include_type_name` has been removed in `8.x` and has been only accepted in rest compatibility mode (see [removal of types](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/removal-of-types.html#_schedule_for_removal_of_mapping_types)). In `9.x` this is not necessary anymore, https://github.com/elastic/elasticsearch/pull/114850 has handled the majority of the removals. We apply some extra cleaning in this PR. --- .../test/indices.put_mapping/10_basic.yml | 26 ------------- .../admin/indices/RestCreateIndexAction.java | 5 +-- .../indices/RestGetFieldMappingAction.java | 10 ----- .../admin/indices/RestGetIndicesAction.java | 4 -- .../admin/indices/RestGetMappingAction.java | 6 --- .../indices/RestRolloverIndexAction.java | 5 --- .../get/GetFieldMappingsResponseTests.java | 38 ------------------- 7 files changed, 1 insertion(+), 93 deletions(-) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.put_mapping/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.put_mapping/10_basic.yml index 335e0b4783bf0..8e8afafc9f069 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.put_mapping/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.put_mapping/10_basic.yml @@ -84,32 +84,6 @@ - match: { error.type: "illegal_argument_exception" } - match: { error.reason: "Types cannot be provided in put mapping requests" } ---- -"Put mappings with explicit _doc type bwc": - - skip: - cluster_features: [ "gte_v8.0.0"] - reason: "old deprecation message for pre 8.0" - - requires: - test_runner_features: ["node_selector"] - - do: - indices.create: - index: test_index - - - do: - node_selector: - version: "original" - catch: bad_request - indices.put_mapping: - index: test_index - body: - _doc: - properties: - field: - type: keyword - - - match: { error.type: "illegal_argument_exception" } - - match: { error.reason: "Types cannot be provided in put mapping requests, unless the include_type_name parameter is set to true." } - --- "Update per-field metadata": diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestCreateIndexAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestCreateIndexAction.java index e30d2f8d5c733..ba6fed3ea35d4 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestCreateIndexAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestCreateIndexAction.java @@ -78,10 +78,7 @@ static Map prepareMappings(Map source) { Map mappings = (Map) source.get("mappings"); if (MapperService.isMappingSourceTyped(MapperService.SINGLE_MAPPING_NAME, mappings)) { throw new IllegalArgumentException( - "The mapping definition cannot be nested under a type " - + "[" - + MapperService.SINGLE_MAPPING_NAME - + "] unless include_type_name is set to true." + "The mapping definition cannot be nested under a type [" + MapperService.SINGLE_MAPPING_NAME + "]." ); } diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetFieldMappingAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetFieldMappingAction.java index 5f648ca8e77e5..0c7772bc3a69c 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetFieldMappingAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetFieldMappingAction.java @@ -9,15 +9,12 @@ package org.elasticsearch.rest.action.admin.indices; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsRequest; import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsResponse; import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsResponse.FieldMappingMetadata; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; @@ -35,13 +32,6 @@ public class RestGetFieldMappingAction extends BaseRestHandler { - private static final Logger logger = LogManager.getLogger(RestGetFieldMappingAction.class); - private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(logger.getName()); - public static final String INCLUDE_TYPE_DEPRECATION_MESSAGE = "[types removal] Using include_type_name in get " - + "field mapping requests is deprecated. The parameter will be removed in the next major version."; - public static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Specifying types in get field mapping request is deprecated. " - + "Use typeless api instead"; - @Override public List routes() { return List.of(new Route(GET, "/_mapping/field/{fields}"), new Route(GET, "/{index}/_mapping/field/{fields}")); diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java index 9ca890eaff65b..9be3462e97e0c 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetIndicesAction.java @@ -13,7 +13,6 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; @@ -35,9 +34,6 @@ */ @ServerlessScope(Scope.PUBLIC) public class RestGetIndicesAction extends BaseRestHandler { - private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(RestGetIndicesAction.class); - public static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Using `include_type_name` in get indices requests" - + " is deprecated. The parameter will be removed in the next major version."; @Override public List routes() { diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetMappingAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetMappingAction.java index 242bcd399413b..27620fa750ea9 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetMappingAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestGetMappingAction.java @@ -13,7 +13,6 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.core.TimeValue; import org.elasticsearch.http.HttpChannel; import org.elasticsearch.rest.BaseRestHandler; @@ -31,11 +30,6 @@ @ServerlessScope(Scope.PUBLIC) public class RestGetMappingAction extends BaseRestHandler { - private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(RestGetMappingAction.class); - public static final String INCLUDE_TYPE_DEPRECATION_MSG = "[types removal] Using include_type_name in get" - + " mapping requests is deprecated. The parameter will be removed in the next major version."; - public static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Specifying types in get mapping request is deprecated. " - + "Use typeless api instead"; public RestGetMappingAction() {} diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestRolloverIndexAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestRolloverIndexAction.java index 776302296b1a2..39d7b1d5851db 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestRolloverIndexAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestRolloverIndexAction.java @@ -14,7 +14,6 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.cluster.metadata.DataStream; -import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.Scope; @@ -33,10 +32,6 @@ @ServerlessScope(Scope.PUBLIC) public class RestRolloverIndexAction extends BaseRestHandler { - private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(RestRolloverIndexAction.class); - public static final String TYPES_DEPRECATION_MESSAGE = "[types removal] Using include_type_name in rollover " - + "index requests is deprecated. The parameter will be removed in the next major version."; - @Override public List routes() { return List.of(new Route(POST, "/{index}/_rollover"), new Route(POST, "/{index}/_rollover/{new_index}")); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetFieldMappingsResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetFieldMappingsResponseTests.java index 7640c1e7af308..550f8e5843628 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetFieldMappingsResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/mapping/get/GetFieldMappingsResponseTests.java @@ -12,24 +12,16 @@ import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsResponse.FieldMappingMetadata; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.core.RestApiVersion; import org.elasticsearch.test.AbstractWireSerializingTestCase; -import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xcontent.json.JsonXContent; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import static org.hamcrest.Matchers.hasKey; - public class GetFieldMappingsResponseTests extends AbstractWireSerializingTestCase { public void testManualSerialization() throws IOException { @@ -56,36 +48,6 @@ public void testNullFieldMappingToXContent() { assertEquals("{\"index\":{\"mappings\":{}}}", Strings.toString(response)); } - public void testToXContentIncludesType() throws Exception { - Map> mappings = new HashMap<>(); - FieldMappingMetadata fieldMappingMetadata = new FieldMappingMetadata("my field", new BytesArray("{}")); - mappings.put("index", Collections.singletonMap("field", fieldMappingMetadata)); - GetFieldMappingsResponse response = new GetFieldMappingsResponse(mappings); - ToXContent.Params params = new ToXContent.MapParams(Collections.singletonMap("include_type_name", "true")); - - // v8 does not have _doc, even when include_type_name is present - // (although this throws unconsumed parameter exception in RestGetFieldMappingsAction) - try (XContentBuilder builder = XContentBuilder.builder(JsonXContent.jsonXContent, RestApiVersion.V_8)) { - response.toXContent(builder, params); - - try (XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(builder))) { - @SuppressWarnings("unchecked") - Map> index = (Map>) parser.map().get("index"); - assertThat(index.get("mappings"), hasKey("field")); - } - } - - try (XContentBuilder builder = XContentBuilder.builder(JsonXContent.jsonXContent, RestApiVersion.V_8)) { - response.toXContent(builder, ToXContent.EMPTY_PARAMS); - - try (XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(builder))) { - @SuppressWarnings("unchecked") - Map> index = (Map>) parser.map().get("index"); - assertThat(index.get("mappings"), hasKey("field")); - } - } - } - @Override protected GetFieldMappingsResponse createTestInstance() { return new GetFieldMappingsResponse(randomMapping()); From c9a6a2c8417c677509549c9f38ee88a02f85585f Mon Sep 17 00:00:00 2001 From: Kathleen DeRusso Date: Thu, 12 Dec 2024 10:55:00 -0500 Subject: [PATCH 58/77] Add match support for semantic_text fields (#117839) * Added query name to inference field metadata * Fix build error * Added query builder service * Add query builder service to query rewrite context * Updated match query to support querying semantic text fields * Fix build error * Fix NPE * Update the POC to rewrite to a bool query when combined inference and non-inference fields * Separate clause for each inference index (to avoid inference ID clashes) * Simplify query builder service concept to a single default inference query * Rename QueryBuilderService, remove query name from inference metadata * Fix too many rewrite rounds error by injecting booleans in constructors for match query builder and semantic text * Fix test compilation errors * Fix tests * Add yaml test for semantic match * Add NodeFeature * Fix license headers * Spotless * Updated getClass comparison in MatchQueryBuilder * Cleanup * Add Mock Inference Query Builder Service * Spotless * Cleanup * Update docs/changelog/117839.yaml * Update changelog * Replace the default inference query builder with a query rewrite interceptor * Cleanup * Some more cleanup/renames * Some more cleanup/renames * Spotless * Checkstyle * Convert List to Map keyed on query name, error on query name collisions * PR feedback - remove check on QueryRewriteContext class only * PR feedback * Remove intercept flag from MatchQueryBuilder and replace with wrapper * Move feature to test feature * Ensure interception happens only once * Rename InterceptedQueryBuilderWrapper to AbstractQueryBuilderWrapper * Add lenient field to SemanticQueryBuilder * Clean up yaml test * Add TODO comment * Add comment * Spotless * Rename AbstractQueryBuilderWrapper back to InterceptedQueryBuilderWrapper * Spotless * Didn't mean to commit that * Remove static class wrapping the InterceptedQueryBuilderWrapper * Make InterceptedQueryBuilderWrapper part of QueryRewriteInterceptor * Refactor the interceptor to be an internal plugin that cannot be used outside inference plugin * Fix tests * Spotless * Minor cleanup * C'mon spotless * Test spotless * Cleanup InternalQueryRewriter * Change if statement to assert * Simplify template of InterceptedQueryBuilderWrapper * Change constructor of InterceptedQueryBuilderWrapper * Refactor InterceptedQueryBuilderWrapper to extend QueryBuilder * Cleanup * Add test * Spotless * Rename rewrite to interceptAndRewrite in QueryRewriteInterceptor * DOESN'T WORK - for testing * Add comment * Getting closer - match on single typed fields works now * Deleted line by mistake * Checkstyle * Fix over-aggressive IntelliJ Refactor/Rename * And another one * Move SemanticMatchQueryRewriteInterceptor.SEMANTIC_MATCH_QUERY_REWRITE_INTERCEPTION_SUPPORTED to Test feature * PR feedback * Require query name with no default * PR feedback & update test * Add rewrite test * Update server/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java Co-authored-by: Mike Pellegrini --------- Co-authored-by: Mike Pellegrini --- docs/changelog/117839.yaml | 5 + server/src/main/java/module-info.java | 2 +- .../org/elasticsearch/TransportVersions.java | 1 + .../org/elasticsearch/index/IndexModule.java | 7 +- .../org/elasticsearch/index/IndexService.java | 7 +- .../index/query/AbstractQueryBuilder.java | 9 + .../query/CoordinatorRewriteContext.java | 1 + .../index/query/InnerHitContextBuilder.java | 3 + .../query/InterceptedQueryBuilderWrapper.java | 109 +++++++ .../index/query/QueryRewriteContext.java | 22 +- .../index/query/SearchExecutionContext.java | 1 + .../elasticsearch/indices/IndicesService.java | 8 +- .../indices/IndicesServiceBuilder.java | 24 ++ .../elasticsearch/plugins/SearchPlugin.java | 9 + .../rewriter/QueryRewriteInterceptor.java | 75 +++++ .../search/TransportSearchActionTests.java | 4 +- .../cluster/metadata/IndexMetadataTests.java | 7 +- .../elasticsearch/index/IndexModuleTests.java | 4 +- ...appingLookupInferenceFieldMapperTests.java | 6 +- .../InterceptedQueryBuilderWrapperTests.java | 92 ++++++ .../index/query/QueryRewriteContextTests.java | 2 + .../rewriter/MockQueryRewriteInterceptor.java | 26 ++ .../test/AbstractBuilderTestCase.java | 9 +- .../xpack/inference/InferenceFeatures.java | 4 +- .../xpack/inference/InferencePlugin.java | 7 + .../SemanticMatchQueryRewriteInterceptor.java | 95 ++++++ .../queries/SemanticQueryBuilder.java | 25 +- .../ShardBulkInferenceActionFilterTests.java | 2 +- .../test/inference/40_semantic_text_query.yml | 64 +++- .../test/inference/45_semantic_text_match.yml | 284 ++++++++++++++++++ .../rank/rrf/RRFRetrieverBuilderTests.java | 8 +- 31 files changed, 890 insertions(+), 32 deletions(-) create mode 100644 docs/changelog/117839.yaml create mode 100644 server/src/main/java/org/elasticsearch/index/query/InterceptedQueryBuilderWrapper.java create mode 100644 server/src/main/java/org/elasticsearch/plugins/internal/rewriter/QueryRewriteInterceptor.java create mode 100644 server/src/test/java/org/elasticsearch/index/query/InterceptedQueryBuilderWrapperTests.java create mode 100644 test/framework/src/main/java/org/elasticsearch/plugins/internal/rewriter/MockQueryRewriteInterceptor.java create mode 100644 x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticMatchQueryRewriteInterceptor.java create mode 100644 x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/45_semantic_text_match.yml diff --git a/docs/changelog/117839.yaml b/docs/changelog/117839.yaml new file mode 100644 index 0000000000000..98c97b5078c02 --- /dev/null +++ b/docs/changelog/117839.yaml @@ -0,0 +1,5 @@ +pr: 117839 +summary: Add match support for `semantic_text` fields +area: "Search" +type: enhancement +issues: [] diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index ff902dbede007..51896fb80a62c 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -479,5 +479,5 @@ exports org.elasticsearch.lucene.spatial; exports org.elasticsearch.inference.configuration; exports org.elasticsearch.monitor.metrics; - + exports org.elasticsearch.plugins.internal.rewriter to org.elasticsearch.inference; } diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index ac083862357d6..4135b1f0b8e9a 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -137,6 +137,7 @@ static TransportVersion def(int id) { public static final TransportVersion RETRIES_AND_OPERATIONS_IN_BLOBSTORE_STATS = def(8_804_00_0); public static final TransportVersion ADD_DATA_STREAM_OPTIONS_TO_TEMPLATES = def(8_805_00_0); public static final TransportVersion KNN_QUERY_RESCORE_OVERSAMPLE = def(8_806_00_0); + public static final TransportVersion SEMANTIC_QUERY_LENIENT = def(8_807_00_0); /* * STOP! READ THIS FIRST! No, really, diff --git a/server/src/main/java/org/elasticsearch/index/IndexModule.java b/server/src/main/java/org/elasticsearch/index/IndexModule.java index 64182b000827d..2168ad1df5d2f 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexModule.java +++ b/server/src/main/java/org/elasticsearch/index/IndexModule.java @@ -58,6 +58,7 @@ import org.elasticsearch.indices.fielddata.cache.IndicesFieldDataCache; import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.plugins.IndexStorePlugin; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.aggregations.support.ValuesSourceRegistry; import org.elasticsearch.threadpool.ThreadPool; @@ -478,7 +479,8 @@ public IndexService newIndexService( IdFieldMapper idFieldMapper, ValuesSourceRegistry valuesSourceRegistry, IndexStorePlugin.IndexFoldersDeletionListener indexFoldersDeletionListener, - Map snapshotCommitSuppliers + Map snapshotCommitSuppliers, + QueryRewriteInterceptor queryRewriteInterceptor ) throws IOException { final IndexEventListener eventListener = freeze(); Function> readerWrapperFactory = indexReaderWrapper @@ -540,7 +542,8 @@ public IndexService newIndexService( indexFoldersDeletionListener, snapshotCommitSupplier, indexCommitListener.get(), - mapperMetrics + mapperMetrics, + queryRewriteInterceptor ); success = true; return indexService; diff --git a/server/src/main/java/org/elasticsearch/index/IndexService.java b/server/src/main/java/org/elasticsearch/index/IndexService.java index 571bbd76a49dd..a5b3991d89bc4 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexService.java +++ b/server/src/main/java/org/elasticsearch/index/IndexService.java @@ -85,6 +85,7 @@ import org.elasticsearch.indices.fielddata.cache.IndicesFieldDataCache; import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.plugins.IndexStorePlugin; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.aggregations.support.ValuesSourceRegistry; import org.elasticsearch.threadpool.ThreadPool; @@ -162,6 +163,7 @@ public class IndexService extends AbstractIndexComponent implements IndicesClust private final Supplier indexSortSupplier; private final ValuesSourceRegistry valuesSourceRegistry; private final MapperMetrics mapperMetrics; + private final QueryRewriteInterceptor queryRewriteInterceptor; @SuppressWarnings("this-escape") public IndexService( @@ -196,7 +198,8 @@ public IndexService( IndexStorePlugin.IndexFoldersDeletionListener indexFoldersDeletionListener, IndexStorePlugin.SnapshotCommitSupplier snapshotCommitSupplier, Engine.IndexCommitListener indexCommitListener, - MapperMetrics mapperMetrics + MapperMetrics mapperMetrics, + QueryRewriteInterceptor queryRewriteInterceptor ) { super(indexSettings); assert indexCreationContext != IndexCreationContext.RELOAD_ANALYZERS @@ -271,6 +274,7 @@ public IndexService( this.indexingOperationListeners = Collections.unmodifiableList(indexingOperationListeners); this.indexCommitListener = indexCommitListener; this.mapperMetrics = mapperMetrics; + this.queryRewriteInterceptor = queryRewriteInterceptor; try (var ignored = threadPool.getThreadContext().clearTraceContext()) { // kick off async ops for the first shard in this index this.refreshTask = new AsyncRefreshTask(this); @@ -802,6 +806,7 @@ public QueryRewriteContext newQueryRewriteContext( allowExpensiveQueries, scriptService, null, + null, null ); } diff --git a/server/src/main/java/org/elasticsearch/index/query/AbstractQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/AbstractQueryBuilder.java index f00e6904feac7..05262798bac2a 100644 --- a/server/src/main/java/org/elasticsearch/index/query/AbstractQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/AbstractQueryBuilder.java @@ -21,6 +21,7 @@ import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.SuggestingErrorOnUnknown; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; import org.elasticsearch.xcontent.AbstractObjectParser; import org.elasticsearch.xcontent.FilterXContentParser; import org.elasticsearch.xcontent.FilterXContentParserWrapper; @@ -278,6 +279,14 @@ protected static List readQueries(StreamInput in) throws IOExcepti @Override public final QueryBuilder rewrite(QueryRewriteContext queryRewriteContext) throws IOException { + QueryRewriteInterceptor queryRewriteInterceptor = queryRewriteContext.getQueryRewriteInterceptor(); + if (queryRewriteInterceptor != null) { + var rewritten = queryRewriteInterceptor.interceptAndRewrite(queryRewriteContext, this); + if (rewritten != this) { + return new InterceptedQueryBuilderWrapper(rewritten); + } + } + QueryBuilder rewritten = doRewrite(queryRewriteContext); if (rewritten == this) { return rewritten; diff --git a/server/src/main/java/org/elasticsearch/index/query/CoordinatorRewriteContext.java b/server/src/main/java/org/elasticsearch/index/query/CoordinatorRewriteContext.java index e054f17ef64d6..a84455ef09bf2 100644 --- a/server/src/main/java/org/elasticsearch/index/query/CoordinatorRewriteContext.java +++ b/server/src/main/java/org/elasticsearch/index/query/CoordinatorRewriteContext.java @@ -104,6 +104,7 @@ public CoordinatorRewriteContext( null, null, null, + null, null ); this.dateFieldRangeInfo = dateFieldRangeInfo; diff --git a/server/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java b/server/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java index aacb4b4129c73..31bc7dddacb7f 100644 --- a/server/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java @@ -66,6 +66,9 @@ public InnerHitBuilder innerHitBuilder() { public static void extractInnerHits(QueryBuilder query, Map innerHitBuilders) { if (query instanceof AbstractQueryBuilder) { ((AbstractQueryBuilder) query).extractInnerHitBuilders(innerHitBuilders); + } else if (query instanceof InterceptedQueryBuilderWrapper interceptedQuery) { + // Unwrap an intercepted query here + extractInnerHits(interceptedQuery.queryBuilder, innerHitBuilders); } else { throw new IllegalStateException( "provided query builder [" + query.getClass() + "] class should inherit from AbstractQueryBuilder, but it doesn't" diff --git a/server/src/main/java/org/elasticsearch/index/query/InterceptedQueryBuilderWrapper.java b/server/src/main/java/org/elasticsearch/index/query/InterceptedQueryBuilderWrapper.java new file mode 100644 index 0000000000000..b1030e4a76d97 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/index/query/InterceptedQueryBuilderWrapper.java @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.query; + +import org.apache.lucene.search.Query; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +/** + * Wrapper for instances of {@link QueryBuilder} that have been intercepted using the {@link QueryRewriteInterceptor} to + * break out of the rewrite phase. These instances are unwrapped on serialization. + */ +class InterceptedQueryBuilderWrapper implements QueryBuilder { + + protected final QueryBuilder queryBuilder; + + InterceptedQueryBuilderWrapper(QueryBuilder queryBuilder) { + super(); + this.queryBuilder = queryBuilder; + } + + @Override + public QueryBuilder rewrite(QueryRewriteContext queryRewriteContext) throws IOException { + QueryRewriteInterceptor queryRewriteInterceptor = queryRewriteContext.getQueryRewriteInterceptor(); + try { + queryRewriteContext.setQueryRewriteInterceptor(null); + QueryBuilder rewritten = queryBuilder.rewrite(queryRewriteContext); + return rewritten != queryBuilder ? new InterceptedQueryBuilderWrapper(rewritten) : this; + } finally { + queryRewriteContext.setQueryRewriteInterceptor(queryRewriteInterceptor); + } + } + + @Override + public String getWriteableName() { + return queryBuilder.getWriteableName(); + } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return queryBuilder.getMinimalSupportedVersion(); + } + + @Override + public Query toQuery(SearchExecutionContext context) throws IOException { + return queryBuilder.toQuery(context); + } + + @Override + public QueryBuilder queryName(String queryName) { + queryBuilder.queryName(queryName); + return this; + } + + @Override + public String queryName() { + return queryBuilder.queryName(); + } + + @Override + public float boost() { + return queryBuilder.boost(); + } + + @Override + public QueryBuilder boost(float boost) { + queryBuilder.boost(boost); + return this; + } + + @Override + public String getName() { + return queryBuilder.getName(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + queryBuilder.writeTo(out); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return queryBuilder.toXContent(builder, params); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o instanceof InterceptedQueryBuilderWrapper == false) return false; + return Objects.equals(queryBuilder, ((InterceptedQueryBuilderWrapper) o).queryBuilder); + } + + @Override + public int hashCode() { + return Objects.hashCode(queryBuilder); + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/QueryRewriteContext.java b/server/src/main/java/org/elasticsearch/index/query/QueryRewriteContext.java index fce74aa60ab16..265a0c52593bd 100644 --- a/server/src/main/java/org/elasticsearch/index/query/QueryRewriteContext.java +++ b/server/src/main/java/org/elasticsearch/index/query/QueryRewriteContext.java @@ -28,6 +28,7 @@ import org.elasticsearch.index.mapper.MappingLookup; import org.elasticsearch.index.mapper.SourceFieldMapper; import org.elasticsearch.index.mapper.TextFieldMapper; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; import org.elasticsearch.script.ScriptCompiler; import org.elasticsearch.search.aggregations.support.ValuesSourceRegistry; import org.elasticsearch.search.builder.PointInTimeBuilder; @@ -70,6 +71,7 @@ public class QueryRewriteContext { protected Predicate allowedFields; private final ResolvedIndices resolvedIndices; private final PointInTimeBuilder pit; + private QueryRewriteInterceptor queryRewriteInterceptor; public QueryRewriteContext( final XContentParserConfiguration parserConfiguration, @@ -86,7 +88,8 @@ public QueryRewriteContext( final BooleanSupplier allowExpensiveQueries, final ScriptCompiler scriptService, final ResolvedIndices resolvedIndices, - final PointInTimeBuilder pit + final PointInTimeBuilder pit, + final QueryRewriteInterceptor queryRewriteInterceptor ) { this.parserConfiguration = parserConfiguration; @@ -105,6 +108,7 @@ public QueryRewriteContext( this.scriptService = scriptService; this.resolvedIndices = resolvedIndices; this.pit = pit; + this.queryRewriteInterceptor = queryRewriteInterceptor; } public QueryRewriteContext(final XContentParserConfiguration parserConfiguration, final Client client, final LongSupplier nowInMillis) { @@ -123,6 +127,7 @@ public QueryRewriteContext(final XContentParserConfiguration parserConfiguration null, null, null, + null, null ); } @@ -132,7 +137,8 @@ public QueryRewriteContext( final Client client, final LongSupplier nowInMillis, final ResolvedIndices resolvedIndices, - final PointInTimeBuilder pit + final PointInTimeBuilder pit, + final QueryRewriteInterceptor queryRewriteInterceptor ) { this( parserConfiguration, @@ -149,7 +155,8 @@ public QueryRewriteContext( null, null, resolvedIndices, - pit + pit, + queryRewriteInterceptor ); } @@ -428,4 +435,13 @@ public String getTierPreference() { // It was decided we should only test the first of these potentially multiple preferences. return value.split(",")[0].trim(); } + + public QueryRewriteInterceptor getQueryRewriteInterceptor() { + return queryRewriteInterceptor; + } + + public void setQueryRewriteInterceptor(QueryRewriteInterceptor queryRewriteInterceptor) { + this.queryRewriteInterceptor = queryRewriteInterceptor; + } + } diff --git a/server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java b/server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java index fbc3696d40221..b2ee6134a7728 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java +++ b/server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java @@ -271,6 +271,7 @@ private SearchExecutionContext( allowExpensiveQueries, scriptService, null, + null, null ); this.shardId = shardId; diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesService.java b/server/src/main/java/org/elasticsearch/indices/IndicesService.java index 818bf2036e3b1..a5765a1a707d2 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesService.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesService.java @@ -135,6 +135,7 @@ import org.elasticsearch.plugins.FieldPredicate; import org.elasticsearch.plugins.IndexStorePlugin; import org.elasticsearch.plugins.PluginsService; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.aggregations.support.ValuesSourceRegistry; @@ -262,6 +263,7 @@ public class IndicesService extends AbstractLifecycleComponent private final MapperMetrics mapperMetrics; private final PostRecoveryMerger postRecoveryMerger; private final List searchOperationListeners; + private final QueryRewriteInterceptor queryRewriteInterceptor; @Override protected void doStart() { @@ -330,6 +332,7 @@ public void onRemoval(ShardId shardId, String fieldName, boolean wasEvicted, lon this.indexFoldersDeletionListeners = new CompositeIndexFoldersDeletionListener(builder.indexFoldersDeletionListeners); this.snapshotCommitSuppliers = builder.snapshotCommitSuppliers; this.requestCacheKeyDifferentiator = builder.requestCacheKeyDifferentiator; + this.queryRewriteInterceptor = builder.queryRewriteInterceptor; this.mapperMetrics = builder.mapperMetrics; // doClose() is called when shutting down a node, yet there might still be ongoing requests // that we need to wait for before closing some resources such as the caches. In order to @@ -779,7 +782,8 @@ private synchronized IndexService createIndexService( idFieldMappers.apply(idxSettings.getMode()), valuesSourceRegistry, indexFoldersDeletionListeners, - snapshotCommitSuppliers + snapshotCommitSuppliers, + queryRewriteInterceptor ); } @@ -1766,7 +1770,7 @@ public AliasFilter buildAliasFilter(ClusterState state, String index, Set requestCacheKeyDifferentiator; MapperMetrics mapperMetrics; List searchOperationListener = List.of(); + QueryRewriteInterceptor queryRewriteInterceptor = null; public IndicesServiceBuilder settings(Settings settings) { this.settings = settings; @@ -239,6 +242,27 @@ public IndicesService build() { .flatMap(m -> m.entrySet().stream()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + var queryRewriteInterceptors = pluginsService.filterPlugins(SearchPlugin.class) + .map(SearchPlugin::getQueryRewriteInterceptors) + .flatMap(List::stream) + .collect(Collectors.toMap(QueryRewriteInterceptor::getQueryName, interceptor -> { + if (interceptor.getQueryName() == null) { + throw new IllegalArgumentException("QueryRewriteInterceptor [" + interceptor.getClass().getName() + "] requires name"); + } + return interceptor; + }, (a, b) -> { + throw new IllegalStateException( + "Conflicting rewrite interceptors [" + + a.getQueryName() + + "] found in [" + + a.getClass().getName() + + "] and [" + + b.getClass().getName() + + "]" + ); + })); + queryRewriteInterceptor = QueryRewriteInterceptor.multi(queryRewriteInterceptors); + return new IndicesService(this); } } diff --git a/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java b/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java index f5670ebd8a543..e87e9ee85b29c 100644 --- a/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/SearchPlugin.java @@ -23,6 +23,7 @@ import org.elasticsearch.index.query.QueryParser; import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder; import org.elasticsearch.index.query.functionscore.ScoreFunctionParser; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; import org.elasticsearch.search.SearchExtBuilder; import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationBuilder; @@ -128,6 +129,14 @@ default List> getQueries() { return emptyList(); } + /** + * @return Applicable {@link QueryRewriteInterceptor}s configured for this plugin. + * Note: This is internal to Elasticsearch's API and not extensible by external plugins. + */ + default List getQueryRewriteInterceptors() { + return emptyList(); + } + /** * The new {@link Aggregation}s added by this plugin. */ diff --git a/server/src/main/java/org/elasticsearch/plugins/internal/rewriter/QueryRewriteInterceptor.java b/server/src/main/java/org/elasticsearch/plugins/internal/rewriter/QueryRewriteInterceptor.java new file mode 100644 index 0000000000000..8f4fb2ce7491a --- /dev/null +++ b/server/src/main/java/org/elasticsearch/plugins/internal/rewriter/QueryRewriteInterceptor.java @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.plugins.internal.rewriter; + +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryRewriteContext; + +import java.util.Map; + +/** + * Enables modules and plugins to intercept and rewrite queries during the query rewrite phase on the coordinator node. + */ +public interface QueryRewriteInterceptor { + + /** + * Intercepts and returns a rewritten query if modifications are required; otherwise, + * returns the same provided {@link QueryBuilder} instance unchanged. + * + * @param context the {@link QueryRewriteContext} providing the context for the rewrite operation + * @param queryBuilder the original {@link QueryBuilder} to potentially rewrite + * @return the rewritten {@link QueryBuilder}, or the original instance if no rewrite was needed + */ + QueryBuilder interceptAndRewrite(QueryRewriteContext context, QueryBuilder queryBuilder); + + /** + * Name of the query to be intercepted and rewritten. + */ + String getQueryName(); + + static QueryRewriteInterceptor multi(Map interceptors) { + return interceptors.isEmpty() ? new NoOpQueryRewriteInterceptor() : new CompositeQueryRewriteInterceptor(interceptors); + } + + class CompositeQueryRewriteInterceptor implements QueryRewriteInterceptor { + final String NAME = "composite"; + private final Map interceptors; + + private CompositeQueryRewriteInterceptor(Map interceptors) { + this.interceptors = interceptors; + } + + @Override + public String getQueryName() { + return NAME; + } + + @Override + public QueryBuilder interceptAndRewrite(QueryRewriteContext context, QueryBuilder queryBuilder) { + QueryRewriteInterceptor interceptor = interceptors.get(queryBuilder.getName()); + if (interceptor != null) { + return interceptor.interceptAndRewrite(context, queryBuilder); + } + return queryBuilder; + } + } + + class NoOpQueryRewriteInterceptor implements QueryRewriteInterceptor { + @Override + public QueryBuilder interceptAndRewrite(QueryRewriteContext context, QueryBuilder queryBuilder) { + return queryBuilder; + } + + @Override + public String getQueryName() { + return null; + } + } +} diff --git a/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java b/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java index a94427cd0df6f..0707f1356516a 100644 --- a/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java @@ -1744,7 +1744,9 @@ protected void doWriteTo(StreamOutput out) throws IOException { NodeClient client = new NodeClient(settings, threadPool); SearchService searchService = mock(SearchService.class); - when(searchService.getRewriteContext(any(), any(), any())).thenReturn(new QueryRewriteContext(null, null, null, null, null)); + when(searchService.getRewriteContext(any(), any(), any())).thenReturn( + new QueryRewriteContext(null, null, null, null, null, null) + ); ClusterService clusterService = new ClusterService( settings, new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetadataTests.java index 8036a964071d2..4abd0c4a9d469 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/IndexMetadataTests.java @@ -690,7 +690,12 @@ public static Map randomInferenceFields() { } private static InferenceFieldMetadata randomInferenceFieldMetadata(String name) { - return new InferenceFieldMetadata(name, randomIdentifier(), randomSet(1, 5, ESTestCase::randomIdentifier).toArray(String[]::new)); + return new InferenceFieldMetadata( + name, + randomIdentifier(), + randomIdentifier(), + randomSet(1, 5, ESTestCase::randomIdentifier).toArray(String[]::new) + ); } private IndexMetadataStats randomIndexStats(int numberOfShards) { diff --git a/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java b/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java index 49a4d519c0ea4..c519d4834148d 100644 --- a/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java +++ b/server/src/test/java/org/elasticsearch/index/IndexModuleTests.java @@ -86,6 +86,7 @@ import org.elasticsearch.indices.fielddata.cache.IndicesFieldDataCache; import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.plugins.IndexStorePlugin; +import org.elasticsearch.plugins.internal.rewriter.MockQueryRewriteInterceptor; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.internal.ReaderContext; import org.elasticsearch.test.ClusterServiceUtils; @@ -223,7 +224,8 @@ private IndexService newIndexService(IndexModule module) throws IOException { module.indexSettings().getMode().idFieldMapperWithoutFieldData(), null, indexDeletionListener, - emptyMap() + emptyMap(), + new MockQueryRewriteInterceptor() ); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/MappingLookupInferenceFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/MappingLookupInferenceFieldMapperTests.java index 809fb161fcbe5..b1470c1ee5b3b 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/MappingLookupInferenceFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/MappingLookupInferenceFieldMapperTests.java @@ -15,7 +15,6 @@ import org.elasticsearch.plugins.MapperPlugin; import org.elasticsearch.plugins.Plugin; -import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Map; @@ -94,6 +93,7 @@ private static class TestInferenceFieldMapper extends FieldMapper implements Inf public static final TypeParser PARSER = new TypeParser((n, c) -> new Builder(n)); public static final String INFERENCE_ID = "test_inference_id"; + public static final String SEARCH_INFERENCE_ID = "test_search_inference_id"; public static final String CONTENT_TYPE = "test_inference_field"; TestInferenceFieldMapper(String simpleName) { @@ -102,7 +102,7 @@ private static class TestInferenceFieldMapper extends FieldMapper implements Inf @Override public InferenceFieldMetadata getMetadata(Set sourcePaths) { - return new InferenceFieldMetadata(fullPath(), INFERENCE_ID, sourcePaths.toArray(new String[0])); + return new InferenceFieldMetadata(fullPath(), INFERENCE_ID, SEARCH_INFERENCE_ID, sourcePaths.toArray(new String[0])); } @Override @@ -111,7 +111,7 @@ public Object getOriginalValue(Map sourceAsMap) { } @Override - protected void parseCreateField(DocumentParserContext context) throws IOException {} + protected void parseCreateField(DocumentParserContext context) {} @Override public Builder getMergeBuilder() { diff --git a/server/src/test/java/org/elasticsearch/index/query/InterceptedQueryBuilderWrapperTests.java b/server/src/test/java/org/elasticsearch/index/query/InterceptedQueryBuilderWrapperTests.java new file mode 100644 index 0000000000000..6c570e0e71725 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/InterceptedQueryBuilderWrapperTests.java @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.query; + +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.client.NoOpClient; +import org.elasticsearch.threadpool.TestThreadPool; +import org.junit.After; +import org.junit.Before; + +import java.io.IOException; + +public class InterceptedQueryBuilderWrapperTests extends ESTestCase { + + private TestThreadPool threadPool; + private NoOpClient client; + + @Before + public void setup() { + threadPool = createThreadPool(); + client = new NoOpClient(threadPool); + } + + @After + public void cleanup() { + threadPool.close(); + } + + public void testQueryNameReturnsWrappedQueryBuilder() { + MatchAllQueryBuilder matchAllQueryBuilder = new MatchAllQueryBuilder(); + InterceptedQueryBuilderWrapper interceptedQueryBuilderWrapper = new InterceptedQueryBuilderWrapper(matchAllQueryBuilder); + String queryName = randomAlphaOfLengthBetween(5, 10); + QueryBuilder namedQuery = interceptedQueryBuilderWrapper.queryName(queryName); + assertTrue(namedQuery instanceof InterceptedQueryBuilderWrapper); + assertEquals(queryName, namedQuery.queryName()); + } + + public void testQueryBoostReturnsWrappedQueryBuilder() { + MatchAllQueryBuilder matchAllQueryBuilder = new MatchAllQueryBuilder(); + InterceptedQueryBuilderWrapper interceptedQueryBuilderWrapper = new InterceptedQueryBuilderWrapper(matchAllQueryBuilder); + float boost = randomFloat(); + QueryBuilder boostedQuery = interceptedQueryBuilderWrapper.boost(boost); + assertTrue(boostedQuery instanceof InterceptedQueryBuilderWrapper); + assertEquals(boost, boostedQuery.boost(), 0.0001f); + } + + public void testRewrite() throws IOException { + QueryRewriteContext context = new QueryRewriteContext(null, client, null); + context.setQueryRewriteInterceptor(myMatchInterceptor); + + // Queries that are not intercepted behave normally + TermQueryBuilder termQueryBuilder = new TermQueryBuilder("field", "value"); + QueryBuilder rewritten = termQueryBuilder.rewrite(context); + assertTrue(rewritten instanceof TermQueryBuilder); + + // Queries that should be intercepted are and the right thing happens + MatchQueryBuilder matchQueryBuilder = new MatchQueryBuilder("field", "value"); + rewritten = matchQueryBuilder.rewrite(context); + assertTrue(rewritten instanceof InterceptedQueryBuilderWrapper); + assertTrue(((InterceptedQueryBuilderWrapper) rewritten).queryBuilder instanceof MatchQueryBuilder); + MatchQueryBuilder rewrittenMatchQueryBuilder = (MatchQueryBuilder) ((InterceptedQueryBuilderWrapper) rewritten).queryBuilder; + assertEquals("intercepted", rewrittenMatchQueryBuilder.value()); + + // An additional rewrite on an already intercepted query returns the same query + QueryBuilder rewrittenAgain = rewritten.rewrite(context); + assertTrue(rewrittenAgain instanceof InterceptedQueryBuilderWrapper); + assertEquals(rewritten, rewrittenAgain); + } + + private final QueryRewriteInterceptor myMatchInterceptor = new QueryRewriteInterceptor() { + @Override + public QueryBuilder interceptAndRewrite(QueryRewriteContext context, QueryBuilder queryBuilder) { + if (queryBuilder instanceof MatchQueryBuilder matchQueryBuilder) { + return new MatchQueryBuilder(matchQueryBuilder.fieldName(), "intercepted"); + } + return queryBuilder; + } + + @Override + public String getQueryName() { + return MatchQueryBuilder.NAME; + } + }; +} diff --git a/server/src/test/java/org/elasticsearch/index/query/QueryRewriteContextTests.java b/server/src/test/java/org/elasticsearch/index/query/QueryRewriteContextTests.java index d07bcf54fdf09..5dd231ab97886 100644 --- a/server/src/test/java/org/elasticsearch/index/query/QueryRewriteContextTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/QueryRewriteContextTests.java @@ -52,6 +52,7 @@ public void testGetTierPreference() { null, null, null, + null, null ); @@ -79,6 +80,7 @@ public void testGetTierPreference() { null, null, null, + null, null ); diff --git a/test/framework/src/main/java/org/elasticsearch/plugins/internal/rewriter/MockQueryRewriteInterceptor.java b/test/framework/src/main/java/org/elasticsearch/plugins/internal/rewriter/MockQueryRewriteInterceptor.java new file mode 100644 index 0000000000000..196e5bd4f4a2d --- /dev/null +++ b/test/framework/src/main/java/org/elasticsearch/plugins/internal/rewriter/MockQueryRewriteInterceptor.java @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.plugins.internal.rewriter; + +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryRewriteContext; + +public class MockQueryRewriteInterceptor implements QueryRewriteInterceptor { + + @Override + public QueryBuilder interceptAndRewrite(QueryRewriteContext context, QueryBuilder queryBuilder) { + return queryBuilder; + } + + @Override + public String getQueryName() { + return this.getClass().getSimpleName(); + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractBuilderTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractBuilderTestCase.java index bdf323afb8d96..20cb66affddee 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractBuilderTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractBuilderTestCase.java @@ -71,6 +71,8 @@ import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.plugins.ScriptPlugin; import org.elasticsearch.plugins.SearchPlugin; +import org.elasticsearch.plugins.internal.rewriter.MockQueryRewriteInterceptor; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; import org.elasticsearch.plugins.scanners.StablePluginsRegistry; import org.elasticsearch.script.MockScriptEngine; import org.elasticsearch.script.MockScriptService; @@ -629,7 +631,8 @@ QueryRewriteContext createQueryRewriteContext() { () -> true, scriptService, createMockResolvedIndices(), - null + null, + createMockQueryRewriteInterceptor() ); } @@ -670,5 +673,9 @@ private ResolvedIndices createMockResolvedIndices() { Map.of(index, indexMetadata) ); } + + private QueryRewriteInterceptor createMockQueryRewriteInterceptor() { + return new MockQueryRewriteInterceptor(); + } } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java index 67892dfe78624..3b7613b8b0e1f 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferenceFeatures.java @@ -10,6 +10,7 @@ import org.elasticsearch.features.FeatureSpecification; import org.elasticsearch.features.NodeFeature; import org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper; +import org.elasticsearch.xpack.inference.queries.SemanticMatchQueryRewriteInterceptor; import org.elasticsearch.xpack.inference.queries.SemanticQueryBuilder; import org.elasticsearch.xpack.inference.rank.random.RandomRankRetrieverBuilder; import org.elasticsearch.xpack.inference.rank.textsimilarity.TextSimilarityRankRetrieverBuilder; @@ -43,7 +44,8 @@ public Set getTestFeatures() { SemanticTextFieldMapper.SEMANTIC_TEXT_DELETE_FIX, SemanticTextFieldMapper.SEMANTIC_TEXT_ZERO_SIZE_FIX, SemanticTextFieldMapper.SEMANTIC_TEXT_ALWAYS_EMIT_INFERENCE_ID_FIX, - SEMANTIC_TEXT_HIGHLIGHTER + SEMANTIC_TEXT_HIGHLIGHTER, + SemanticMatchQueryRewriteInterceptor.SEMANTIC_MATCH_QUERY_REWRITE_INTERCEPTION_SUPPORTED ); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java index eef07aefb30c8..93743a5485c2c 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/InferencePlugin.java @@ -36,6 +36,7 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.SearchPlugin; import org.elasticsearch.plugins.SystemIndexPlugin; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; import org.elasticsearch.search.fetch.subphase.highlight.Highlighter; @@ -77,6 +78,7 @@ import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import org.elasticsearch.xpack.inference.mapper.OffsetSourceFieldMapper; import org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper; +import org.elasticsearch.xpack.inference.queries.SemanticMatchQueryRewriteInterceptor; import org.elasticsearch.xpack.inference.queries.SemanticQueryBuilder; import org.elasticsearch.xpack.inference.rank.random.RandomRankBuilder; import org.elasticsearch.xpack.inference.rank.random.RandomRankRetrieverBuilder; @@ -436,6 +438,11 @@ public List> getQueries() { return List.of(new QuerySpec<>(SemanticQueryBuilder.NAME, SemanticQueryBuilder::new, SemanticQueryBuilder::fromXContent)); } + @Override + public List getQueryRewriteInterceptors() { + return List.of(new SemanticMatchQueryRewriteInterceptor()); + } + @Override public List> getRetrievers() { return List.of( diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticMatchQueryRewriteInterceptor.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticMatchQueryRewriteInterceptor.java new file mode 100644 index 0000000000000..a4a8123935c3e --- /dev/null +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticMatchQueryRewriteInterceptor.java @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.inference.queries; + +import org.elasticsearch.action.ResolvedIndices; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.InferenceFieldMetadata; +import org.elasticsearch.features.NodeFeature; +import org.elasticsearch.index.mapper.IndexFieldMapper; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.MatchQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryRewriteContext; +import org.elasticsearch.index.query.TermQueryBuilder; +import org.elasticsearch.index.query.TermsQueryBuilder; +import org.elasticsearch.plugins.internal.rewriter.QueryRewriteInterceptor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class SemanticMatchQueryRewriteInterceptor implements QueryRewriteInterceptor { + + public static final NodeFeature SEMANTIC_MATCH_QUERY_REWRITE_INTERCEPTION_SUPPORTED = new NodeFeature( + "search.semantic_match_query_rewrite_interception_supported" + ); + + public SemanticMatchQueryRewriteInterceptor() {} + + @Override + public QueryBuilder interceptAndRewrite(QueryRewriteContext context, QueryBuilder queryBuilder) { + assert (queryBuilder instanceof MatchQueryBuilder); + MatchQueryBuilder matchQueryBuilder = (MatchQueryBuilder) queryBuilder; + QueryBuilder rewritten = queryBuilder; + ResolvedIndices resolvedIndices = context.getResolvedIndices(); + if (resolvedIndices != null) { + Collection indexMetadataCollection = resolvedIndices.getConcreteLocalIndicesMetadata().values(); + List inferenceIndices = new ArrayList<>(); + List nonInferenceIndices = new ArrayList<>(); + for (IndexMetadata indexMetadata : indexMetadataCollection) { + String indexName = indexMetadata.getIndex().getName(); + InferenceFieldMetadata inferenceFieldMetadata = indexMetadata.getInferenceFields().get(matchQueryBuilder.fieldName()); + if (inferenceFieldMetadata != null) { + inferenceIndices.add(indexName); + } else { + nonInferenceIndices.add(indexName); + } + } + + if (inferenceIndices.isEmpty()) { + return rewritten; + } else if (nonInferenceIndices.isEmpty() == false) { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + for (String inferenceIndexName : inferenceIndices) { + // Add a separate clause for each semantic query, because they may be using different inference endpoints + // TODO - consolidate this to a single clause once the semantic query supports multiple inference endpoints + boolQueryBuilder.should( + createSemanticSubQuery(inferenceIndexName, matchQueryBuilder.fieldName(), (String) matchQueryBuilder.value()) + ); + } + boolQueryBuilder.should(createMatchSubQuery(nonInferenceIndices, matchQueryBuilder)); + rewritten = boolQueryBuilder; + } else { + rewritten = new SemanticQueryBuilder(matchQueryBuilder.fieldName(), (String) matchQueryBuilder.value(), false); + } + } + + return rewritten; + + } + + @Override + public String getQueryName() { + return MatchQueryBuilder.NAME; + } + + private QueryBuilder createSemanticSubQuery(String indexName, String fieldName, String value) { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + boolQueryBuilder.must(new SemanticQueryBuilder(fieldName, value, true)); + boolQueryBuilder.filter(new TermQueryBuilder(IndexFieldMapper.NAME, indexName)); + return boolQueryBuilder; + } + + private QueryBuilder createMatchSubQuery(List indices, MatchQueryBuilder matchQueryBuilder) { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + boolQueryBuilder.must(matchQueryBuilder); + boolQueryBuilder.filter(new TermsQueryBuilder(IndexFieldMapper.NAME, indices)); + return boolQueryBuilder; + } +} diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java index d648db2fbfdbc..2a34651efcd9d 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java @@ -46,6 +46,7 @@ import java.util.Objects; import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; +import static org.elasticsearch.xcontent.ConstructingObjectParser.optionalConstructorArg; import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; @@ -57,16 +58,18 @@ public class SemanticQueryBuilder extends AbstractQueryBuilder PARSER = new ConstructingObjectParser<>( NAME, false, - args -> new SemanticQueryBuilder((String) args[0], (String) args[1]) + args -> new SemanticQueryBuilder((String) args[0], (String) args[1], (Boolean) args[2]) ); static { PARSER.declareString(constructorArg(), FIELD_FIELD); PARSER.declareString(constructorArg(), QUERY_FIELD); + PARSER.declareBoolean(optionalConstructorArg(), LENIENT_FIELD); declareStandardFields(PARSER); } @@ -75,8 +78,13 @@ public class SemanticQueryBuilder extends AbstractQueryBuilder inferenceResultsSupplier; private final InferenceResults inferenceResults; private final boolean noInferenceResults; + private final Boolean lenient; public SemanticQueryBuilder(String fieldName, String query) { + this(fieldName, query, null); + } + + public SemanticQueryBuilder(String fieldName, String query, Boolean lenient) { if (fieldName == null) { throw new IllegalArgumentException("[" + NAME + "] requires a " + FIELD_FIELD.getPreferredName() + " value"); } @@ -88,6 +96,7 @@ public SemanticQueryBuilder(String fieldName, String query) { this.inferenceResults = null; this.inferenceResultsSupplier = null; this.noInferenceResults = false; + this.lenient = lenient; } public SemanticQueryBuilder(StreamInput in) throws IOException { @@ -97,6 +106,11 @@ public SemanticQueryBuilder(StreamInput in) throws IOException { this.inferenceResults = in.readOptionalNamedWriteable(InferenceResults.class); this.noInferenceResults = in.readBoolean(); this.inferenceResultsSupplier = null; + if (in.getTransportVersion().onOrAfter(TransportVersions.SEMANTIC_QUERY_LENIENT)) { + this.lenient = in.readOptionalBoolean(); + } else { + this.lenient = null; + } } @Override @@ -108,6 +122,9 @@ protected void doWriteTo(StreamOutput out) throws IOException { out.writeString(query); out.writeOptionalNamedWriteable(inferenceResults); out.writeBoolean(noInferenceResults); + if (out.getTransportVersion().onOrAfter(TransportVersions.SEMANTIC_QUERY_LENIENT)) { + out.writeOptionalBoolean(lenient); + } } private SemanticQueryBuilder( @@ -123,6 +140,7 @@ private SemanticQueryBuilder( this.inferenceResultsSupplier = inferenceResultsSupplier; this.inferenceResults = inferenceResults; this.noInferenceResults = noInferenceResults; + this.lenient = other.lenient; } @Override @@ -144,6 +162,9 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep builder.startObject(NAME); builder.field(FIELD_FIELD.getPreferredName(), fieldName); builder.field(QUERY_FIELD.getPreferredName(), query); + if (lenient != null) { + builder.field(LENIENT_FIELD.getPreferredName(), lenient); + } boostAndQueryNameToXContent(builder); builder.endObject(); } @@ -171,6 +192,8 @@ private QueryBuilder doRewriteBuildSemanticQuery(SearchExecutionContext searchEx } return semanticTextFieldType.semanticQuery(inferenceResults, searchExecutionContext.requestSize(), boost(), queryName()); + } else if (lenient != null && lenient) { + return new MatchNoneQueryBuilder(); } else { throw new IllegalArgumentException( "Field [" + fieldName + "] of type [" + fieldType.typeName() + "] does not support " + NAME + " queries" diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterTests.java index 2416aeb62ff33..c68a629b999c5 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/action/filter/ShardBulkInferenceActionFilterTests.java @@ -102,7 +102,7 @@ public void testFilterNoop() throws Exception { new BulkItemRequest[0] ); request.setInferenceFieldMap( - Map.of("foo", new InferenceFieldMetadata("foo", "bar", generateRandomStringArray(5, 10, false, false))) + Map.of("foo", new InferenceFieldMetadata("foo", "bar", "baz", generateRandomStringArray(5, 10, false, false))) ); filter.apply(task, TransportShardBulkAction.ACTION_NAME, request, actionListener, actionFilterChain); awaitLatch(chainExecuted, 10, TimeUnit.SECONDS); diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/40_semantic_text_query.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/40_semantic_text_query.yml index c2704a4c22914..3d3790d879ef1 100644 --- a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/40_semantic_text_query.yml +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/40_semantic_text_query.yml @@ -101,7 +101,7 @@ setup: index: test-sparse-index id: doc_1 body: - inference_field: ["inference test", "another inference test"] + inference_field: [ "inference test", "another inference test" ] non_inference_field: "non inference test" refresh: true @@ -132,7 +132,7 @@ setup: index: test-sparse-index id: doc_1 body: - inference_field: [40, 49.678] + inference_field: [ 40, 49.678 ] refresh: true - do: @@ -229,7 +229,7 @@ setup: index: test-dense-index id: doc_1 body: - inference_field: ["inference test", "another inference test"] + inference_field: [ "inference test", "another inference test" ] non_inference_field: "non inference test" refresh: true @@ -260,7 +260,7 @@ setup: index: test-dense-index id: doc_1 body: - inference_field: [45.1, 100] + inference_field: [ 45.1, 100 ] refresh: true - do: @@ -387,7 +387,7 @@ setup: index: test-dense-index id: doc_1 body: - inference_field: ["inference test", "another inference test"] + inference_field: [ "inference test", "another inference test" ] non_inference_field: "non inference test" refresh: true @@ -418,7 +418,7 @@ setup: index: test-sparse-index id: doc_1 body: - inference_field: ["inference test", "another inference test"] + inference_field: [ "inference test", "another inference test" ] non_inference_field: "non inference test" refresh: true @@ -440,7 +440,7 @@ setup: - match: { hits.hits.0._id: "doc_1" } - close_to: { hits.hits.0._score: { value: 3.783733e19, error: 1e13 } } - length: { hits.hits.0._source.inference_field.inference.chunks: 2 } - - match: { hits.hits.0.matched_queries: ["i-like-naming-my-queries"] } + - match: { hits.hits.0.matched_queries: [ "i-like-naming-my-queries" ] } --- "Query an index alias": @@ -452,7 +452,7 @@ setup: index: test-sparse-index id: doc_1 body: - inference_field: ["inference test", "another inference test"] + inference_field: [ "inference test", "another inference test" ] non_inference_field: "non inference test" refresh: true @@ -503,6 +503,48 @@ setup: - match: { error.root_cause.0.type: "illegal_argument_exception" } - match: { error.root_cause.0.reason: "Field [non_inference_field] of type [text] does not support semantic queries" } +--- +"Query the wrong field type with lenient: true": + - requires: + cluster_features: "search.semantic_match_query_rewrite_interception_supported" + reason: lenient introduced in 8.18.0 + + - do: + index: + index: test-sparse-index + id: doc_1 + body: + inference_field: "inference test" + non_inference_field: "non inference test" + refresh: true + + - do: + catch: bad_request + search: + index: test-sparse-index + body: + query: + semantic: + field: "non_inference_field" + query: "inference test" + + - match: { error.type: "search_phase_execution_exception" } + - match: { error.root_cause.0.type: "illegal_argument_exception" } + - match: { error.root_cause.0.reason: "Field [non_inference_field] of type [text] does not support semantic queries" } + + - do: + search: + index: test-sparse-index + body: + query: + semantic: + field: "non_inference_field" + query: "inference test" + lenient: true + + - match: { hits.total.value: 0 } + + --- "Query a missing field": - do: @@ -783,7 +825,7 @@ setup: index: test-dense-index id: doc_1 body: - inference_field: ["inference test", "another inference test"] + inference_field: [ "inference test", "another inference test" ] non_inference_field: "non inference test" refresh: true @@ -844,11 +886,11 @@ setup: "Query a field that uses the default ELSER 2 endpoint": - requires: reason: "default ELSER 2 inference ID is enabled via a capability" - test_runner_features: [capabilities] + test_runner_features: [ capabilities ] capabilities: - method: GET path: /_inference - capabilities: [default_elser_2] + capabilities: [ default_elser_2 ] - do: indices.create: diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/45_semantic_text_match.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/45_semantic_text_match.yml new file mode 100644 index 0000000000000..cdbf73d31a272 --- /dev/null +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/45_semantic_text_match.yml @@ -0,0 +1,284 @@ +setup: + - requires: + cluster_features: "search.semantic_match_query_rewrite_interception_supported" + reason: semantic_text match support introduced in 8.18.0 + + - do: + inference.put: + task_type: sparse_embedding + inference_id: sparse-inference-id + body: > + { + "service": "test_service", + "service_settings": { + "model": "my_model", + "api_key": "abc64" + }, + "task_settings": { + } + } + + - do: + inference.put: + task_type: sparse_embedding + inference_id: sparse-inference-id-2 + body: > + { + "service": "test_service", + "service_settings": { + "model": "my_model", + "api_key": "abc64" + }, + "task_settings": { + } + } + + - do: + inference.put: + task_type: text_embedding + inference_id: dense-inference-id + body: > + { + "service": "text_embedding_test_service", + "service_settings": { + "model": "my_model", + "dimensions": 10, + "api_key": "abc64", + "similarity": "COSINE" + }, + "task_settings": { + } + } + + - do: + indices.create: + index: test-sparse-index + body: + mappings: + properties: + inference_field: + type: semantic_text + inference_id: sparse-inference-id + non_inference_field: + type: text + + - do: + indices.create: + index: test-dense-index + body: + mappings: + properties: + inference_field: + type: semantic_text + inference_id: dense-inference-id + non_inference_field: + type: text + + - do: + indices.create: + index: test-text-only-index + body: + mappings: + properties: + inference_field: + type: text + non_inference_field: + type: text + +--- +"Query using a sparse embedding model": + - skip: + features: [ "headers", "close_to" ] + + - do: + index: + index: test-sparse-index + id: doc_1 + body: + inference_field: [ "inference test", "another inference test" ] + non_inference_field: "non inference test" + refresh: true + + - do: + headers: + # Force JSON content type so that we use a parser that interprets the floating-point score as a double + Content-Type: application/json + search: + index: test-sparse-index + body: + query: + match: + inference_field: + query: "inference test" + + - match: { hits.total.value: 1 } + - match: { hits.hits.0._id: "doc_1" } + +--- +"Query using a dense embedding model": + - skip: + features: [ "headers", "close_to" ] + + - do: + index: + index: test-dense-index + id: doc_1 + body: + inference_field: [ "inference test", "another inference test" ] + non_inference_field: "non inference test" + refresh: true + + - do: + headers: + # Force JSON content type so that we use a parser that interprets the floating-point score as a double + Content-Type: application/json + search: + index: test-dense-index + body: + query: + match: + inference_field: + query: "inference test" + + - match: { hits.total.value: 1 } + - match: { hits.hits.0._id: "doc_1" } + +--- +"Query an index alias": + - skip: + features: [ "headers", "close_to" ] + + - do: + index: + index: test-sparse-index + id: doc_1 + body: + inference_field: [ "inference test", "another inference test" ] + non_inference_field: "non inference test" + refresh: true + + - do: + indices.put_alias: + index: test-sparse-index + name: my-alias + + - do: + headers: + # Force JSON content type so that we use a parser that interprets the floating-point score as a double + Content-Type: application/json + search: + index: my-alias + body: + query: + match: + inference_field: + query: "inference test" + + - match: { hits.total.value: 1 } + - match: { hits.hits.0._id: "doc_1" } + +--- +"Query indices with both semantic_text and regular text content": + + - do: + index: + index: test-sparse-index + id: doc_1 + body: + inference_field: [ "inference test", "another inference test" ] + non_inference_field: "non inference test" + refresh: true + + - do: + index: + index: test-text-only-index + id: doc_2 + body: + inference_field: [ "inference test", "not an inference field" ] + non_inference_field: "non inference test" + refresh: true + + - do: + search: + index: + - test-sparse-index + - test-text-only-index + body: + query: + match: + inference_field: + query: "inference test" + + - match: { hits.total.value: 2 } + - match: { hits.hits.0._id: "doc_1" } + - match: { hits.hits.1._id: "doc_2" } + + # Test querying multiple indices that either use the same inference ID or combine semantic_text with lexical search + - do: + indices.create: + index: test-sparse-index-2 + body: + mappings: + properties: + inference_field: + type: semantic_text + inference_id: sparse-inference-id + non_inference_field: + type: text + + - do: + index: + index: test-sparse-index-2 + id: doc_3 + body: + inference_field: "another inference test" + refresh: true + + - do: + search: + index: + - test-sparse-index* + - test-text-only-index + body: + query: + match: + inference_field: + query: "inference test" + + - match: { hits.total.value: 3 } + - match: { hits.hits.0._id: "doc_1" } + - match: { hits.hits.1._id: "doc_3" } + - match: { hits.hits.2._id: "doc_2" } + +--- +"Query a field that has no indexed inference results": + - skip: + features: [ "headers" ] + + - do: + headers: + # Force JSON content type so that we use a parser that interprets the floating-point score as a double + Content-Type: application/json + search: + index: test-sparse-index + body: + query: + match: + inference_field: + query: "inference test" + + - match: { hits.total.value: 0 } + + - do: + headers: + # Force JSON content type so that we use a parser that interprets the floating-point score as a double + Content-Type: application/json + search: + index: test-dense-index + body: + query: + match: + inference_field: + query: "inference test" + + - match: { hits.total.value: 0 } diff --git a/x-pack/plugin/rank-rrf/src/test/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilderTests.java b/x-pack/plugin/rank-rrf/src/test/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilderTests.java index d20f0f88aeb16..bdd6d73ec0fbf 100644 --- a/x-pack/plugin/rank-rrf/src/test/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilderTests.java +++ b/x-pack/plugin/rank-rrf/src/test/java/org/elasticsearch/xpack/rank/rrf/RRFRetrieverBuilderTests.java @@ -54,7 +54,9 @@ public void testRetrieverExtractionErrors() throws IOException { IllegalArgumentException iae = expectThrows( IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true) - .rewrite(new QueryRewriteContext(parserConfig(), null, null, null, new PointInTimeBuilder(new BytesArray("pitid")))) + .rewrite( + new QueryRewriteContext(parserConfig(), null, null, null, new PointInTimeBuilder(new BytesArray("pitid")), null) + ) ); assertEquals("[search_after] cannot be used in children of compound retrievers", iae.getMessage()); } @@ -70,7 +72,9 @@ public void testRetrieverExtractionErrors() throws IOException { IllegalArgumentException iae = expectThrows( IllegalArgumentException.class, () -> ssb.parseXContent(parser, true, nf -> true) - .rewrite(new QueryRewriteContext(parserConfig(), null, null, null, new PointInTimeBuilder(new BytesArray("pitid")))) + .rewrite( + new QueryRewriteContext(parserConfig(), null, null, null, new PointInTimeBuilder(new BytesArray("pitid")), null) + ) ); assertEquals("[terminate_after] cannot be used in children of compound retrievers", iae.getMessage()); } From 3cbdfba6109331e9ba245ffad827921ab777dc60 Mon Sep 17 00:00:00 2001 From: Marci W <333176+marciw@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:59:47 -0500 Subject: [PATCH 59/77] Fix invalid index mode (#118579) --- docs/reference/index-modules.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/index-modules.asciidoc b/docs/reference/index-modules.asciidoc index d9b8f8802a04b..73e2db6e45e34 100644 --- a/docs/reference/index-modules.asciidoc +++ b/docs/reference/index-modules.asciidoc @@ -113,7 +113,7 @@ Index mode supports the following values: `standard`::: Standard indexing with default settings. -`tsds`::: _(data streams only)_ Index mode optimized for storage of metrics. For more information, see <>. +`time_series`::: _(data streams only)_ Index mode optimized for storage of metrics. For more information, see <>. `logsdb`::: _(data streams only)_ Index mode optimized for <>. From ce990a5ee21f7c6eccdd7f30c432d95f0655272e Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Fri, 13 Dec 2024 03:00:08 +1100 Subject: [PATCH 60/77] Remove 8.15 from branches.json --- branches.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/branches.json b/branches.json index 0e23a795664dd..95fbdb1efd655 100644 --- a/branches.json +++ b/branches.json @@ -13,9 +13,6 @@ { "branch": "8.x" }, - { - "branch": "8.15" - }, { "branch": "7.17" } From 555ff55a6e463d8682190ebcda57a72708753449 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Thu, 12 Dec 2024 12:02:24 -0500 Subject: [PATCH 61/77] ESQL: Enterprise license enforcement for CCS (#118102) ES|QL CCS is an enterprise licensed feature. This PR enforces that no ES|QL CCS query can proceed unless a valid enterprise or trial license is present on the querying cluster. If a valid license is not present a 400 Bad Request error is returned explaining that an enterprise license is needed and showing what license (if any) was found. If a valid license is found, then the license usage timestamp will be updated. Subsequent calls to the `GET /_license/feature_usage` endpoint will show an entry for `esql-ccs` with the last timestamp that it was checked and used. ``` { "features": [ { "family": null, "name": "esql-ccs", "context": null, "last_used": "2024-12-09T19:54:38.767Z", "license_level": "enterprise" } ] } ``` --- docs/changelog/118102.yaml | 5 + .../license/LicensedFeature.java | 2 +- .../license/XPackLicenseState.java | 21 ++ .../license/XPackLicenseStateTests.java | 17 + ...stractEnrichBasedCrossClusterTestCase.java | 290 ++++++++++++++++++ .../esql/action/CrossClusterAsyncQueryIT.java | 3 +- ...ossClusterEnrichUnavailableClustersIT.java | 165 +--------- ...CrossClusterQueryUnavailableRemotesIT.java | 5 +- .../action/CrossClustersCancellationIT.java | 3 +- .../esql/action/CrossClustersEnrichIT.java | 246 +-------------- ...ssClustersQueriesWithInvalidLicenseIT.java | 203 ++++++++++++ .../esql/action/CrossClustersQueryIT.java | 17 +- ...sqlPluginWithEnterpriseOrTrialLicense.java | 26 ++ ...uginWithNonEnterpriseOrExpiredLicense.java | 47 +++ .../xpack/esql/analysis/Verifier.java | 4 + .../esql/session/EsqlLicenseChecker.java | 51 +++ .../xpack/esql/session/EsqlSession.java | 3 + .../esql/session/EsqlSessionCCSUtils.java | 43 +++ .../session/EsqlSessionCCSUtilsTests.java | 158 ++++++++++ .../RemoteClusterSecurityEsqlIT.java | 35 +++ .../test/querying_cluster/80_esql.yml | 63 +--- 21 files changed, 935 insertions(+), 472 deletions(-) create mode 100644 docs/changelog/118102.yaml create mode 100644 x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractEnrichBasedCrossClusterTestCase.java create mode 100644 x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersQueriesWithInvalidLicenseIT.java create mode 100644 x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithEnterpriseOrTrialLicense.java create mode 100644 x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithNonEnterpriseOrExpiredLicense.java create mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlLicenseChecker.java diff --git a/docs/changelog/118102.yaml b/docs/changelog/118102.yaml new file mode 100644 index 0000000000000..e5ec32cdddbec --- /dev/null +++ b/docs/changelog/118102.yaml @@ -0,0 +1,5 @@ +pr: 118102 +summary: "ESQL: Enterprise license enforcement for CCS" +area: ES|QL +type: enhancement +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensedFeature.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensedFeature.java index d86c15aa14bc9..558303f7e0f0f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensedFeature.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensedFeature.java @@ -104,7 +104,7 @@ public boolean isNeedsActive() { return needsActive; } - /** Create a momentary feature for hte given license level */ + /** Create a momentary feature for the given license level */ public static Momentary momentary(String family, String name, License.OperationMode licenseLevel) { return new Momentary(family, name, licenseLevel, true); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java index 4f8a18e28aea1..3c7b089b4cd63 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java @@ -106,6 +106,7 @@ public class XPackLicenseState { messages.put(XPackField.CCR, XPackLicenseState::ccrAcknowledgementMessages); messages.put(XPackField.ENTERPRISE_SEARCH, XPackLicenseState::enterpriseSearchAcknowledgementMessages); messages.put(XPackField.REDACT_PROCESSOR, XPackLicenseState::redactProcessorAcknowledgementMessages); + messages.put(XPackField.ESQL, XPackLicenseState::esqlAcknowledgementMessages); ACKNOWLEDGMENT_MESSAGES = Collections.unmodifiableMap(messages); } @@ -243,6 +244,26 @@ private static String[] enterpriseSearchAcknowledgementMessages(OperationMode cu return Strings.EMPTY_ARRAY; } + private static String[] esqlAcknowledgementMessages(OperationMode currentMode, OperationMode newMode) { + /* + * Provide an acknowledgement warning to customers that downgrade from Trial or Enterprise to a lower + * license level (Basic, Standard, Gold or Premium) that they will no longer be able to do CCS in ES|QL. + */ + switch (newMode) { + case BASIC: + case STANDARD: + case GOLD: + case PLATINUM: + switch (currentMode) { + case TRIAL: + case ENTERPRISE: + return new String[] { "ES|QL cross-cluster search will be disabled." }; + } + break; + } + return Strings.EMPTY_ARRAY; + } + private static String[] machineLearningAcknowledgementMessages(OperationMode currentMode, OperationMode newMode) { switch (newMode) { case BASIC: diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java index e889d25cd7a96..d788a0b5abd37 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/XPackLicenseStateTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.xpack.core.XPackField; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @@ -59,6 +60,12 @@ void assertAckMessages(String feature, OperationMode from, OperationMode to, int assertEquals(expectedMessages, gotMessages.length); } + void assertAckMessages(String feature, OperationMode from, OperationMode to, Set expectedMessages) { + String[] gotMessages = XPackLicenseState.ACKNOWLEDGMENT_MESSAGES.get(feature).apply(from, to); + Set actualMessages = Arrays.stream(gotMessages).collect(Collectors.toSet()); + assertThat(actualMessages, equalTo(expectedMessages)); + } + static T randomFrom(T[] values, Predicate filter) { return randomFrom(Arrays.stream(values).filter(filter).collect(Collectors.toList())); } @@ -143,6 +150,16 @@ public void testCcrAckTrialOrPlatinumToNotTrialOrPlatinum() { assertAckMessages(XPackField.CCR, randomTrialOrPlatinumMode(), randomBasicStandardOrGold(), 1); } + public void testEsqlAckToTrialOrPlatinum() { + assertAckMessages(XPackField.ESQL, randomMode(), randomFrom(TRIAL, ENTERPRISE), 0); + } + + public void testEsqlAckTrialOrEnterpriseToNotTrialOrEnterprise() { + for (OperationMode to : List.of(BASIC, STANDARD, GOLD, PLATINUM)) { + assertAckMessages(XPackField.ESQL, randomFrom(TRIAL, ENTERPRISE), to, Set.of("ES|QL cross-cluster search will be disabled.")); + } + } + public void testExpiredLicense() { // use standard feature which would normally be allowed at all license levels LicensedFeature feature = LicensedFeature.momentary("family", "enterpriseFeature", STANDARD); diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractEnrichBasedCrossClusterTestCase.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractEnrichBasedCrossClusterTestCase.java new file mode 100644 index 0000000000000..66ac32b33cd4d --- /dev/null +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractEnrichBasedCrossClusterTestCase.java @@ -0,0 +1,290 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.action; + +import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.TransportAction; +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.ingest.common.IngestCommonPlugin; +import org.elasticsearch.injection.guice.Inject; +import org.elasticsearch.license.LicenseService; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.protocol.xpack.XPackInfoRequest; +import org.elasticsearch.protocol.xpack.XPackInfoResponse; +import org.elasticsearch.reindex.ReindexPlugin; +import org.elasticsearch.test.AbstractMultiClustersTestCase; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.action.TransportXPackInfoAction; +import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction; +import org.elasticsearch.xpack.core.action.XPackInfoFeatureResponse; +import org.elasticsearch.xpack.core.enrich.EnrichPolicy; +import org.elasticsearch.xpack.core.enrich.action.DeleteEnrichPolicyAction; +import org.elasticsearch.xpack.core.enrich.action.ExecuteEnrichPolicyAction; +import org.elasticsearch.xpack.core.enrich.action.PutEnrichPolicyAction; +import org.elasticsearch.xpack.enrich.EnrichPlugin; +import org.elasticsearch.xpack.esql.EsqlTestUtils; +import org.elasticsearch.xpack.esql.plan.logical.Enrich; +import org.junit.After; +import org.junit.Before; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.hamcrest.Matchers.containsString; + +public abstract class AbstractEnrichBasedCrossClusterTestCase extends AbstractMultiClustersTestCase { + + public static String REMOTE_CLUSTER_1 = "c1"; + public static String REMOTE_CLUSTER_2 = "c2"; + + /** + * subclasses should override if they don't want enrich policies wiped after each test method run + */ + protected boolean tolerateErrorsWhenWipingEnrichPolicies() { + return false; + } + + @Override + protected List remoteClusterAlias() { + return List.of(REMOTE_CLUSTER_1, REMOTE_CLUSTER_2); + } + + protected Collection allClusters() { + return CollectionUtils.appendToCopy(remoteClusterAlias(), LOCAL_CLUSTER); + } + + @Override + protected Collection> nodePlugins(String clusterAlias) { + List> plugins = new ArrayList<>(super.nodePlugins(clusterAlias)); + plugins.add(CrossClustersEnrichIT.LocalStateEnrich.class); + plugins.add(IngestCommonPlugin.class); + plugins.add(ReindexPlugin.class); + return plugins; + } + + @Override + protected Settings nodeSettings() { + return Settings.builder().put(super.nodeSettings()).put(XPackSettings.SECURITY_ENABLED.getKey(), false).build(); + } + + static final EnrichPolicy hostPolicy = new EnrichPolicy("match", null, List.of("hosts"), "ip", List.of("ip", "os")); + static final EnrichPolicy vendorPolicy = new EnrichPolicy("match", null, List.of("vendors"), "os", List.of("os", "vendor")); + + @Before + public void setupHostsEnrich() { + // the hosts policy are identical on every node + Map allHosts = Map.of( + "192.168.1.2", + "Windows", + "192.168.1.3", + "MacOS", + "192.168.1.4", + "Linux", + "192.168.1.5", + "Android", + "192.168.1.6", + "iOS", + "192.168.1.7", + "Windows", + "192.168.1.8", + "MacOS", + "192.168.1.9", + "Linux", + "192.168.1.10", + "Linux", + "192.168.1.11", + "Windows" + ); + for (String cluster : allClusters()) { + Client client = client(cluster); + client.admin().indices().prepareCreate("hosts").setMapping("ip", "type=ip", "os", "type=keyword").get(); + for (Map.Entry h : allHosts.entrySet()) { + client.prepareIndex("hosts").setSource("ip", h.getKey(), "os", h.getValue()).get(); + } + client.admin().indices().prepareRefresh("hosts").get(); + client.execute(PutEnrichPolicyAction.INSTANCE, new PutEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, "hosts", hostPolicy)) + .actionGet(); + client.execute(ExecuteEnrichPolicyAction.INSTANCE, new ExecuteEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, "hosts")) + .actionGet(); + assertAcked(client.admin().indices().prepareDelete("hosts")); + } + } + + @Before + public void setupVendorPolicy() { + var localVendors = Map.of("Windows", "Microsoft", "MacOS", "Apple", "iOS", "Apple", "Android", "Samsung", "Linux", "Redhat"); + var c1Vendors = Map.of("Windows", "Microsoft", "MacOS", "Apple", "iOS", "Apple", "Android", "Google", "Linux", "Suse"); + var c2Vendors = Map.of("Windows", "Microsoft", "MacOS", "Apple", "iOS", "Apple", "Android", "Sony", "Linux", "Ubuntu"); + var vendors = Map.of(LOCAL_CLUSTER, localVendors, REMOTE_CLUSTER_1, c1Vendors, REMOTE_CLUSTER_2, c2Vendors); + for (Map.Entry> e : vendors.entrySet()) { + Client client = client(e.getKey()); + client.admin().indices().prepareCreate("vendors").setMapping("os", "type=keyword", "vendor", "type=keyword").get(); + for (Map.Entry v : e.getValue().entrySet()) { + client.prepareIndex("vendors").setSource("os", v.getKey(), "vendor", v.getValue()).get(); + } + client.admin().indices().prepareRefresh("vendors").get(); + client.execute(PutEnrichPolicyAction.INSTANCE, new PutEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, "vendors", vendorPolicy)) + .actionGet(); + client.execute(ExecuteEnrichPolicyAction.INSTANCE, new ExecuteEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, "vendors")) + .actionGet(); + assertAcked(client.admin().indices().prepareDelete("vendors")); + } + } + + @Before + public void setupEventsIndices() { + record Event(long timestamp, String user, String host) { + + } + List e0 = List.of( + new Event(1, "matthew", "192.168.1.3"), + new Event(2, "simon", "192.168.1.5"), + new Event(3, "park", "192.168.1.2"), + new Event(4, "andrew", "192.168.1.7"), + new Event(5, "simon", "192.168.1.20"), + new Event(6, "kevin", "192.168.1.2"), + new Event(7, "akio", "192.168.1.5"), + new Event(8, "luke", "192.168.1.2"), + new Event(9, "jack", "192.168.1.4") + ); + List e1 = List.of( + new Event(1, "andres", "192.168.1.2"), + new Event(2, "sergio", "192.168.1.6"), + new Event(3, "kylian", "192.168.1.8"), + new Event(4, "andrew", "192.168.1.9"), + new Event(5, "jack", "192.168.1.3"), + new Event(6, "kevin", "192.168.1.4"), + new Event(7, "akio", "192.168.1.7"), + new Event(8, "kevin", "192.168.1.21"), + new Event(9, "andres", "192.168.1.8") + ); + List e2 = List.of( + new Event(1, "park", "192.168.1.25"), + new Event(2, "akio", "192.168.1.5"), + new Event(3, "park", "192.168.1.2"), + new Event(4, "kevin", "192.168.1.3") + ); + for (var c : Map.of(LOCAL_CLUSTER, e0, REMOTE_CLUSTER_1, e1, REMOTE_CLUSTER_2, e2).entrySet()) { + Client client = client(c.getKey()); + client.admin() + .indices() + .prepareCreate("events") + .setMapping("timestamp", "type=long", "user", "type=keyword", "host", "type=ip") + .get(); + for (var e : c.getValue()) { + client.prepareIndex("events").setSource("timestamp", e.timestamp, "user", e.user, "host", e.host).get(); + } + client.admin().indices().prepareRefresh("events").get(); + } + } + + @After + public void wipeEnrichPolicies() { + for (String cluster : allClusters()) { + cluster(cluster).wipe(Set.of()); + for (String policy : List.of("hosts", "vendors")) { + if (tolerateErrorsWhenWipingEnrichPolicies()) { + try { + client(cluster).execute( + DeleteEnrichPolicyAction.INSTANCE, + new DeleteEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, policy) + ); + } catch (Exception e) { + assertThat(e.getMessage(), containsString("Cluster is already closed")); + } + + } else { + client(cluster).execute( + DeleteEnrichPolicyAction.INSTANCE, + new DeleteEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, policy) + ); + } + } + } + } + + static String enrichHosts(Enrich.Mode mode) { + return EsqlTestUtils.randomEnrichCommand("hosts", mode, hostPolicy.getMatchField(), hostPolicy.getEnrichFields()); + } + + static String enrichVendors(Enrich.Mode mode) { + return EsqlTestUtils.randomEnrichCommand("vendors", mode, vendorPolicy.getMatchField(), vendorPolicy.getEnrichFields()); + } + + protected EsqlQueryResponse runQuery(String query, Boolean ccsMetadataInResponse) { + EsqlQueryRequest request = EsqlQueryRequest.syncEsqlQueryRequest(); + request.query(query); + request.pragmas(AbstractEsqlIntegTestCase.randomPragmas()); + if (randomBoolean()) { + request.profile(true); + } + if (ccsMetadataInResponse != null) { + request.includeCCSMetadata(ccsMetadataInResponse); + } + return client(LOCAL_CLUSTER).execute(EsqlQueryAction.INSTANCE, request).actionGet(30, TimeUnit.SECONDS); + } + + public static Tuple randomIncludeCCSMetadata() { + return switch (randomIntBetween(1, 3)) { + case 1 -> new Tuple<>(Boolean.TRUE, Boolean.TRUE); + case 2 -> new Tuple<>(Boolean.FALSE, Boolean.FALSE); + case 3 -> new Tuple<>(null, Boolean.FALSE); + default -> throw new AssertionError("should not get here"); + }; + } + + public static class LocalStateEnrich extends LocalStateCompositeXPackPlugin { + public LocalStateEnrich(final Settings settings, final Path configPath) throws Exception { + super(settings, configPath); + + plugins.add(new EnrichPlugin(settings) { + @Override + protected XPackLicenseState getLicenseState() { + return this.getLicenseState(); + } + }); + } + + public static class EnrichTransportXPackInfoAction extends TransportXPackInfoAction { + @Inject + public EnrichTransportXPackInfoAction( + TransportService transportService, + ActionFilters actionFilters, + LicenseService licenseService, + NodeClient client + ) { + super(transportService, actionFilters, licenseService, client); + } + + @Override + protected List> infoActions() { + return Collections.singletonList(XPackInfoFeatureAction.ENRICH); + } + } + + @Override + protected Class> getInfoAction() { + return CrossClustersQueriesWithInvalidLicenseIT.LocalStateEnrich.EnrichTransportXPackInfoAction.class; + } + } +} diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterAsyncQueryIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterAsyncQueryIT.java index c8206621de419..a2bba19db50fc 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterAsyncQueryIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterAsyncQueryIT.java @@ -35,7 +35,6 @@ import org.elasticsearch.xpack.core.async.DeleteAsyncResultRequest; import org.elasticsearch.xpack.core.async.GetAsyncResultRequest; import org.elasticsearch.xpack.core.async.TransportDeleteAsyncResultAction; -import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; import org.junit.Before; import java.io.IOException; @@ -78,7 +77,7 @@ protected Map skipUnavailableForRemoteClusters() { @Override protected Collection> nodePlugins(String clusterAlias) { List> plugins = new ArrayList<>(super.nodePlugins(clusterAlias)); - plugins.add(EsqlPlugin.class); + plugins.add(EsqlPluginWithEnterpriseOrTrialLicense.class); plugins.add(EsqlAsyncActionIT.LocalStateEsqlAsync.class); // allows the async_search DELETE action plugins.add(InternalExchangePlugin.class); plugins.add(PauseFieldPlugin.class); diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterEnrichUnavailableClustersIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterEnrichUnavailableClustersIT.java index 5c3e1974e924f..09ad97b08f357 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterEnrichUnavailableClustersIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterEnrichUnavailableClustersIT.java @@ -8,36 +8,21 @@ package org.elasticsearch.xpack.esql.action; import org.elasticsearch.ExceptionsHelper; -import org.elasticsearch.client.internal.Client; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.core.Tuple; -import org.elasticsearch.ingest.common.IngestCommonPlugin; import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.reindex.ReindexPlugin; -import org.elasticsearch.test.AbstractMultiClustersTestCase; import org.elasticsearch.transport.RemoteClusterAware; -import org.elasticsearch.xpack.core.XPackSettings; -import org.elasticsearch.xpack.core.enrich.action.ExecuteEnrichPolicyAction; -import org.elasticsearch.xpack.core.enrich.action.PutEnrichPolicyAction; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.plan.logical.Enrich; -import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; -import org.junit.Before; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.xpack.esql.EsqlTestUtils.getValuesList; -import static org.elasticsearch.xpack.esql.action.CrossClustersEnrichIT.enrichHosts; -import static org.elasticsearch.xpack.esql.action.CrossClustersEnrichIT.enrichVendors; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -47,151 +32,26 @@ * This IT test is the dual of CrossClustersEnrichIT, which tests "happy path" * and this one tests unavailable cluster scenarios using (most of) the same tests. */ -public class CrossClusterEnrichUnavailableClustersIT extends AbstractMultiClustersTestCase { - - public static String REMOTE_CLUSTER_1 = "c1"; - public static String REMOTE_CLUSTER_2 = "c2"; - - @Override - protected List remoteClusterAlias() { - return List.of(REMOTE_CLUSTER_1, REMOTE_CLUSTER_2); - } +public class CrossClusterEnrichUnavailableClustersIT extends AbstractEnrichBasedCrossClusterTestCase { @Override protected boolean reuseClusters() { return false; } - private Collection allClusters() { - return CollectionUtils.appendToCopy(remoteClusterAlias(), LOCAL_CLUSTER); + @Override + protected boolean tolerateErrorsWhenWipingEnrichPolicies() { + // attempt to wipe will fail since some clusters are already closed + return true; } @Override protected Collection> nodePlugins(String clusterAlias) { List> plugins = new ArrayList<>(super.nodePlugins(clusterAlias)); - plugins.add(EsqlPlugin.class); - plugins.add(CrossClustersEnrichIT.LocalStateEnrich.class); - plugins.add(IngestCommonPlugin.class); - plugins.add(ReindexPlugin.class); + plugins.add(EsqlPluginWithEnterpriseOrTrialLicense.class); return plugins; } - @Override - protected Settings nodeSettings() { - return Settings.builder().put(super.nodeSettings()).put(XPackSettings.SECURITY_ENABLED.getKey(), false).build(); - } - - @Before - public void setupHostsEnrich() { - // the hosts policy are identical on every node - Map allHosts = Map.of( - "192.168.1.2", - "Windows", - "192.168.1.3", - "MacOS", - "192.168.1.4", - "Linux", - "192.168.1.5", - "Android", - "192.168.1.6", - "iOS", - "192.168.1.7", - "Windows", - "192.168.1.8", - "MacOS", - "192.168.1.9", - "Linux", - "192.168.1.10", - "Linux", - "192.168.1.11", - "Windows" - ); - for (String cluster : allClusters()) { - Client client = client(cluster); - client.admin().indices().prepareCreate("hosts").setMapping("ip", "type=ip", "os", "type=keyword").get(); - for (Map.Entry h : allHosts.entrySet()) { - client.prepareIndex("hosts").setSource("ip", h.getKey(), "os", h.getValue()).get(); - } - client.admin().indices().prepareRefresh("hosts").get(); - client.execute( - PutEnrichPolicyAction.INSTANCE, - new PutEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, "hosts", CrossClustersEnrichIT.hostPolicy) - ).actionGet(); - client.execute(ExecuteEnrichPolicyAction.INSTANCE, new ExecuteEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, "hosts")) - .actionGet(); - assertAcked(client.admin().indices().prepareDelete("hosts")); - } - } - - @Before - public void setupVendorPolicy() { - var localVendors = Map.of("Windows", "Microsoft", "MacOS", "Apple", "iOS", "Apple", "Android", "Samsung", "Linux", "Redhat"); - var c1Vendors = Map.of("Windows", "Microsoft", "MacOS", "Apple", "iOS", "Apple", "Android", "Google", "Linux", "Suse"); - var c2Vendors = Map.of("Windows", "Microsoft", "MacOS", "Apple", "iOS", "Apple", "Android", "Sony", "Linux", "Ubuntu"); - var vendors = Map.of(LOCAL_CLUSTER, localVendors, "c1", c1Vendors, "c2", c2Vendors); - for (Map.Entry> e : vendors.entrySet()) { - Client client = client(e.getKey()); - client.admin().indices().prepareCreate("vendors").setMapping("os", "type=keyword", "vendor", "type=keyword").get(); - for (Map.Entry v : e.getValue().entrySet()) { - client.prepareIndex("vendors").setSource("os", v.getKey(), "vendor", v.getValue()).get(); - } - client.admin().indices().prepareRefresh("vendors").get(); - client.execute( - PutEnrichPolicyAction.INSTANCE, - new PutEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, "vendors", CrossClustersEnrichIT.vendorPolicy) - ).actionGet(); - client.execute(ExecuteEnrichPolicyAction.INSTANCE, new ExecuteEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, "vendors")) - .actionGet(); - assertAcked(client.admin().indices().prepareDelete("vendors")); - } - } - - @Before - public void setupEventsIndices() { - record Event(long timestamp, String user, String host) {} - - List e0 = List.of( - new Event(1, "matthew", "192.168.1.3"), - new Event(2, "simon", "192.168.1.5"), - new Event(3, "park", "192.168.1.2"), - new Event(4, "andrew", "192.168.1.7"), - new Event(5, "simon", "192.168.1.20"), - new Event(6, "kevin", "192.168.1.2"), - new Event(7, "akio", "192.168.1.5"), - new Event(8, "luke", "192.168.1.2"), - new Event(9, "jack", "192.168.1.4") - ); - List e1 = List.of( - new Event(1, "andres", "192.168.1.2"), - new Event(2, "sergio", "192.168.1.6"), - new Event(3, "kylian", "192.168.1.8"), - new Event(4, "andrew", "192.168.1.9"), - new Event(5, "jack", "192.168.1.3"), - new Event(6, "kevin", "192.168.1.4"), - new Event(7, "akio", "192.168.1.7"), - new Event(8, "kevin", "192.168.1.21"), - new Event(9, "andres", "192.168.1.8") - ); - List e2 = List.of( - new Event(1, "park", "192.168.1.25"), - new Event(2, "akio", "192.168.1.5"), - new Event(3, "park", "192.168.1.2"), - new Event(4, "kevin", "192.168.1.3") - ); - for (var c : Map.of(LOCAL_CLUSTER, e0, "c1", e1, "c2", e2).entrySet()) { - Client client = client(c.getKey()); - client.admin() - .indices() - .prepareCreate("events") - .setMapping("timestamp", "type=long", "user", "type=keyword", "host", "type=ip") - .get(); - for (var e : c.getValue()) { - client.prepareIndex("events").setSource("timestamp", e.timestamp, "user", e.user, "host", e.host).get(); - } - client.admin().indices().prepareRefresh("events").get(); - } - } - public void testEnrichWithHostsPolicyAndDisconnectedRemotesWithSkipUnavailableTrue() throws IOException { setSkipUnavailable(REMOTE_CLUSTER_1, true); setSkipUnavailable(REMOTE_CLUSTER_2, true); @@ -645,19 +505,6 @@ public void testEnrichRemoteWithVendor() throws IOException { } } - protected EsqlQueryResponse runQuery(String query, Boolean ccsMetadataInResponse) { - EsqlQueryRequest request = EsqlQueryRequest.syncEsqlQueryRequest(); - request.query(query); - request.pragmas(AbstractEsqlIntegTestCase.randomPragmas()); - if (randomBoolean()) { - request.profile(true); - } - if (ccsMetadataInResponse != null) { - request.includeCCSMetadata(ccsMetadataInResponse); - } - return client(LOCAL_CLUSTER).execute(EsqlQueryAction.INSTANCE, request).actionGet(30, TimeUnit.SECONDS); - } - private static void assertCCSExecutionInfoDetails(EsqlExecutionInfo executionInfo) { assertThat(executionInfo.overallTook().millis(), greaterThanOrEqualTo(0L)); assertTrue(executionInfo.isCrossClusterSearch()); diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueryUnavailableRemotesIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueryUnavailableRemotesIT.java index d1c9b5cfb2ac7..f65764daafb8a 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueryUnavailableRemotesIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueryUnavailableRemotesIT.java @@ -18,7 +18,6 @@ import org.elasticsearch.test.AbstractMultiClustersTestCase; import org.elasticsearch.test.XContentTestUtils; import org.elasticsearch.xpack.esql.core.type.DataType; -import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; import java.io.IOException; import java.util.ArrayList; @@ -54,8 +53,8 @@ protected boolean reuseClusters() { @Override protected Collection> nodePlugins(String clusterAlias) { List> plugins = new ArrayList<>(super.nodePlugins(clusterAlias)); - plugins.add(EsqlPlugin.class); - plugins.add(org.elasticsearch.xpack.esql.action.CrossClustersQueryIT.InternalExchangePlugin.class); + plugins.add(EsqlPluginWithEnterpriseOrTrialLicense.class); + plugins.add(CrossClustersQueryIT.InternalExchangePlugin.class); return plugins; } diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersCancellationIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersCancellationIT.java index 5291ad3b0d039..17f5f81486651 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersCancellationIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersCancellationIT.java @@ -33,7 +33,6 @@ import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.json.JsonXContent; import org.elasticsearch.xpack.esql.plugin.ComputeService; -import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; import org.junit.Before; import java.util.ArrayList; @@ -62,7 +61,7 @@ protected List remoteClusterAlias() { @Override protected Collection> nodePlugins(String clusterAlias) { List> plugins = new ArrayList<>(super.nodePlugins(clusterAlias)); - plugins.add(EsqlPlugin.class); + plugins.add(EsqlPluginWithEnterpriseOrTrialLicense.class); plugins.add(InternalExchangePlugin.class); plugins.add(PauseFieldPlugin.class); return plugins; diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersEnrichIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersEnrichIT.java index 57f85751999a5..4e6be6cc2bf74 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersEnrichIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersEnrichIT.java @@ -7,218 +7,34 @@ package org.elasticsearch.xpack.esql.action; -import org.elasticsearch.action.ActionType; -import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.TransportAction; -import org.elasticsearch.client.internal.Client; -import org.elasticsearch.client.internal.node.NodeClient; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.core.Tuple; -import org.elasticsearch.ingest.common.IngestCommonPlugin; -import org.elasticsearch.injection.guice.Inject; -import org.elasticsearch.license.LicenseService; -import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.protocol.xpack.XPackInfoRequest; -import org.elasticsearch.protocol.xpack.XPackInfoResponse; -import org.elasticsearch.reindex.ReindexPlugin; -import org.elasticsearch.test.AbstractMultiClustersTestCase; -import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin; -import org.elasticsearch.xpack.core.XPackSettings; -import org.elasticsearch.xpack.core.action.TransportXPackInfoAction; -import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction; -import org.elasticsearch.xpack.core.action.XPackInfoFeatureResponse; -import org.elasticsearch.xpack.core.enrich.EnrichPolicy; -import org.elasticsearch.xpack.core.enrich.action.DeleteEnrichPolicyAction; -import org.elasticsearch.xpack.core.enrich.action.ExecuteEnrichPolicyAction; -import org.elasticsearch.xpack.core.enrich.action.PutEnrichPolicyAction; -import org.elasticsearch.xpack.enrich.EnrichPlugin; -import org.elasticsearch.xpack.esql.EsqlTestUtils; import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.plan.logical.Enrich; -import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; -import org.junit.After; -import org.junit.Before; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.xpack.esql.EsqlTestUtils.getValuesList; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; -public class CrossClustersEnrichIT extends AbstractMultiClustersTestCase { - - @Override - protected List remoteClusterAlias() { - return List.of("c1", "c2"); - } - - protected Collection allClusters() { - return CollectionUtils.appendToCopy(remoteClusterAlias(), LOCAL_CLUSTER); - } +public class CrossClustersEnrichIT extends AbstractEnrichBasedCrossClusterTestCase { @Override protected Collection> nodePlugins(String clusterAlias) { List> plugins = new ArrayList<>(super.nodePlugins(clusterAlias)); - plugins.add(EsqlPlugin.class); - plugins.add(LocalStateEnrich.class); - plugins.add(IngestCommonPlugin.class); - plugins.add(ReindexPlugin.class); + plugins.add(EsqlPluginWithEnterpriseOrTrialLicense.class); return plugins; } - @Override - protected Settings nodeSettings() { - return Settings.builder().put(super.nodeSettings()).put(XPackSettings.SECURITY_ENABLED.getKey(), false).build(); - } - - static final EnrichPolicy hostPolicy = new EnrichPolicy("match", null, List.of("hosts"), "ip", List.of("ip", "os")); - static final EnrichPolicy vendorPolicy = new EnrichPolicy("match", null, List.of("vendors"), "os", List.of("os", "vendor")); - - @Before - public void setupHostsEnrich() { - // the hosts policy are identical on every node - Map allHosts = Map.of( - "192.168.1.2", - "Windows", - "192.168.1.3", - "MacOS", - "192.168.1.4", - "Linux", - "192.168.1.5", - "Android", - "192.168.1.6", - "iOS", - "192.168.1.7", - "Windows", - "192.168.1.8", - "MacOS", - "192.168.1.9", - "Linux", - "192.168.1.10", - "Linux", - "192.168.1.11", - "Windows" - ); - for (String cluster : allClusters()) { - Client client = client(cluster); - client.admin().indices().prepareCreate("hosts").setMapping("ip", "type=ip", "os", "type=keyword").get(); - for (Map.Entry h : allHosts.entrySet()) { - client.prepareIndex("hosts").setSource("ip", h.getKey(), "os", h.getValue()).get(); - } - client.admin().indices().prepareRefresh("hosts").get(); - client.execute(PutEnrichPolicyAction.INSTANCE, new PutEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, "hosts", hostPolicy)) - .actionGet(); - client.execute(ExecuteEnrichPolicyAction.INSTANCE, new ExecuteEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, "hosts")) - .actionGet(); - assertAcked(client.admin().indices().prepareDelete("hosts")); - } - } - - @Before - public void setupVendorPolicy() { - var localVendors = Map.of("Windows", "Microsoft", "MacOS", "Apple", "iOS", "Apple", "Android", "Samsung", "Linux", "Redhat"); - var c1Vendors = Map.of("Windows", "Microsoft", "MacOS", "Apple", "iOS", "Apple", "Android", "Google", "Linux", "Suse"); - var c2Vendors = Map.of("Windows", "Microsoft", "MacOS", "Apple", "iOS", "Apple", "Android", "Sony", "Linux", "Ubuntu"); - var vendors = Map.of(LOCAL_CLUSTER, localVendors, "c1", c1Vendors, "c2", c2Vendors); - for (Map.Entry> e : vendors.entrySet()) { - Client client = client(e.getKey()); - client.admin().indices().prepareCreate("vendors").setMapping("os", "type=keyword", "vendor", "type=keyword").get(); - for (Map.Entry v : e.getValue().entrySet()) { - client.prepareIndex("vendors").setSource("os", v.getKey(), "vendor", v.getValue()).get(); - } - client.admin().indices().prepareRefresh("vendors").get(); - client.execute(PutEnrichPolicyAction.INSTANCE, new PutEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, "vendors", vendorPolicy)) - .actionGet(); - client.execute(ExecuteEnrichPolicyAction.INSTANCE, new ExecuteEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, "vendors")) - .actionGet(); - assertAcked(client.admin().indices().prepareDelete("vendors")); - } - } - - @Before - public void setupEventsIndices() { - record Event(long timestamp, String user, String host) { - - } - List e0 = List.of( - new Event(1, "matthew", "192.168.1.3"), - new Event(2, "simon", "192.168.1.5"), - new Event(3, "park", "192.168.1.2"), - new Event(4, "andrew", "192.168.1.7"), - new Event(5, "simon", "192.168.1.20"), - new Event(6, "kevin", "192.168.1.2"), - new Event(7, "akio", "192.168.1.5"), - new Event(8, "luke", "192.168.1.2"), - new Event(9, "jack", "192.168.1.4") - ); - List e1 = List.of( - new Event(1, "andres", "192.168.1.2"), - new Event(2, "sergio", "192.168.1.6"), - new Event(3, "kylian", "192.168.1.8"), - new Event(4, "andrew", "192.168.1.9"), - new Event(5, "jack", "192.168.1.3"), - new Event(6, "kevin", "192.168.1.4"), - new Event(7, "akio", "192.168.1.7"), - new Event(8, "kevin", "192.168.1.21"), - new Event(9, "andres", "192.168.1.8") - ); - List e2 = List.of( - new Event(1, "park", "192.168.1.25"), - new Event(2, "akio", "192.168.1.5"), - new Event(3, "park", "192.168.1.2"), - new Event(4, "kevin", "192.168.1.3") - ); - for (var c : Map.of(LOCAL_CLUSTER, e0, "c1", e1, "c2", e2).entrySet()) { - Client client = client(c.getKey()); - client.admin() - .indices() - .prepareCreate("events") - .setMapping("timestamp", "type=long", "user", "type=keyword", "host", "type=ip") - .get(); - for (var e : c.getValue()) { - client.prepareIndex("events").setSource("timestamp", e.timestamp, "user", e.user, "host", e.host).get(); - } - client.admin().indices().prepareRefresh("events").get(); - } - } - - @After - public void wipeEnrichPolicies() { - for (String cluster : allClusters()) { - cluster(cluster).wipe(Set.of()); - for (String policy : List.of("hosts", "vendors")) { - client(cluster).execute( - DeleteEnrichPolicyAction.INSTANCE, - new DeleteEnrichPolicyAction.Request(TEST_REQUEST_TIMEOUT, policy) - ); - } - } - } - - static String enrichHosts(Enrich.Mode mode) { - return EsqlTestUtils.randomEnrichCommand("hosts", mode, hostPolicy.getMatchField(), hostPolicy.getEnrichFields()); - } - - static String enrichVendors(Enrich.Mode mode) { - return EsqlTestUtils.randomEnrichCommand("vendors", mode, vendorPolicy.getMatchField(), vendorPolicy.getEnrichFields()); - } - public void testWithHostsPolicy() { for (var mode : Enrich.Mode.values()) { String query = "FROM events | eval ip= TO_STR(host) | " + enrichHosts(mode) + " | stats c = COUNT(*) by os | SORT os"; @@ -606,19 +422,6 @@ public void testEnrichCoordinatorThenEnrichRemote() { ); } - protected EsqlQueryResponse runQuery(String query, Boolean ccsMetadataInResponse) { - EsqlQueryRequest request = EsqlQueryRequest.syncEsqlQueryRequest(); - request.query(query); - request.pragmas(AbstractEsqlIntegTestCase.randomPragmas()); - if (randomBoolean()) { - request.profile(true); - } - if (ccsMetadataInResponse != null) { - request.includeCCSMetadata(ccsMetadataInResponse); - } - return client(LOCAL_CLUSTER).execute(EsqlQueryAction.INSTANCE, request).actionGet(30, TimeUnit.SECONDS); - } - private static void assertCCSExecutionInfoDetails(EsqlExecutionInfo executionInfo) { assertThat(executionInfo.overallTook().millis(), greaterThanOrEqualTo(0L)); assertTrue(executionInfo.isCrossClusterSearch()); @@ -637,49 +440,4 @@ private static void assertCCSExecutionInfoDetails(EsqlExecutionInfo executionInf assertThat(cluster.getFailedShards(), equalTo(0)); } } - - public static Tuple randomIncludeCCSMetadata() { - return switch (randomIntBetween(1, 3)) { - case 1 -> new Tuple<>(Boolean.TRUE, Boolean.TRUE); - case 2 -> new Tuple<>(Boolean.FALSE, Boolean.FALSE); - case 3 -> new Tuple<>(null, Boolean.FALSE); - default -> throw new AssertionError("should not get here"); - }; - } - - public static class LocalStateEnrich extends LocalStateCompositeXPackPlugin { - - public LocalStateEnrich(final Settings settings, final Path configPath) throws Exception { - super(settings, configPath); - - plugins.add(new EnrichPlugin(settings) { - @Override - protected XPackLicenseState getLicenseState() { - return this.getLicenseState(); - } - }); - } - - public static class EnrichTransportXPackInfoAction extends TransportXPackInfoAction { - @Inject - public EnrichTransportXPackInfoAction( - TransportService transportService, - ActionFilters actionFilters, - LicenseService licenseService, - NodeClient client - ) { - super(transportService, actionFilters, licenseService, client); - } - - @Override - protected List> infoActions() { - return Collections.singletonList(XPackInfoFeatureAction.ENRICH); - } - } - - @Override - protected Class> getInfoAction() { - return EnrichTransportXPackInfoAction.class; - } - } } diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersQueriesWithInvalidLicenseIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersQueriesWithInvalidLicenseIT.java new file mode 100644 index 0000000000000..1ed42b696d65e --- /dev/null +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersQueriesWithInvalidLicenseIT.java @@ -0,0 +1,203 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.action; + +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.core.Tuple; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.xpack.esql.plan.logical.Enrich; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import static org.elasticsearch.xpack.esql.EsqlTestUtils.getValuesList; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +public class CrossClustersQueriesWithInvalidLicenseIT extends AbstractEnrichBasedCrossClusterTestCase { + + private static final String LICENSE_ERROR_MESSAGE = "A valid Enterprise license is required to run ES|QL cross-cluster searches."; + + @Override + protected Collection> nodePlugins(String clusterAlias) { + List> plugins = new ArrayList<>(super.nodePlugins(clusterAlias)); + plugins.add(EsqlPluginWithNonEnterpriseOrExpiredLicense.class); // key plugin for the test + return plugins; + } + + public void testBasicCrossClusterQuery() { + Tuple includeCCSMetadata = randomIncludeCCSMetadata(); + Boolean requestIncludeMeta = includeCCSMetadata.v1(); + ElasticsearchStatusException e = expectThrows( + ElasticsearchStatusException.class, + () -> runQuery("FROM *,*:* | LIMIT 5", requestIncludeMeta) + ); + assertThat(e.getMessage(), containsString(LICENSE_ERROR_MESSAGE)); + } + + public void testMetadataCrossClusterQuery() { + Tuple includeCCSMetadata = randomIncludeCCSMetadata(); + Boolean requestIncludeMeta = includeCCSMetadata.v1(); + ElasticsearchStatusException e = expectThrows( + ElasticsearchStatusException.class, + () -> runQuery("FROM events,*:* METADATA _index | SORT _index", requestIncludeMeta) + ); + assertThat(e.getMessage(), containsString(LICENSE_ERROR_MESSAGE)); + } + + public void testQueryAgainstNonMatchingClusterWildcardPattern() { + Tuple includeCCSMetadata = randomIncludeCCSMetadata(); + Boolean requestIncludeMeta = includeCCSMetadata.v1(); + boolean responseExpectMeta = includeCCSMetadata.v2(); + + // since this wildcarded expression does not resolve to a valid remote cluster, it is not considered + // a cross-cluster search and thus should not throw a license error + String q = "FROM xremote*:events"; + { + String limit1 = q + " | STATS count(*)"; + try (EsqlQueryResponse resp = runQuery(limit1, requestIncludeMeta)) { + assertThat(resp.columns().size(), equalTo(1)); + EsqlExecutionInfo executionInfo = resp.getExecutionInfo(); + assertThat(executionInfo.isCrossClusterSearch(), is(false)); + assertThat(executionInfo.includeCCSMetadata(), equalTo(responseExpectMeta)); + } + + String limit0 = q + " | LIMIT 0"; + try (EsqlQueryResponse resp = runQuery(limit0, requestIncludeMeta)) { + assertThat(resp.columns().size(), equalTo(1)); + assertThat(getValuesList(resp).size(), equalTo(0)); + EsqlExecutionInfo executionInfo = resp.getExecutionInfo(); + assertThat(executionInfo.isCrossClusterSearch(), is(false)); + assertThat(executionInfo.includeCCSMetadata(), equalTo(responseExpectMeta)); + } + } + } + + public void testCCSWithLimit0() { + Tuple includeCCSMetadata = randomIncludeCCSMetadata(); + Boolean requestIncludeMeta = includeCCSMetadata.v1(); + + // local only query does not need a valid Enterprise or Trial license + try (EsqlQueryResponse resp = runQuery("FROM events | LIMIT 0", requestIncludeMeta)) { + EsqlExecutionInfo executionInfo = resp.getExecutionInfo(); + assertNotNull(executionInfo); + assertThat(executionInfo.isCrossClusterSearch(), is(false)); + assertThat(executionInfo.overallTook().millis(), greaterThanOrEqualTo(0L)); + } + + // cross-cluster searches should fail with license error + String q = randomFrom("FROM events,c1:* | LIMIT 0", "FROM c1:* | LIMIT 0"); + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> runQuery(q, requestIncludeMeta)); + assertThat(e.getMessage(), containsString(LICENSE_ERROR_MESSAGE)); + } + + public void testSearchesWhereNonExistentClusterIsSpecified() { + Tuple includeCCSMetadata = randomIncludeCCSMetadata(); + Boolean requestIncludeMeta = includeCCSMetadata.v1(); + boolean responseExpectMeta = includeCCSMetadata.v2(); + + // this one query should be allowed since x* does not resolve to any known remote cluster + try (EsqlQueryResponse resp = runQuery("FROM events,x*:no_such_index* | STATS count(*)", requestIncludeMeta)) { + EsqlExecutionInfo executionInfo = resp.getExecutionInfo(); + List> values = getValuesList(resp); + assertThat(values, hasSize(1)); + + assertNotNull(executionInfo); + assertThat(executionInfo.clusterAliases(), equalTo(Set.of(LOCAL_CLUSTER))); + assertThat(executionInfo.isCrossClusterSearch(), is(false)); + assertThat(executionInfo.includeCCSMetadata(), equalTo(responseExpectMeta)); + // since this not a CCS, only the overall took time in the EsqlExecutionInfo matters + assertThat(executionInfo.overallTook().millis(), greaterThanOrEqualTo(0L)); + } + + ElasticsearchStatusException e = expectThrows( + ElasticsearchStatusException.class, + () -> runQuery("FROM events,no_such_cluster:no_such_index* | STATS count(*)", requestIncludeMeta) + ); + // with a valid license this would throw "no such remote cluster" exception, but without a valid license, should get a license error + assertThat(e.getMessage(), containsString(LICENSE_ERROR_MESSAGE)); + } + + public void testEnrichWithHostsPolicy() { + // local-only queries do not need an Enterprise or Trial license + for (var mode : Enrich.Mode.values()) { + String query = "FROM events | eval ip= TO_STR(host) | " + enrichHosts(mode) + " | stats c = COUNT(*) by os | SORT os"; + try (EsqlQueryResponse resp = runQuery(query, null)) { + List> rows = getValuesList(resp); + assertThat( + rows, + equalTo( + List.of( + List.of(2L, "Android"), + List.of(1L, "Linux"), + List.of(1L, "MacOS"), + List.of(4L, "Windows"), + Arrays.asList(1L, (String) null) + ) + ) + ); + assertFalse(resp.getExecutionInfo().isCrossClusterSearch()); + } + } + + // cross-cluster query should fail due to not having valid Enterprise or Trial license + Tuple includeCCSMetadata = randomIncludeCCSMetadata(); + Boolean requestIncludeMeta = includeCCSMetadata.v1(); + + for (var mode : Enrich.Mode.values()) { + String query = "FROM *:events | eval ip= TO_STR(host) | " + enrichHosts(mode) + " | stats c = COUNT(*) by os | SORT os"; + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> runQuery(query, requestIncludeMeta)); + assertThat(e.getMessage(), containsString("A valid Enterprise license is required to run ES|QL cross-cluster searches.")); + } + + for (var mode : Enrich.Mode.values()) { + String query = "FROM *:events,events | eval ip= TO_STR(host) | " + enrichHosts(mode) + " | stats c = COUNT(*) by os | SORT os"; + ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> runQuery(query, requestIncludeMeta)); + assertThat(e.getMessage(), containsString("A valid Enterprise license is required to run ES|QL cross-cluster searches.")); + } + } + + public void testAggThenEnrichRemote() { + String query = String.format(Locale.ROOT, """ + FROM *:events,events + | eval ip= TO_STR(host) + | %s + | stats c = COUNT(*) by os + | %s + | sort vendor + """, enrichHosts(Enrich.Mode.ANY), enrichVendors(Enrich.Mode.REMOTE)); + var error = expectThrows(ElasticsearchStatusException.class, () -> runQuery(query, randomBoolean()).close()); + // with a valid license this would fail with "ENRICH with remote policy can't be executed after STATS", so ensure here + // that the license error is detected first and returned rather than a VerificationException + assertThat(error.getMessage(), containsString(LICENSE_ERROR_MESSAGE)); + } + + public void testEnrichCoordinatorThenEnrichRemote() { + String query = String.format(Locale.ROOT, """ + FROM *:events,events + | eval ip= TO_STR(host) + | %s + | %s + | sort vendor + """, enrichHosts(Enrich.Mode.COORDINATOR), enrichVendors(Enrich.Mode.REMOTE)); + var error = expectThrows(ElasticsearchStatusException.class, () -> runQuery(query, randomBoolean()).close()); + assertThat( + error.getMessage(), + // with a valid license the error is "ENRICH with remote policy can't be executed after another ENRICH with coordinator policy", + // so ensure here that the license error is detected first and returned rather than a VerificationException + containsString(LICENSE_ERROR_MESSAGE) + ); + } +} diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersQueryIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersQueryIT.java index 46bbad5551e6b..347ef419cab9b 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersQueryIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClustersQueryIT.java @@ -32,7 +32,6 @@ import org.elasticsearch.transport.RemoteClusterAware; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.esql.VerificationException; -import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; import org.elasticsearch.xpack.esql.plugin.QueryPragmas; import java.io.IOException; @@ -73,13 +72,13 @@ protected List remoteClusterAlias() { @Override protected Map skipUnavailableForRemoteClusters() { - return Map.of(REMOTE_CLUSTER_1, randomBoolean()); + return Map.of(REMOTE_CLUSTER_1, randomBoolean(), REMOTE_CLUSTER_2, randomBoolean()); } @Override protected Collection> nodePlugins(String clusterAlias) { List> plugins = new ArrayList<>(super.nodePlugins(clusterAlias)); - plugins.add(EsqlPlugin.class); + plugins.add(EsqlPluginWithEnterpriseOrTrialLicense.class); plugins.add(InternalExchangePlugin.class); return plugins; } @@ -184,7 +183,7 @@ public void testSuccessfulPathways() { } public void testSearchesAgainstNonMatchingIndicesWithLocalOnly() { - Map testClusterInfo = setupClusters(2); + Map testClusterInfo = setupTwoClusters(); String localIndex = (String) testClusterInfo.get("local.index"); { @@ -905,7 +904,7 @@ public void testSearchesWhereNonExistentClusterIsSpecifiedWithWildcards() { // cluster-foo* matches nothing and so should not be present in the EsqlExecutionInfo try ( EsqlQueryResponse resp = runQuery( - "from logs-*,no_such_index*,cluster-a:no_such_index*,cluster-foo*:* | stats sum (v)", + "FROM logs-*,no_such_index*,cluster-a:no_such_index*,cluster-foo*:* | STATS sum (v)", requestIncludeMeta ) ) { @@ -1009,7 +1008,7 @@ public void testMetadataIndex() { try ( EsqlQueryResponse resp = runQuery( - "FROM logs*,*:logs* METADATA _index | stats sum(v) by _index | sort _index", + Strings.format("FROM logs*,%s:logs* METADATA _index | stats sum(v) by _index | sort _index", REMOTE_CLUSTER_1), requestIncludeMeta ) ) { @@ -1091,7 +1090,7 @@ public void testProfile() { final int remoteOnlyProfiles; { EsqlQueryRequest request = EsqlQueryRequest.syncEsqlQueryRequest(); - request.query("FROM *:logs* | stats sum(v)"); + request.query("FROM c*:logs* | stats sum(v)"); request.pragmas(pragmas); request.profile(true); try (EsqlQueryResponse resp = runQuery(request)) { @@ -1124,7 +1123,7 @@ public void testProfile() { final int allProfiles; { EsqlQueryRequest request = EsqlQueryRequest.syncEsqlQueryRequest(); - request.query("FROM logs*,*:logs* | stats total = sum(v)"); + request.query("FROM logs*,c*:logs* | stats total = sum(v)"); request.pragmas(pragmas); request.profile(true); try (EsqlQueryResponse resp = runQuery(request)) { @@ -1169,7 +1168,7 @@ public void testWarnings() throws Exception { int remoteNumShards = (Integer) testClusterInfo.get("remote.num_shards"); EsqlQueryRequest request = EsqlQueryRequest.syncEsqlQueryRequest(); - request.query("FROM logs*,*:logs* | EVAL ip = to_ip(id) | STATS total = sum(v) by ip | LIMIT 10"); + request.query("FROM logs*,c*:logs* | EVAL ip = to_ip(id) | STATS total = sum(v) by ip | LIMIT 10"); InternalTestCluster cluster = cluster(LOCAL_CLUSTER); String node = randomFrom(cluster.getNodeNames()); CountDownLatch latch = new CountDownLatch(1); diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithEnterpriseOrTrialLicense.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithEnterpriseOrTrialLicense.java new file mode 100644 index 0000000000000..34d09fc541572 --- /dev/null +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithEnterpriseOrTrialLicense.java @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.action; + +import org.elasticsearch.license.License; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.license.internal.XPackLicenseStatus; +import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; + +import static org.elasticsearch.test.ESTestCase.randomFrom; + +/** + * In IT tests, use this instead of the EsqlPlugin in order to use ES|QL features + * that require an Enteprise (or Trial) license. + */ +public class EsqlPluginWithEnterpriseOrTrialLicense extends EsqlPlugin { + protected XPackLicenseState getLicenseState() { + License.OperationMode operationMode = randomFrom(License.OperationMode.ENTERPRISE, License.OperationMode.TRIAL); + return new XPackLicenseState(() -> System.currentTimeMillis(), new XPackLicenseStatus(operationMode, true, "Test license expired")); + } +} diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithNonEnterpriseOrExpiredLicense.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithNonEnterpriseOrExpiredLicense.java new file mode 100644 index 0000000000000..46c3f3f6204cd --- /dev/null +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlPluginWithNonEnterpriseOrExpiredLicense.java @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.action; + +import org.elasticsearch.license.License; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.license.internal.XPackLicenseStatus; +import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; + +import static org.elasticsearch.test.ESTestCase.randomBoolean; +import static org.elasticsearch.test.ESTestCase.randomFrom; + +/** + * In IT tests, use this instead of the EsqlPlugin in order to test ES|QL features + * using either a: + * - an active (non-expired) basic, standard, missing, gold or platinum Elasticsearch license, OR + * - an expired enterprise or trial license + */ +public class EsqlPluginWithNonEnterpriseOrExpiredLicense extends EsqlPlugin { + protected XPackLicenseState getLicenseState() { + License.OperationMode operationMode; + boolean active; + if (randomBoolean()) { + operationMode = randomFrom( + License.OperationMode.PLATINUM, + License.OperationMode.GOLD, + License.OperationMode.BASIC, + License.OperationMode.MISSING, + License.OperationMode.STANDARD + ); + active = true; + } else { + operationMode = randomFrom(License.OperationMode.ENTERPRISE, License.OperationMode.TRIAL); + active = false; // expired + } + + return new XPackLicenseState( + () -> System.currentTimeMillis(), + new XPackLicenseStatus(operationMode, active, "Test license expired") + ); + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java index 7a733d73941e4..f01cc265e330b 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java @@ -605,6 +605,10 @@ private void gatherMetrics(LogicalPlan plan, BitSet b) { functions.forEach(f -> metrics.incFunctionMetric(f)); } + public XPackLicenseState licenseState() { + return licenseState; + } + /** * Limit QL's comparisons to types we support. This should agree with * {@link EsqlBinaryComparison}'s checkCompatibility method diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlLicenseChecker.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlLicenseChecker.java new file mode 100644 index 0000000000000..0a52ee75de3b2 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlLicenseChecker.java @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.session; + +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.license.License; +import org.elasticsearch.license.LicensedFeature; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestStatus; + +public class EsqlLicenseChecker { + + public static final LicensedFeature.Momentary CCS_FEATURE = LicensedFeature.momentary( + null, + "esql-ccs", + License.OperationMode.ENTERPRISE + ); + + /** + * Only call this method once you know the user is doing a cross-cluster query, as it will update + * the license_usage timestamp for the esql-ccs feature if the license is Enterprise (or Trial). + * @param licenseState + * @return true if the user has a license that allows ESQL CCS. + */ + public static boolean isCcsAllowed(XPackLicenseState licenseState) { + if (licenseState == null) { + return false; + } + return CCS_FEATURE.check(licenseState); + } + + /** + * @param licenseState existing license state. Need to extract info on the current installed license. + * @return ElasticsearchStatusException with an error message informing the caller what license is needed + * to run ES|QL cross-cluster searches and what license (if any) was found. + */ + public static ElasticsearchStatusException invalidLicenseForCcsException(XPackLicenseState licenseState) { + String message = "A valid Enterprise license is required to run ES|QL cross-cluster searches. License found: "; + if (licenseState == null) { + message += "none"; + } else { + message += licenseState.statusDescription(); + } + return new ElasticsearchStatusException(message, RestStatus.BAD_REQUEST); + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java index 4f7c620bc8d12..83480f6651abf 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java @@ -298,6 +298,9 @@ public void analyzedPlan( .map(e -> new EnrichPolicyResolver.UnresolvedPolicy((String) e.policyName().fold(), e.mode())) .collect(Collectors.toSet()); final List indices = preAnalysis.indices; + + EsqlSessionCCSUtils.checkForCcsLicense(indices, indicesExpressionGrouper, verifier.licenseState()); + // TODO: make a separate call for lookup indices final Set targetClusters = enrichPolicyResolver.groupIndicesPerCluster( indices.stream().flatMap(t -> Arrays.stream(Strings.commaDelimitedListToStringArray(t.id().index()))).toArray(String[]::new) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSessionCCSUtils.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSessionCCSUtils.java index 4fe2fef7e3f45..662572c466511 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSessionCCSUtils.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSessionCCSUtils.java @@ -9,17 +9,24 @@ import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.OriginalIndices; import org.elasticsearch.action.fieldcaps.FieldCapabilitiesFailure; import org.elasticsearch.action.search.ShardSearchFailure; +import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.common.Strings; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.TimeValue; +import org.elasticsearch.indices.IndicesExpressionGrouper; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.transport.ConnectTransportException; +import org.elasticsearch.transport.NoSuchRemoteClusterException; import org.elasticsearch.transport.RemoteClusterAware; +import org.elasticsearch.transport.RemoteClusterService; import org.elasticsearch.transport.RemoteTransportException; import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.action.EsqlExecutionInfo; import org.elasticsearch.xpack.esql.analysis.Analyzer; +import org.elasticsearch.xpack.esql.analysis.TableInfo; import org.elasticsearch.xpack.esql.index.IndexResolution; import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan; @@ -255,6 +262,9 @@ static boolean missingIndicesIsFatal(String clusterAlias, EsqlExecutionInfo exec } private static boolean concreteIndexRequested(String indexExpression) { + if (Strings.isNullOrBlank(indexExpression)) { + return false; + } for (String expr : indexExpression.split(",")) { if (expr.charAt(0) == '<' || expr.startsWith("-<")) { // skip date math expressions @@ -288,4 +298,37 @@ static void updateExecutionInfoAtEndOfPlanning(EsqlExecutionInfo execInfo) { } } } + + /** + * Checks the index expression for the presence of remote clusters. If found, it will ensure that the caller + * has a valid Enterprise (or Trial) license on the querying cluster. + * @param indices index expression requested by user + * @param indicesGrouper grouper of index expressions by cluster alias + * @param licenseState license state on the querying cluster + * @throws org.elasticsearch.ElasticsearchStatusException if the license is not valid (or present) for ES|QL CCS search. + */ + public static void checkForCcsLicense( + List indices, + IndicesExpressionGrouper indicesGrouper, + XPackLicenseState licenseState + ) { + for (TableInfo tableInfo : indices) { + Map groupedIndices; + try { + groupedIndices = indicesGrouper.groupIndices(IndicesOptions.DEFAULT, tableInfo.id().index()); + } catch (NoSuchRemoteClusterException e) { + if (EsqlLicenseChecker.isCcsAllowed(licenseState)) { + throw e; + } else { + throw EsqlLicenseChecker.invalidLicenseForCcsException(licenseState); + } + } + // check if it is a cross-cluster query + if (groupedIndices.size() > 1 || groupedIndices.containsKey(RemoteClusterService.LOCAL_CLUSTER_GROUP_KEY) == false) { + if (EsqlLicenseChecker.isCcsAllowed(licenseState) == false) { + throw EsqlLicenseChecker.invalidLicenseForCcsException(licenseState); + } + } + } + } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/EsqlSessionCCSUtilsTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/EsqlSessionCCSUtilsTests.java index 60b632c443f8e..1000c05282fdb 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/EsqlSessionCCSUtilsTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/EsqlSessionCCSUtilsTests.java @@ -8,10 +8,18 @@ package org.elasticsearch.xpack.esql.session; import org.apache.lucene.index.CorruptIndexException; +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.action.OriginalIndices; import org.elasticsearch.action.fieldcaps.FieldCapabilitiesFailure; import org.elasticsearch.action.search.ShardSearchFailure; +import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.common.Strings; import org.elasticsearch.index.IndexMode; +import org.elasticsearch.indices.IndicesExpressionGrouper; +import org.elasticsearch.license.License; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.license.internal.XPackLicenseStatus; +import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.ConnectTransportException; import org.elasticsearch.transport.NoSeedNodeLeftException; @@ -20,9 +28,11 @@ import org.elasticsearch.transport.RemoteTransportException; import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.action.EsqlExecutionInfo; +import org.elasticsearch.xpack.esql.analysis.TableInfo; import org.elasticsearch.xpack.esql.core.type.EsField; import org.elasticsearch.xpack.esql.index.EsIndex; import org.elasticsearch.xpack.esql.index.IndexResolution; +import org.elasticsearch.xpack.esql.plan.TableIdentifier; import org.elasticsearch.xpack.esql.type.EsFieldTests; import java.util.ArrayList; @@ -32,8 +42,12 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.LongSupplier; import java.util.function.Predicate; +import java.util.stream.Collectors; +import static org.elasticsearch.xpack.esql.core.tree.Source.EMPTY; +import static org.elasticsearch.xpack.esql.session.EsqlSessionCCSUtils.checkForCcsLicense; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -627,4 +641,148 @@ public void testMissingIndicesIsFatal() { } } + + public void testCheckForCcsLicense() { + final TestIndicesExpressionGrouper indicesGrouper = new TestIndicesExpressionGrouper(); + + // this seems to be used only for tracking usage of features, not for checking if a license is expired + final LongSupplier currTime = () -> System.currentTimeMillis(); + + XPackLicenseState enterpriseLicenseValid = new XPackLicenseState(currTime, activeLicenseStatus(License.OperationMode.ENTERPRISE)); + XPackLicenseState trialLicenseValid = new XPackLicenseState(currTime, activeLicenseStatus(License.OperationMode.TRIAL)); + XPackLicenseState platinumLicenseValid = new XPackLicenseState(currTime, activeLicenseStatus(License.OperationMode.PLATINUM)); + XPackLicenseState goldLicenseValid = new XPackLicenseState(currTime, activeLicenseStatus(License.OperationMode.GOLD)); + XPackLicenseState basicLicenseValid = new XPackLicenseState(currTime, activeLicenseStatus(License.OperationMode.BASIC)); + XPackLicenseState standardLicenseValid = new XPackLicenseState(currTime, activeLicenseStatus(License.OperationMode.STANDARD)); + XPackLicenseState missingLicense = new XPackLicenseState(currTime, activeLicenseStatus(License.OperationMode.MISSING)); + XPackLicenseState nullLicense = null; + + final XPackLicenseStatus enterpriseStatus = inactiveLicenseStatus(License.OperationMode.ENTERPRISE); + XPackLicenseState enterpriseLicenseInactive = new XPackLicenseState(currTime, enterpriseStatus); + XPackLicenseState trialLicenseInactive = new XPackLicenseState(currTime, inactiveLicenseStatus(License.OperationMode.TRIAL)); + XPackLicenseState platinumLicenseInactive = new XPackLicenseState(currTime, inactiveLicenseStatus(License.OperationMode.PLATINUM)); + XPackLicenseState goldLicenseInactive = new XPackLicenseState(currTime, inactiveLicenseStatus(License.OperationMode.GOLD)); + XPackLicenseState basicLicenseInactive = new XPackLicenseState(currTime, inactiveLicenseStatus(License.OperationMode.BASIC)); + XPackLicenseState standardLicenseInactive = new XPackLicenseState(currTime, inactiveLicenseStatus(License.OperationMode.STANDARD)); + XPackLicenseState missingLicenseInactive = new XPackLicenseState(currTime, inactiveLicenseStatus(License.OperationMode.MISSING)); + + // local only search does not require an enterprise license + { + List indices = new ArrayList<>(); + indices.add(new TableInfo(new TableIdentifier(EMPTY, null, randomFrom("idx", "idx1,idx2*")))); + + checkForCcsLicense(indices, indicesGrouper, enterpriseLicenseValid); + checkForCcsLicense(indices, indicesGrouper, platinumLicenseValid); + checkForCcsLicense(indices, indicesGrouper, goldLicenseValid); + checkForCcsLicense(indices, indicesGrouper, trialLicenseValid); + checkForCcsLicense(indices, indicesGrouper, basicLicenseValid); + checkForCcsLicense(indices, indicesGrouper, standardLicenseValid); + checkForCcsLicense(indices, indicesGrouper, missingLicense); + checkForCcsLicense(indices, indicesGrouper, nullLicense); + + checkForCcsLicense(indices, indicesGrouper, enterpriseLicenseInactive); + checkForCcsLicense(indices, indicesGrouper, platinumLicenseInactive); + checkForCcsLicense(indices, indicesGrouper, goldLicenseInactive); + checkForCcsLicense(indices, indicesGrouper, trialLicenseInactive); + checkForCcsLicense(indices, indicesGrouper, basicLicenseInactive); + checkForCcsLicense(indices, indicesGrouper, standardLicenseInactive); + checkForCcsLicense(indices, indicesGrouper, missingLicenseInactive); + } + + // cross-cluster search requires a valid (active, non-expired) enterprise license OR a valid trial license + { + List indices = new ArrayList<>(); + final String indexExprWithRemotes = randomFrom("remote:idx", "idx1,remote:idx2*,remote:logs,c*:idx4"); + if (randomBoolean()) { + indices.add(new TableInfo(new TableIdentifier(EMPTY, null, indexExprWithRemotes))); + } else { + indices.add(new TableInfo(new TableIdentifier(EMPTY, null, randomFrom("idx", "idx1,idx2*")))); + indices.add(new TableInfo(new TableIdentifier(EMPTY, null, indexExprWithRemotes))); + } + + // licenses that work + checkForCcsLicense(indices, indicesGrouper, enterpriseLicenseValid); + checkForCcsLicense(indices, indicesGrouper, trialLicenseValid); + + // all others fail --- + + // active non-expired non-Enterprise non-Trial licenses + assertLicenseCheckFails(indices, indicesGrouper, platinumLicenseValid, "active platinum license"); + assertLicenseCheckFails(indices, indicesGrouper, goldLicenseValid, "active gold license"); + assertLicenseCheckFails(indices, indicesGrouper, basicLicenseValid, "active basic license"); + assertLicenseCheckFails(indices, indicesGrouper, standardLicenseValid, "active standard license"); + assertLicenseCheckFails(indices, indicesGrouper, missingLicense, "active missing license"); + assertLicenseCheckFails(indices, indicesGrouper, nullLicense, "none"); + + // inactive/expired licenses + assertLicenseCheckFails(indices, indicesGrouper, enterpriseLicenseInactive, "expired enterprise license"); + assertLicenseCheckFails(indices, indicesGrouper, trialLicenseInactive, "expired trial license"); + assertLicenseCheckFails(indices, indicesGrouper, platinumLicenseInactive, "expired platinum license"); + assertLicenseCheckFails(indices, indicesGrouper, goldLicenseInactive, "expired gold license"); + assertLicenseCheckFails(indices, indicesGrouper, basicLicenseInactive, "expired basic license"); + assertLicenseCheckFails(indices, indicesGrouper, standardLicenseInactive, "expired standard license"); + assertLicenseCheckFails(indices, indicesGrouper, missingLicenseInactive, "expired missing license"); + } + } + + private XPackLicenseStatus activeLicenseStatus(License.OperationMode operationMode) { + return new XPackLicenseStatus(operationMode, true, null); + } + + private XPackLicenseStatus inactiveLicenseStatus(License.OperationMode operationMode) { + return new XPackLicenseStatus(operationMode, false, "License Expired 123"); + } + + private void assertLicenseCheckFails( + List indices, + TestIndicesExpressionGrouper indicesGrouper, + XPackLicenseState licenseState, + String expectedErrorMessageSuffix + ) { + ElasticsearchStatusException e = expectThrows( + ElasticsearchStatusException.class, + () -> checkForCcsLicense(indices, indicesGrouper, licenseState) + ); + assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST)); + assertThat( + e.getMessage(), + equalTo( + "A valid Enterprise license is required to run ES|QL cross-cluster searches. License found: " + expectedErrorMessageSuffix + ) + ); + } + + static class TestIndicesExpressionGrouper implements IndicesExpressionGrouper { + @Override + public Map groupIndices(IndicesOptions indicesOptions, String[] indexExpressions) { + final Map originalIndicesMap = new HashMap<>(); + final String localKey = RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY; + + for (String expr : indexExpressions) { + assertFalse(Strings.isNullOrBlank(expr)); + String[] split = expr.split(":", 2); + assertTrue("Bad index expression: " + expr, split.length < 3); + String clusterAlias; + String indexExpr; + if (split.length == 1) { + clusterAlias = localKey; + indexExpr = expr; + } else { + clusterAlias = split[0]; + indexExpr = split[1]; + + } + OriginalIndices currIndices = originalIndicesMap.get(clusterAlias); + if (currIndices == null) { + originalIndicesMap.put(clusterAlias, new OriginalIndices(new String[] { indexExpr }, indicesOptions)); + } else { + List indicesList = Arrays.stream(currIndices.indices()).collect(Collectors.toList()); + indicesList.add(indexExpr); + originalIndicesMap.put(clusterAlias, new OriginalIndices(indicesList.toArray(new String[0]), indicesOptions)); + } + } + return originalIndicesMap; + } + } + } diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java index 09449f81121fd..d6bad85161fd9 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.remotecluster; +import org.apache.http.client.methods.HttpGet; import org.elasticsearch.Build; import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; @@ -22,6 +23,7 @@ import org.elasticsearch.test.cluster.ElasticsearchCluster; import org.elasticsearch.test.cluster.util.resource.Resource; import org.elasticsearch.test.junit.RunnableTestRuleAdapter; +import org.elasticsearch.test.rest.ObjectPath; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.json.JsonXContent; import org.junit.After; @@ -34,6 +36,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -51,6 +54,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.not; public class RemoteClusterSecurityEsqlIT extends AbstractRemoteClusterSecurityTestCase { private static final AtomicReference> API_KEY_MAP_REF = new AtomicReference<>(); @@ -342,6 +346,14 @@ public void testCrossClusterQuery() throws Exception { configureRemoteCluster(); populateData(); + Map esqlCcsLicenseFeatureUsage = fetchEsqlCcsFeatureUsageFromNode(client()); + + Object ccsLastUsedTimestampAtStartOfTest = null; + if (esqlCcsLicenseFeatureUsage.isEmpty() == false) { + // some test runs will have a usage value already, so capture that to compare at end of test + ccsLastUsedTimestampAtStartOfTest = esqlCcsLicenseFeatureUsage.get("last_used"); + } + // query remote cluster only Request request = esqlRequest(""" FROM my_remote_cluster:employees @@ -385,6 +397,15 @@ public void testCrossClusterQuery() throws Exception { | LIMIT 2 | KEEP emp_id, department""")); assertRemoteOnlyAgainst2IndexResults(response); + + // check that the esql-ccs license feature is now present and that the last_used field has been updated + esqlCcsLicenseFeatureUsage = fetchEsqlCcsFeatureUsageFromNode(client()); + assertThat(esqlCcsLicenseFeatureUsage.size(), equalTo(5)); + Object lastUsed = esqlCcsLicenseFeatureUsage.get("last_used"); + assertNotNull("lastUsed should not be null", lastUsed); + if (ccsLastUsedTimestampAtStartOfTest != null) { + assertThat(lastUsed.toString(), not(equalTo(ccsLastUsedTimestampAtStartOfTest.toString()))); + } } @SuppressWarnings("unchecked") @@ -1660,4 +1681,18 @@ void assertExpectedClustersForMissingIndicesTests(Map responseMa assertThat((int) shards.get("failed"), is(0)); } } + + private static Map fetchEsqlCcsFeatureUsageFromNode(RestClient client) throws IOException { + Request request = new Request(HttpGet.METHOD_NAME, "_license/feature_usage"); + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", basicAuthHeaderValue(USER, PASS))); + Response response = client.performRequest(request); + ObjectPath path = ObjectPath.createFromResponse(response); + List> features = path.evaluate("features"); + for (var feature : features) { + if ("esql-ccs".equals(feature.get("name"))) { + return feature; + } + } + return Collections.emptyMap(); + } } diff --git a/x-pack/qa/multi-cluster-search-security/legacy-with-basic-license/src/test/resources/rest-api-spec/test/querying_cluster/80_esql.yml b/x-pack/qa/multi-cluster-search-security/legacy-with-basic-license/src/test/resources/rest-api-spec/test/querying_cluster/80_esql.yml index 4c0bbfd7ec139..1b435c551fbe9 100644 --- a/x-pack/qa/multi-cluster-search-security/legacy-with-basic-license/src/test/resources/rest-api-spec/test/querying_cluster/80_esql.yml +++ b/x-pack/qa/multi-cluster-search-security/legacy-with-basic-license/src/test/resources/rest-api-spec/test/querying_cluster/80_esql.yml @@ -86,11 +86,12 @@ teardown: ignore: 404 --- -"Index data and search on the mixed cluster": +"ES|QL cross-cluster query fails with basic license": - skip: features: allowed_warnings - do: + catch: bad_request allowed_warnings: - "Line 1:21: Square brackets '[]' need to be removed in FROM METADATA declaration" headers: { Authorization: "Basic am9lOnMza3JpdC1wYXNzd29yZA==" } @@ -98,23 +99,11 @@ teardown: body: query: 'FROM *:esql*,esql_* | STATS total = sum(cost) by tag | SORT tag | LIMIT 10' - - match: {columns.0.name: "total"} - - match: {columns.0.type: "long"} - - match: {columns.1.name: "tag"} - - match: {columns.1.type: "keyword"} - - - match: {values.0.0: 2200} - - match: {values.0.1: "computer"} - - match: {values.1.0: 170} - - match: {values.1.1: "headphone"} - - match: {values.2.0: 2100 } - - match: {values.2.1: "laptop" } - - match: {values.3.0: 1000 } - - match: {values.3.1: "monitor" } - - match: {values.4.0: 550 } - - match: {values.4.1: "tablet" } + - match: { error.type: "status_exception" } + - match: { error.reason: "A valid Enterprise license is required to run ES|QL cross-cluster searches. License found: active basic license" } - do: + catch: bad_request allowed_warnings: - "Line 1:21: Square brackets '[]' need to be removed in FROM METADATA declaration" headers: { Authorization: "Basic am9lOnMza3JpdC1wYXNzd29yZA==" } @@ -128,28 +117,11 @@ teardown: lte: "2023-01-03" format: "yyyy-MM-dd" - - match: {columns.0.name: "_index"} - - match: {columns.0.type: "keyword"} - - match: {columns.1.name: "tag"} - - match: {columns.1.type: "keyword"} - - match: {columns.2.name: "cost" } - - match: {columns.2.type: "long" } - - - match: {values.0.0: "esql_local"} - - match: {values.0.1: "monitor"} - - match: {values.0.2: 250 } - - match: {values.1.0: "my_remote_cluster:esql_index" } - - match: {values.1.1: "tablet"} - - match: {values.1.2: 450 } - - match: {values.2.0: "my_remote_cluster:esql_index" } - - match: {values.2.1: "computer" } - - match: {values.2.2: 1200 } - - match: {values.3.0: "esql_local"} - - match: {values.3.1: "laptop" } - - match: {values.3.2: 2100 } + - match: { error.type: "status_exception" } + - match: { error.reason: "A valid Enterprise license is required to run ES|QL cross-cluster searches. License found: active basic license" } --- -"Enrich across clusters": +"ES|QL enrich query across clusters fails with basic license": - requires: cluster_features: ["gte_v8.13.0"] reason: "Enrich across clusters available in 8.13 or later" @@ -194,27 +166,14 @@ teardown: index: suggestions - do: + catch: bad_request headers: { Authorization: "Basic am9lOnMza3JpdC1wYXNzd29yZA==" } esql.query: body: query: 'FROM *:esql*,esql_* | STATS total = sum(cost) by tag | SORT total DESC | LIMIT 3 | ENRICH suggestions | KEEP tag, total, phrase' - - match: {columns.0.name: "tag"} - - match: {columns.0.type: "keyword"} - - match: {columns.1.name: "total" } - - match: {columns.1.type: "long" } - - match: {columns.2.name: "phrase" } - - match: {columns.2.type: "keyword" } - - - match: {values.0.0: "computer"} - - match: {values.0.1: 2200} - - match: {values.0.2: "best desktop for programming"} - - match: {values.1.0: "laptop"} - - match: {values.1.1: 2100 } - - match: {values.1.2: "the best battery life laptop"} - - match: {values.2.0: "monitor" } - - match: {values.2.1: 1000 } - - match: {values.2.2: "4k or 5k or 6K monitor?" } + - match: { error.type: "status_exception" } + - match: { error.reason: "A valid Enterprise license is required to run ES|QL cross-cluster searches. License found: active basic license" } - do: enrich.delete_policy: From 9503afc34872554b803b0b46e9444aeaa458e514 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Thu, 12 Dec 2024 18:08:33 +0100 Subject: [PATCH 62/77] [Build] Cache spotless p2 dependencies when baking ci image (#118523) The eclipse formatter used by spotless is resolved at runtime and not declared as gradle dependency. Therefore we need to run the spotless task to ensure we have the dependencies resolved as part of our ci image baking. This should avoid issues with connecting to p2 repos we have experienced lately in our ci environment * Revert "[Build] Declare mirror for eclipse p2 repository (#117732)" This reverts commit c35777a175f10a49ae860d28aa16b40d6f66c49a. --- .../conventions/precommit/FormattingPrecommitPlugin.java | 5 +---- build.gradle | 5 ++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build-conventions/src/main/java/org/elasticsearch/gradle/internal/conventions/precommit/FormattingPrecommitPlugin.java b/build-conventions/src/main/java/org/elasticsearch/gradle/internal/conventions/precommit/FormattingPrecommitPlugin.java index 41c0b4d67e1df..ea9009172c7e2 100644 --- a/build-conventions/src/main/java/org/elasticsearch/gradle/internal/conventions/precommit/FormattingPrecommitPlugin.java +++ b/build-conventions/src/main/java/org/elasticsearch/gradle/internal/conventions/precommit/FormattingPrecommitPlugin.java @@ -17,8 +17,6 @@ import org.gradle.api.Project; import java.io.File; -import java.util.Arrays; -import java.util.Map; /** * This plugin configures formatting for Java source using Spotless @@ -66,8 +64,7 @@ public void apply(Project project) { java.importOrderFile(new File(elasticsearchWorkspace, importOrderPath)); // Most formatting is done through the Eclipse formatter - java.eclipse().withP2Mirrors(Map.of("https://download.eclipse.org/", "https://mirror.umd.edu/eclipse/")) - .configFile(new File(elasticsearchWorkspace, formatterConfigPath)); + java.eclipse().configFile(new File(elasticsearchWorkspace, formatterConfigPath)); // Ensure blank lines are actually empty. Since formatters are applied in // order, apply this one last, otherwise non-empty blank lines can creep diff --git a/build.gradle b/build.gradle index 715614c1beea4..b95e34640cb5f 100644 --- a/build.gradle +++ b/build.gradle @@ -301,7 +301,10 @@ allprojects { if (project.path.contains(":distribution:docker")) { enabled = false } - + if (project.path.contains(":libs:cli")) { + // ensure we resolve p2 dependencies for the spotless eclipse formatter + dependsOn "spotlessJavaCheck" + } } plugins.withId('lifecycle-base') { From ce0e4e4fa8ebf6b3a2d379aa937fd0cde521439b Mon Sep 17 00:00:00 2001 From: John Wagster Date: Thu, 12 Dec 2024 13:17:32 -0600 Subject: [PATCH 63/77] small doc fix for updates related to _source defaults found by community member (#118605) --- docs/reference/docs/update.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/docs/update.asciidoc b/docs/reference/docs/update.asciidoc index ca6a7e489449b..a212c4e152b0e 100644 --- a/docs/reference/docs/update.asciidoc +++ b/docs/reference/docs/update.asciidoc @@ -71,7 +71,7 @@ include::{es-ref-dir}/rest-api/common-parms.asciidoc[tag=refresh] include::{es-ref-dir}/rest-api/common-parms.asciidoc[tag=routing] `_source`:: -(Optional, list) Set to `false` to disable source retrieval (default: `true`). +(Optional, list) Set to `true` to enable source retrieval (default: `false`). You can also specify a comma-separated list of the fields you want to retrieve. `_source_excludes`:: From 2f1e1f8632cdfbdb942123039b5eaeaf2e6bfa5b Mon Sep 17 00:00:00 2001 From: Mark Vieira Date: Thu, 12 Dec 2024 11:18:41 -0800 Subject: [PATCH 64/77] Remove version 8.15.6 --- .buildkite/pipelines/intake.yml | 2 +- .buildkite/pipelines/periodic-packaging.yml | 6 +++--- .buildkite/pipelines/periodic.yml | 10 +++++----- .ci/bwcVersions | 2 +- .ci/snapshotBwcVersions | 1 - server/src/main/java/org/elasticsearch/Version.java | 2 +- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.buildkite/pipelines/intake.yml b/.buildkite/pipelines/intake.yml index 4bc72aec20972..6c8b8edfcbac1 100644 --- a/.buildkite/pipelines/intake.yml +++ b/.buildkite/pipelines/intake.yml @@ -56,7 +56,7 @@ steps: timeout_in_minutes: 300 matrix: setup: - BWC_VERSION: ["8.15.6", "8.16.2", "8.17.0", "8.18.0", "9.0.0"] + BWC_VERSION: ["8.16.2", "8.17.0", "8.18.0", "9.0.0"] agents: provider: gcp image: family/elasticsearch-ubuntu-2004 diff --git a/.buildkite/pipelines/periodic-packaging.yml b/.buildkite/pipelines/periodic-packaging.yml index c58201258fbbf..2fbcd075b9719 100644 --- a/.buildkite/pipelines/periodic-packaging.yml +++ b/.buildkite/pipelines/periodic-packaging.yml @@ -269,8 +269,8 @@ steps: env: BWC_VERSION: 8.14.3 - - label: "{{matrix.image}} / 8.15.6 / packaging-tests-upgrade" - command: ./.ci/scripts/packaging-test.sh -Dbwc.checkout.align=true destructiveDistroUpgradeTest.v8.15.6 + - label: "{{matrix.image}} / 8.15.5 / packaging-tests-upgrade" + command: ./.ci/scripts/packaging-test.sh -Dbwc.checkout.align=true destructiveDistroUpgradeTest.v8.15.5 timeout_in_minutes: 300 matrix: setup: @@ -283,7 +283,7 @@ steps: machineType: custom-16-32768 buildDirectory: /dev/shm/bk env: - BWC_VERSION: 8.15.6 + BWC_VERSION: 8.15.5 - label: "{{matrix.image}} / 8.16.2 / packaging-tests-upgrade" command: ./.ci/scripts/packaging-test.sh -Dbwc.checkout.align=true destructiveDistroUpgradeTest.v8.16.2 diff --git a/.buildkite/pipelines/periodic.yml b/.buildkite/pipelines/periodic.yml index 3d6095d0b9e63..94c9020a794a2 100644 --- a/.buildkite/pipelines/periodic.yml +++ b/.buildkite/pipelines/periodic.yml @@ -287,8 +287,8 @@ steps: - signal_reason: agent_stop limit: 3 - - label: 8.15.6 / bwc - command: .ci/scripts/run-gradle.sh -Dbwc.checkout.align=true v8.15.6#bwcTest + - label: 8.15.5 / bwc + command: .ci/scripts/run-gradle.sh -Dbwc.checkout.align=true v8.15.5#bwcTest timeout_in_minutes: 300 agents: provider: gcp @@ -297,7 +297,7 @@ steps: buildDirectory: /dev/shm/bk preemptible: true env: - BWC_VERSION: 8.15.6 + BWC_VERSION: 8.15.5 retry: automatic: - exit_status: "-1" @@ -448,7 +448,7 @@ steps: setup: ES_RUNTIME_JAVA: - openjdk21 - BWC_VERSION: ["8.15.6", "8.16.2", "8.17.0", "8.18.0", "9.0.0"] + BWC_VERSION: ["8.16.2", "8.17.0", "8.18.0", "9.0.0"] agents: provider: gcp image: family/elasticsearch-ubuntu-2004 @@ -490,7 +490,7 @@ steps: ES_RUNTIME_JAVA: - openjdk21 - openjdk23 - BWC_VERSION: ["8.15.6", "8.16.2", "8.17.0", "8.18.0", "9.0.0"] + BWC_VERSION: ["8.16.2", "8.17.0", "8.18.0", "9.0.0"] agents: provider: gcp image: family/elasticsearch-ubuntu-2004 diff --git a/.ci/bwcVersions b/.ci/bwcVersions index 826091807ce57..79de891452117 100644 --- a/.ci/bwcVersions +++ b/.ci/bwcVersions @@ -14,7 +14,7 @@ BWC_VERSION: - "8.12.2" - "8.13.4" - "8.14.3" - - "8.15.6" + - "8.15.5" - "8.16.2" - "8.17.0" - "8.18.0" diff --git a/.ci/snapshotBwcVersions b/.ci/snapshotBwcVersions index f92881da7fea4..5514fc376a285 100644 --- a/.ci/snapshotBwcVersions +++ b/.ci/snapshotBwcVersions @@ -1,5 +1,4 @@ BWC_VERSION: - - "8.15.6" - "8.16.2" - "8.17.0" - "8.18.0" diff --git a/server/src/main/java/org/elasticsearch/Version.java b/server/src/main/java/org/elasticsearch/Version.java index 24aa5bd261d7e..f03505de310d5 100644 --- a/server/src/main/java/org/elasticsearch/Version.java +++ b/server/src/main/java/org/elasticsearch/Version.java @@ -187,7 +187,7 @@ public class Version implements VersionId, ToXContentFragment { public static final Version V_8_15_2 = new Version(8_15_02_99); public static final Version V_8_15_3 = new Version(8_15_03_99); public static final Version V_8_15_4 = new Version(8_15_04_99); - public static final Version V_8_15_6 = new Version(8_15_06_99); + public static final Version V_8_15_5 = new Version(8_15_05_99); public static final Version V_8_16_0 = new Version(8_16_00_99); public static final Version V_8_16_1 = new Version(8_16_01_99); public static final Version V_8_16_2 = new Version(8_16_02_99); From 59690f5e67c9c019275d030a641e19198f777ae3 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 12 Dec 2024 20:30:57 +0000 Subject: [PATCH 65/77] Add integ test for EC2 special network addresses (#118560) Replaces the `Ec2NetworkTests` unit test suite with an integ test suite to cover the resolution process end-to-end. --- .../RepositoryS3ImdsV1CredentialsRestIT.java | 2 +- .../RepositoryS3ImdsV2CredentialsRestIT.java | 2 +- plugins/discovery-ec2/build.gradle | 5 +- .../DiscoveryEc2NetworkAddressesTestCase.java | 42 ++++ ...DiscoveryEc2RegularNetworkAddressesIT.java | 50 +++++ ...DiscoveryEc2SpecialNetworkAddressesIT.java | 77 ++++++++ ...yEc2AvailabilityZoneAttributeTestCase.java | 4 +- .../discovery/ec2/Ec2NetworkTests.java | 181 ------------------ .../fixture/aws/imds/Ec2ImdsHttpFixture.java | 37 ++++ .../fixture/aws/imds/Ec2ImdsHttpHandler.java | 26 ++- .../aws/imds/Ec2ImdsServiceBuilder.java | 16 +- 11 files changed, 247 insertions(+), 195 deletions(-) create mode 100644 plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2NetworkAddressesTestCase.java create mode 100644 plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2RegularNetworkAddressesIT.java create mode 100644 plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2SpecialNetworkAddressesIT.java delete mode 100644 plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2NetworkTests.java diff --git a/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV1CredentialsRestIT.java b/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV1CredentialsRestIT.java index dcdf52e963eef..bc41b9fd62ca9 100644 --- a/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV1CredentialsRestIT.java +++ b/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV1CredentialsRestIT.java @@ -44,7 +44,7 @@ public class RepositoryS3ImdsV1CredentialsRestIT extends AbstractRepositoryS3Res public static ElasticsearchCluster cluster = ElasticsearchCluster.local() .module("repository-s3") .setting("s3.client." + CLIENT + ".endpoint", s3Fixture::getAddress) - .systemProperty("com.amazonaws.sdk.ec2MetadataServiceEndpointOverride", ec2ImdsHttpFixture::getAddress) + .systemProperty(Ec2ImdsHttpFixture.ENDPOINT_OVERRIDE_SYSPROP_NAME, ec2ImdsHttpFixture::getAddress) .build(); @ClassRule diff --git a/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV2CredentialsRestIT.java b/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV2CredentialsRestIT.java index 434fc9720fc29..34500ff5227f1 100644 --- a/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV2CredentialsRestIT.java +++ b/modules/repository-s3/src/javaRestTest/java/org/elasticsearch/repositories/s3/RepositoryS3ImdsV2CredentialsRestIT.java @@ -44,7 +44,7 @@ public class RepositoryS3ImdsV2CredentialsRestIT extends AbstractRepositoryS3Res public static ElasticsearchCluster cluster = ElasticsearchCluster.local() .module("repository-s3") .setting("s3.client." + CLIENT + ".endpoint", s3Fixture::getAddress) - .systemProperty("com.amazonaws.sdk.ec2MetadataServiceEndpointOverride", ec2ImdsHttpFixture::getAddress) + .systemProperty(Ec2ImdsHttpFixture.ENDPOINT_OVERRIDE_SYSPROP_NAME, ec2ImdsHttpFixture::getAddress) .build(); @ClassRule diff --git a/plugins/discovery-ec2/build.gradle b/plugins/discovery-ec2/build.gradle index d4b56015edaa8..95715217fa59a 100644 --- a/plugins/discovery-ec2/build.gradle +++ b/plugins/discovery-ec2/build.gradle @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ apply plugin: 'elasticsearch.internal-java-rest-test' +apply plugin: 'elasticsearch.internal-cluster-test' esplugin { description 'The EC2 discovery plugin allows to use AWS API for the unicast discovery mechanism.' @@ -29,6 +30,8 @@ dependencies { javaRestTestImplementation project(':plugins:discovery-ec2') javaRestTestImplementation project(':test:fixtures:ec2-imds-fixture') + + internalClusterTestImplementation project(':test:fixtures:ec2-imds-fixture') } tasks.named("dependencyLicenses").configure { @@ -82,7 +85,7 @@ tasks.register("writeTestJavaPolicy") { } } -tasks.named("test").configure { +tasks.withType(Test).configureEach { dependsOn "writeTestJavaPolicy" // this is needed for insecure plugins, remove if possible! systemProperty 'tests.artifact', project.name diff --git a/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2NetworkAddressesTestCase.java b/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2NetworkAddressesTestCase.java new file mode 100644 index 0000000000000..b6a4845977b09 --- /dev/null +++ b/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2NetworkAddressesTestCase.java @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.discovery.ec2; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.http.HttpServerTransport; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESIntegTestCase; + +import java.io.IOException; +import java.util.Collection; + +@ESIntegTestCase.ClusterScope(numDataNodes = 0) +public abstract class DiscoveryEc2NetworkAddressesTestCase extends ESIntegTestCase { + + @Override + protected Collection> nodePlugins() { + return CollectionUtils.appendToCopyNoNullElements(super.nodePlugins(), Ec2DiscoveryPlugin.class); + } + + @Override + protected boolean addMockHttpTransport() { + return false; + } + + void verifyPublishAddress(String publishAddressSetting, String expectedAddress) throws IOException { + final var node = internalCluster().startNode(Settings.builder().put("http.publish_host", publishAddressSetting)); + assertEquals( + expectedAddress, + internalCluster().getInstance(HttpServerTransport.class, node).boundAddress().publishAddress().getAddress() + ); + internalCluster().stopNode(node); + } +} diff --git a/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2RegularNetworkAddressesIT.java b/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2RegularNetworkAddressesIT.java new file mode 100644 index 0000000000000..491fa37a4c87d --- /dev/null +++ b/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2RegularNetworkAddressesIT.java @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.discovery.ec2; + +import fixture.aws.imds.Ec2ImdsHttpFixture; +import fixture.aws.imds.Ec2ImdsServiceBuilder; +import fixture.aws.imds.Ec2ImdsVersion; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.transport.BindTransportException; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +import static org.hamcrest.Matchers.containsString; + +public class DiscoveryEc2RegularNetworkAddressesIT extends DiscoveryEc2NetworkAddressesTestCase { + public void testLocalIgnoresImds() { + Ec2ImdsHttpFixture.runWithFixture(new Ec2ImdsServiceBuilder(randomFrom(Ec2ImdsVersion.values())), imdsFixture -> { + try (var ignored = Ec2ImdsHttpFixture.withEc2MetadataServiceEndpointOverride(imdsFixture.getAddress())) { + verifyPublishAddress("_local_", "127.0.0.1"); + } + }); + } + + public void testImdsNotAvailable() throws IOException { + try (var ignored = Ec2ImdsHttpFixture.withEc2MetadataServiceEndpointOverride("http://127.0.0.1")) { + // if IMDS is not running, regular values like `_local_` should still work + verifyPublishAddress("_local_", "127.0.0.1"); + + // but EC2 addresses will cause the node to fail to start + final var assertionError = expectThrows( + AssertionError.class, + () -> internalCluster().startNode(Settings.builder().put("http.publish_host", "_ec2_")) + ); + final var executionException = asInstanceOf(ExecutionException.class, assertionError.getCause()); + final var bindTransportException = asInstanceOf(BindTransportException.class, executionException.getCause()); + assertEquals("Failed to resolve publish address", bindTransportException.getMessage()); + final var ioException = asInstanceOf(IOException.class, bindTransportException.getCause()); + assertThat(ioException.getMessage(), containsString("/latest/meta-data/local-ipv4")); + } + } +} diff --git a/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2SpecialNetworkAddressesIT.java b/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2SpecialNetworkAddressesIT.java new file mode 100644 index 0000000000000..f541c4cdd979b --- /dev/null +++ b/plugins/discovery-ec2/src/internalClusterTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2SpecialNetworkAddressesIT.java @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.discovery.ec2; + +import fixture.aws.imds.Ec2ImdsHttpFixture; +import fixture.aws.imds.Ec2ImdsServiceBuilder; +import fixture.aws.imds.Ec2ImdsVersion; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import java.util.Map; +import java.util.stream.Stream; + +public class DiscoveryEc2SpecialNetworkAddressesIT extends DiscoveryEc2NetworkAddressesTestCase { + + private final String imdsAddressName; + private final String elasticsearchAddressName; + private final Ec2ImdsVersion imdsVersion; + + public DiscoveryEc2SpecialNetworkAddressesIT( + @Name("imdsAddressName") String imdsAddressName, + @Name("elasticsearchAddressName") String elasticsearchAddressName, + @Name("imdsVersion") Ec2ImdsVersion imdsVersion + ) { + this.imdsAddressName = imdsAddressName; + this.elasticsearchAddressName = elasticsearchAddressName; + this.imdsVersion = imdsVersion; + } + + @ParametersFactory + public static Iterable parameters() { + return Map.of( + "_ec2:privateIpv4_", + "local-ipv4", + "_ec2:privateDns_", + "local-hostname", + "_ec2:publicIpv4_", + "public-ipv4", + "_ec2:publicDns_", + "public-hostname", + "_ec2:publicIp_", + "public-ipv4", + "_ec2:privateIp_", + "local-ipv4", + "_ec2_", + "local-ipv4" + ) + .entrySet() + .stream() + .flatMap( + addresses -> Stream.of(Ec2ImdsVersion.values()) + .map(ec2ImdsVersion -> new Object[] { addresses.getValue(), addresses.getKey(), ec2ImdsVersion }) + ) + .toList(); + } + + public void testSpecialNetworkAddresses() { + final var publishAddress = "10.0." + between(0, 255) + "." + between(0, 255); + Ec2ImdsHttpFixture.runWithFixture( + new Ec2ImdsServiceBuilder(imdsVersion).addInstanceAddress(imdsAddressName, publishAddress), + imdsFixture -> { + try (var ignored = Ec2ImdsHttpFixture.withEc2MetadataServiceEndpointOverride(imdsFixture.getAddress())) { + verifyPublishAddress(elasticsearchAddressName, publishAddress); + } + } + ); + } + +} diff --git a/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeTestCase.java b/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeTestCase.java index 7eb18eec5c0b9..178c5c3ad4cae 100644 --- a/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeTestCase.java +++ b/plugins/discovery-ec2/src/javaRestTest/java/org/elasticsearch/discovery/ec2/DiscoveryEc2AvailabilityZoneAttributeTestCase.java @@ -9,6 +9,8 @@ package org.elasticsearch.discovery.ec2; +import fixture.aws.imds.Ec2ImdsHttpFixture; + import org.elasticsearch.client.Request; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.test.cluster.ElasticsearchCluster; @@ -34,7 +36,7 @@ protected static ElasticsearchCluster buildCluster(Supplier imdsFixtureA return ElasticsearchCluster.local() .plugin("discovery-ec2") .setting(AwsEc2Service.AUTO_ATTRIBUTE_SETTING.getKey(), "true") - .systemProperty("com.amazonaws.sdk.ec2MetadataServiceEndpointOverride", imdsFixtureAddressSupplier) + .systemProperty(Ec2ImdsHttpFixture.ENDPOINT_OVERRIDE_SYSPROP_NAME, imdsFixtureAddressSupplier) .build(); } diff --git a/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2NetworkTests.java b/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2NetworkTests.java deleted file mode 100644 index 82787f53c9f76..0000000000000 --- a/plugins/discovery-ec2/src/test/java/org/elasticsearch/discovery/ec2/Ec2NetworkTests.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.discovery.ec2; - -import com.sun.net.httpserver.HttpServer; - -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.network.NetworkService; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.core.SuppressForbidden; -import org.elasticsearch.mocksocket.MockHttpServer; -import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.test.ESTestCase; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.Arrays; -import java.util.Collections; -import java.util.function.BiConsumer; - -import static com.amazonaws.SDKGlobalConfiguration.EC2_METADATA_SERVICE_OVERRIDE_SYSTEM_PROPERTY; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.hamcrest.Matchers.arrayContaining; -import static org.hamcrest.Matchers.equalTo; - -/** - * Test for EC2 network.host settings. - *

- * Warning: This test doesn't assert that the exceptions are thrown. - * They aren't. - */ -@SuppressForbidden(reason = "use http server") -public class Ec2NetworkTests extends ESTestCase { - - private static HttpServer httpServer; - - @BeforeClass - public static void startHttp() throws Exception { - httpServer = MockHttpServer.createHttp(new InetSocketAddress(InetAddress.getLoopbackAddress().getHostAddress(), 0), 0); - - BiConsumer registerContext = (path, v) -> { - final byte[] message = v.getBytes(UTF_8); - httpServer.createContext(path, (s) -> { - s.sendResponseHeaders(RestStatus.OK.getStatus(), message.length); - OutputStream responseBody = s.getResponseBody(); - responseBody.write(message); - responseBody.close(); - }); - }; - registerContext.accept("/latest/meta-data/local-ipv4", "127.0.0.1"); - registerContext.accept("/latest/meta-data/public-ipv4", "165.168.10.2"); - registerContext.accept("/latest/meta-data/public-hostname", "165.168.10.3"); - registerContext.accept("/latest/meta-data/local-hostname", "10.10.10.5"); - - httpServer.start(); - } - - @Before - public void setup() { - // redirect EC2 metadata service to httpServer - AccessController.doPrivileged( - (PrivilegedAction) () -> System.setProperty( - EC2_METADATA_SERVICE_OVERRIDE_SYSTEM_PROPERTY, - "http://" + httpServer.getAddress().getHostName() + ":" + httpServer.getAddress().getPort() - ) - ); - } - - @AfterClass - public static void stopHttp() { - httpServer.stop(0); - httpServer = null; - } - - /** - * Test for network.host: _ec2_ - */ - public void testNetworkHostEc2() throws IOException { - resolveEc2("_ec2_", InetAddress.getByName("127.0.0.1")); - } - - /** - * Test for network.host: _ec2_ - */ - public void testNetworkHostUnableToResolveEc2() { - // redirect EC2 metadata service to unknown location - AccessController.doPrivileged( - (PrivilegedAction) () -> System.setProperty(EC2_METADATA_SERVICE_OVERRIDE_SYSTEM_PROPERTY, "http://127.0.0.1/") - ); - - try { - resolveEc2("_ec2_", (InetAddress[]) null); - } catch (IOException e) { - assertThat( - e.getMessage(), - equalTo("IOException caught when fetching InetAddress from [http://127.0.0.1//latest/meta-data/local-ipv4]") - ); - } - } - - /** - * Test for network.host: _ec2:publicIp_ - */ - public void testNetworkHostEc2PublicIp() throws IOException { - resolveEc2("_ec2:publicIp_", InetAddress.getByName("165.168.10.2")); - } - - /** - * Test for network.host: _ec2:privateIp_ - */ - public void testNetworkHostEc2PrivateIp() throws IOException { - resolveEc2("_ec2:privateIp_", InetAddress.getByName("127.0.0.1")); - } - - /** - * Test for network.host: _ec2:privateIpv4_ - */ - public void testNetworkHostEc2PrivateIpv4() throws IOException { - resolveEc2("_ec2:privateIpv4_", InetAddress.getByName("127.0.0.1")); - } - - /** - * Test for network.host: _ec2:privateDns_ - */ - public void testNetworkHostEc2PrivateDns() throws IOException { - resolveEc2("_ec2:privateDns_", InetAddress.getByName("10.10.10.5")); - } - - /** - * Test for network.host: _ec2:publicIpv4_ - */ - public void testNetworkHostEc2PublicIpv4() throws IOException { - resolveEc2("_ec2:publicIpv4_", InetAddress.getByName("165.168.10.2")); - } - - /** - * Test for network.host: _ec2:publicDns_ - */ - public void testNetworkHostEc2PublicDns() throws IOException { - resolveEc2("_ec2:publicDns_", InetAddress.getByName("165.168.10.3")); - } - - private InetAddress[] resolveEc2(String host, InetAddress... expected) throws IOException { - Settings nodeSettings = Settings.builder().put("network.host", host).build(); - - NetworkService networkService = new NetworkService(Collections.singletonList(new Ec2NameResolver())); - - InetAddress[] addresses = networkService.resolveBindHostAddresses( - NetworkService.GLOBAL_NETWORK_BIND_HOST_SETTING.get(nodeSettings).toArray(Strings.EMPTY_ARRAY) - ); - if (expected == null) { - fail("We should get an IOException, resolved addressed:" + Arrays.toString(addresses)); - } - assertThat(addresses, arrayContaining(expected)); - return addresses; - } - - /** - * Test that we don't have any regression with network host core settings such as - * network.host: _local_ - */ - public void testNetworkHostCoreLocal() throws IOException { - NetworkService networkService = new NetworkService(Collections.singletonList(new Ec2NameResolver())); - InetAddress[] addresses = networkService.resolveBindHostAddresses(null); - assertThat(addresses, arrayContaining(networkService.resolveBindHostAddresses(new String[] { "_local_" }))); - } -} diff --git a/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpFixture.java b/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpFixture.java index cc268a6021cb3..e232d10fdddbd 100644 --- a/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpFixture.java +++ b/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpFixture.java @@ -10,15 +10,24 @@ import com.sun.net.httpserver.HttpServer; +import org.elasticsearch.core.CheckedConsumer; +import org.elasticsearch.core.Releasable; +import org.elasticsearch.core.SuppressForbidden; import org.junit.rules.ExternalResource; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.util.Objects; public class Ec2ImdsHttpFixture extends ExternalResource { + public static final String ENDPOINT_OVERRIDE_SYSPROP_NAME = "com.amazonaws.sdk.ec2MetadataServiceEndpointOverride"; + private final Ec2ImdsServiceBuilder ec2ImdsServiceBuilder; private HttpServer server; @@ -52,4 +61,32 @@ private static InetSocketAddress resolveAddress() { throw new RuntimeException(e); } } + + @SuppressForbidden(reason = "deliberately adjusting system property for endpoint override for use in internal-cluster tests") + public static Releasable withEc2MetadataServiceEndpointOverride(String endpointOverride) { + final PrivilegedAction resetProperty = System.getProperty(ENDPOINT_OVERRIDE_SYSPROP_NAME) instanceof String originalValue + ? () -> System.setProperty(ENDPOINT_OVERRIDE_SYSPROP_NAME, originalValue) + : () -> System.clearProperty(ENDPOINT_OVERRIDE_SYSPROP_NAME); + doPrivileged(() -> System.setProperty(ENDPOINT_OVERRIDE_SYSPROP_NAME, endpointOverride)); + return () -> doPrivileged(resetProperty); + } + + private static void doPrivileged(PrivilegedAction privilegedAction) { + AccessController.doPrivileged(privilegedAction); + } + + public static void runWithFixture(Ec2ImdsServiceBuilder ec2ImdsServiceBuilder, CheckedConsumer action) { + final var imdsFixture = new Ec2ImdsHttpFixture(ec2ImdsServiceBuilder); + try { + imdsFixture.apply(new Statement() { + @Override + public void evaluate() throws Exception { + action.accept(imdsFixture); + } + }, Description.EMPTY).evaluate(); + } catch (Throwable e) { + throw new AssertionError(e); + } + } + } diff --git a/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpHandler.java b/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpHandler.java index fd2044357257b..0c58205ac8d60 100644 --- a/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpHandler.java +++ b/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsHttpHandler.java @@ -23,6 +23,7 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Collection; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; @@ -43,6 +44,7 @@ public class Ec2ImdsHttpHandler implements HttpHandler { private final Set validImdsTokens = ConcurrentCollections.newConcurrentSet(); private final BiConsumer newCredentialsConsumer; + private final Map instanceAddresses; private final Set validCredentialsEndpoints = ConcurrentCollections.newConcurrentSet(); private final Supplier availabilityZoneSupplier; @@ -50,10 +52,12 @@ public Ec2ImdsHttpHandler( Ec2ImdsVersion ec2ImdsVersion, BiConsumer newCredentialsConsumer, Collection alternativeCredentialsEndpoints, - Supplier availabilityZoneSupplier + Supplier availabilityZoneSupplier, + Map instanceAddresses ) { this.ec2ImdsVersion = Objects.requireNonNull(ec2ImdsVersion); this.newCredentialsConsumer = Objects.requireNonNull(newCredentialsConsumer); + this.instanceAddresses = instanceAddresses; this.validCredentialsEndpoints.addAll(alternativeCredentialsEndpoints); this.availabilityZoneSupplier = availabilityZoneSupplier; } @@ -97,17 +101,11 @@ public void handle(final HttpExchange exchange) throws IOException { if (path.equals(IMDS_SECURITY_CREDENTIALS_PATH)) { final var profileName = randomIdentifier(); validCredentialsEndpoints.add(IMDS_SECURITY_CREDENTIALS_PATH + profileName); - final byte[] response = profileName.getBytes(StandardCharsets.UTF_8); - exchange.getResponseHeaders().add("Content-Type", "text/plain"); - exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length); - exchange.getResponseBody().write(response); + sendStringResponse(exchange, profileName); return; } else if (path.equals("/latest/meta-data/placement/availability-zone")) { final var availabilityZone = availabilityZoneSupplier.get(); - final byte[] response = availabilityZone.getBytes(StandardCharsets.UTF_8); - exchange.getResponseHeaders().add("Content-Type", "text/plain"); - exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length); - exchange.getResponseBody().write(response); + sendStringResponse(exchange, availabilityZone); return; } else if (validCredentialsEndpoints.contains(path)) { final String accessKey = randomIdentifier(); @@ -132,10 +130,20 @@ public void handle(final HttpExchange exchange) throws IOException { exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length); exchange.getResponseBody().write(response); return; + } else if (instanceAddresses.get(path) instanceof String instanceAddress) { + sendStringResponse(exchange, instanceAddress); + return; } } ExceptionsHelper.maybeDieOnAnotherThread(new AssertionError("not supported: " + requestMethod + " " + path)); } } + + private void sendStringResponse(HttpExchange exchange, String value) throws IOException { + final byte[] response = value.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "text/plain"); + exchange.sendResponseHeaders(RestStatus.OK.getStatus(), response.length); + exchange.getResponseBody().write(response); + } } diff --git a/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsServiceBuilder.java b/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsServiceBuilder.java index bca43da8683b6..505c9978bc4fb 100644 --- a/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsServiceBuilder.java +++ b/test/fixtures/ec2-imds-fixture/src/main/java/fixture/aws/imds/Ec2ImdsServiceBuilder.java @@ -12,6 +12,8 @@ import org.elasticsearch.test.ESTestCase; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Supplier; @@ -22,6 +24,7 @@ public class Ec2ImdsServiceBuilder { private BiConsumer newCredentialsConsumer = Ec2ImdsServiceBuilder::rejectNewCredentials; private Collection alternativeCredentialsEndpoints = Set.of(); private Supplier availabilityZoneSupplier = Ec2ImdsServiceBuilder::rejectAvailabilityZone; + private final Map instanceAddresses = new HashMap<>(); public Ec2ImdsServiceBuilder(Ec2ImdsVersion ec2ImdsVersion) { this.ec2ImdsVersion = ec2ImdsVersion; @@ -50,8 +53,19 @@ public Ec2ImdsServiceBuilder availabilityZoneSupplier(Supplier availabil return this; } + public Ec2ImdsServiceBuilder addInstanceAddress(String addressType, String addressValue) { + instanceAddresses.put("/latest/meta-data/" + addressType, addressValue); + return this; + } + public Ec2ImdsHttpHandler buildHandler() { - return new Ec2ImdsHttpHandler(ec2ImdsVersion, newCredentialsConsumer, alternativeCredentialsEndpoints, availabilityZoneSupplier); + return new Ec2ImdsHttpHandler( + ec2ImdsVersion, + newCredentialsConsumer, + alternativeCredentialsEndpoints, + availabilityZoneSupplier, + Map.copyOf(instanceAddresses) + ); } } From 9b095eb765fb08e192bc4960281f1f55d4ed826c Mon Sep 17 00:00:00 2001 From: Keith Massey Date: Thu, 12 Dec 2024 14:39:25 -0600 Subject: [PATCH 66/77] Adding a migration reindex cancel API (#118291) This introduces the migration reindex cancel API, which cancels a migration reindex task for a given data stream name that was started with #118109. For example: ``` POST localhost:9200/_migration/reindex/my-data-stream/_cancel?pretty ``` returns ``` { "acknowledged" : true } ``` This cancels the task, and cancels any ongoing reindexing of backing indices, but does not do any cleanup. --- docs/changelog/118291.yaml | 5 ++ .../api/migrate.cancel_reindex.json | 30 +++++++ .../ReindexDataStreamTransportActionIT.java | 17 ++++ .../xpack/migrate/MigratePlugin.java | 5 ++ .../action/CancelReindexDataStreamAction.java | 90 +++++++++++++++++++ ...ancelReindexDataStreamTransportAction.java | 57 ++++++++++++ .../RestCancelReindexDataStreamAction.java | 39 ++++++++ ...indexDataStreamPersistentTaskExecutor.java | 6 +- .../migrate/task/ReindexDataStreamTask.java | 45 ++++++++-- .../CancelReindexDataStreamRequestTests.java | 32 +++++++ .../xpack/security/operator/Constants.java | 1 + .../rest-api-spec/test/migrate/10_reindex.yml | 31 ++++--- .../test/migrate/20_reindex_status.yml | 58 +++++++----- 13 files changed, 371 insertions(+), 45 deletions(-) create mode 100644 docs/changelog/118291.yaml create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/api/migrate.cancel_reindex.json create mode 100644 x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CancelReindexDataStreamAction.java create mode 100644 x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CancelReindexDataStreamTransportAction.java create mode 100644 x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/rest/RestCancelReindexDataStreamAction.java create mode 100644 x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/action/CancelReindexDataStreamRequestTests.java diff --git a/docs/changelog/118291.yaml b/docs/changelog/118291.yaml new file mode 100644 index 0000000000000..8001b3972e876 --- /dev/null +++ b/docs/changelog/118291.yaml @@ -0,0 +1,5 @@ +pr: 118291 +summary: Adding a migration reindex cancel API +area: Data streams +type: enhancement +issues: [] diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/migrate.cancel_reindex.json b/rest-api-spec/src/main/resources/rest-api-spec/api/migrate.cancel_reindex.json new file mode 100644 index 0000000000000..a034f204edbfb --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/migrate.cancel_reindex.json @@ -0,0 +1,30 @@ +{ + "migrate.cancel_reindex":{ + "documentation":{ + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/data-stream-reindex.html", + "description":"This API returns the status of a migration reindex attempt for a data stream or index" + }, + "stability":"experimental", + "visibility":"private", + "headers":{ + "accept": [ "application/json"], + "content_type": ["application/json"] + }, + "url":{ + "paths":[ + { + "path":"/_migration/reindex/{index}/_cancel", + "methods":[ + "POST" + ], + "parts":{ + "index":{ + "type":"string", + "description":"The index or data stream name" + } + } + } + ] + } + } +} diff --git a/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportActionIT.java b/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportActionIT.java index 7f2243ed76849..6e24e644cb2af 100644 --- a/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportActionIT.java +++ b/x-pack/plugin/migrate/src/internalClusterTest/java/org/elasticsearch/xpack/migrate/action/ReindexDataStreamTransportActionIT.java @@ -117,6 +117,23 @@ public void testAlreadyUpToDateDataStream() throws Exception { assertThat(status.totalIndices(), equalTo(backingIndexCount)); assertThat(status.totalIndicesToBeUpgraded(), equalTo(0)); }); + AcknowledgedResponse cancelResponse = client().execute( + CancelReindexDataStreamAction.INSTANCE, + new CancelReindexDataStreamAction.Request(dataStreamName) + ).actionGet(); + assertNotNull(cancelResponse); + assertThrows( + ResourceNotFoundException.class, + () -> client().execute(CancelReindexDataStreamAction.INSTANCE, new CancelReindexDataStreamAction.Request(dataStreamName)) + .actionGet() + ); + assertThrows( + ResourceNotFoundException.class, + () -> client().execute( + new ActionType(GetMigrationReindexStatusAction.NAME), + new GetMigrationReindexStatusAction.Request(dataStreamName) + ).actionGet() + ); } private int createDataStream(String dataStreamName) { diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/MigratePlugin.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/MigratePlugin.java index 1af66a2c61d56..26f8e57102a4d 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/MigratePlugin.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/MigratePlugin.java @@ -32,10 +32,13 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xpack.migrate.action.CancelReindexDataStreamAction; +import org.elasticsearch.xpack.migrate.action.CancelReindexDataStreamTransportAction; import org.elasticsearch.xpack.migrate.action.GetMigrationReindexStatusAction; import org.elasticsearch.xpack.migrate.action.GetMigrationReindexStatusTransportAction; import org.elasticsearch.xpack.migrate.action.ReindexDataStreamAction; import org.elasticsearch.xpack.migrate.action.ReindexDataStreamTransportAction; +import org.elasticsearch.xpack.migrate.rest.RestCancelReindexDataStreamAction; import org.elasticsearch.xpack.migrate.rest.RestGetMigrationReindexStatusAction; import org.elasticsearch.xpack.migrate.rest.RestMigrationReindexAction; import org.elasticsearch.xpack.migrate.task.ReindexDataStreamPersistentTaskExecutor; @@ -69,6 +72,7 @@ public List getRestHandlers( if (REINDEX_DATA_STREAM_FEATURE_FLAG.isEnabled()) { handlers.add(new RestMigrationReindexAction()); handlers.add(new RestGetMigrationReindexStatusAction()); + handlers.add(new RestCancelReindexDataStreamAction()); } return handlers; } @@ -79,6 +83,7 @@ public List getRestHandlers( if (REINDEX_DATA_STREAM_FEATURE_FLAG.isEnabled()) { actions.add(new ActionHandler<>(ReindexDataStreamAction.INSTANCE, ReindexDataStreamTransportAction.class)); actions.add(new ActionHandler<>(GetMigrationReindexStatusAction.INSTANCE, GetMigrationReindexStatusTransportAction.class)); + actions.add(new ActionHandler<>(CancelReindexDataStreamAction.INSTANCE, CancelReindexDataStreamTransportAction.class)); } return actions; } diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CancelReindexDataStreamAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CancelReindexDataStreamAction.java new file mode 100644 index 0000000000000..635d8b8f30978 --- /dev/null +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CancelReindexDataStreamAction.java @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.migrate.action; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Objects; + +public class CancelReindexDataStreamAction extends ActionType { + + public static final CancelReindexDataStreamAction INSTANCE = new CancelReindexDataStreamAction(); + public static final String NAME = "indices:admin/data_stream/reindex_cancel"; + + public CancelReindexDataStreamAction() { + super(NAME); + } + + public static class Request extends ActionRequest implements IndicesRequest { + private final String index; + + public Request(String index) { + super(); + this.index = index; + } + + public Request(StreamInput in) throws IOException { + super(in); + this.index = in.readString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(index); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public boolean getShouldStoreResult() { + return true; + } + + public String getIndex() { + return index; + } + + @Override + public int hashCode() { + return Objects.hashCode(index); + } + + @Override + public boolean equals(Object other) { + return other instanceof Request && index.equals(((Request) other).index); + } + + public Request nodeRequest(String thisNodeId, long thisTaskId) { + Request copy = new Request(index); + copy.setParentTask(thisNodeId, thisTaskId); + return copy; + } + + @Override + public String[] indices() { + return new String[] { index }; + } + + @Override + public IndicesOptions indicesOptions() { + return IndicesOptions.strictSingleIndexNoExpandForbidClosed(); + } + } +} diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CancelReindexDataStreamTransportAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CancelReindexDataStreamTransportAction.java new file mode 100644 index 0000000000000..00a846bf7eb9a --- /dev/null +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/action/CancelReindexDataStreamTransportAction.java @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.migrate.action; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.injection.guice.Inject; +import org.elasticsearch.persistent.PersistentTasksCustomMetadata; +import org.elasticsearch.persistent.PersistentTasksService; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.migrate.action.CancelReindexDataStreamAction.Request; + +public class CancelReindexDataStreamTransportAction extends HandledTransportAction { + private final PersistentTasksService persistentTasksService; + + @Inject + public CancelReindexDataStreamTransportAction( + TransportService transportService, + ActionFilters actionFilters, + PersistentTasksService persistentTasksService + ) { + super(CancelReindexDataStreamAction.NAME, transportService, actionFilters, Request::new, EsExecutors.DIRECT_EXECUTOR_SERVICE); + this.persistentTasksService = persistentTasksService; + } + + @Override + protected void doExecute(Task task, Request request, ActionListener listener) { + String index = request.getIndex(); + String persistentTaskId = ReindexDataStreamAction.TASK_ID_PREFIX + index; + /* + * This removes the persistent task from the cluster state and results in the running task being cancelled (but not removed from + * the task manager). The running task is removed from the task manager in ReindexDataStreamTask::onCancelled, which is called as + * as result of this. + */ + persistentTasksService.sendRemoveRequest(persistentTaskId, TimeValue.MAX_VALUE, new ActionListener<>() { + @Override + public void onResponse(PersistentTasksCustomMetadata.PersistentTask persistentTask) { + listener.onResponse(AcknowledgedResponse.TRUE); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }); + } +} diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/rest/RestCancelReindexDataStreamAction.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/rest/RestCancelReindexDataStreamAction.java new file mode 100644 index 0000000000000..0bd68e8b2df73 --- /dev/null +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/rest/RestCancelReindexDataStreamAction.java @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.migrate.rest; + +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.xpack.migrate.action.CancelReindexDataStreamAction; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +public class RestCancelReindexDataStreamAction extends BaseRestHandler { + + @Override + public String getName() { + return "cancel_reindex_data_stream_action"; + } + + @Override + public List routes() { + return List.of(new Route(POST, "/_migration/reindex/{index}/_cancel")); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + String index = request.param("index"); + CancelReindexDataStreamAction.Request cancelTaskRequest = new CancelReindexDataStreamAction.Request(index); + return channel -> client.execute(CancelReindexDataStreamAction.INSTANCE, cancelTaskRequest, new RestToXContentListener<>(channel)); + } +} diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java index 7ec5014b9edff..176220a1ccae8 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamPersistentTaskExecutor.java @@ -91,13 +91,11 @@ protected void nodeOperation(AllocatedPersistentTask task, ReindexDataStreamTask } private void completeSuccessfulPersistentTask(ReindexDataStreamTask persistentTask) { - persistentTask.allReindexesCompleted(); - threadPool.schedule(persistentTask::markAsCompleted, getTimeToLive(persistentTask), threadPool.generic()); + persistentTask.allReindexesCompleted(threadPool, getTimeToLive(persistentTask)); } private void completeFailedPersistentTask(ReindexDataStreamTask persistentTask, Exception e) { - persistentTask.taskFailed(e); - threadPool.schedule(() -> persistentTask.markAsFailed(e), getTimeToLive(persistentTask), threadPool.generic()); + persistentTask.taskFailed(threadPool, getTimeToLive(persistentTask), e); } private TimeValue getTimeToLive(ReindexDataStreamTask reindexDataStreamTask) { diff --git a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamTask.java b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamTask.java index 72ddb87e9dea5..844f24f45ab77 100644 --- a/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamTask.java +++ b/x-pack/plugin/migrate/src/main/java/org/elasticsearch/xpack/migrate/task/ReindexDataStreamTask.java @@ -7,9 +7,12 @@ package org.elasticsearch.xpack.migrate.task; +import org.elasticsearch.common.util.concurrent.RunOnce; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; import org.elasticsearch.persistent.AllocatedPersistentTask; import org.elasticsearch.tasks.TaskId; +import org.elasticsearch.threadpool.ThreadPool; import java.util.ArrayList; import java.util.List; @@ -21,12 +24,14 @@ public class ReindexDataStreamTask extends AllocatedPersistentTask { private final long persistentTaskStartTime; private final int totalIndices; private final int totalIndicesToBeUpgraded; - private boolean complete = false; - private Exception exception; - private AtomicInteger inProgress = new AtomicInteger(0); - private AtomicInteger pending = new AtomicInteger(); - private List> errors = new ArrayList<>(); + private volatile boolean complete = false; + private volatile Exception exception; + private final AtomicInteger inProgress = new AtomicInteger(0); + private final AtomicInteger pending = new AtomicInteger(); + private final List> errors = new ArrayList<>(); + private final RunOnce completeTask; + @SuppressWarnings("this-escape") public ReindexDataStreamTask( long persistentTaskStartTime, int totalIndices, @@ -42,6 +47,13 @@ public ReindexDataStreamTask( this.persistentTaskStartTime = persistentTaskStartTime; this.totalIndices = totalIndices; this.totalIndicesToBeUpgraded = totalIndicesToBeUpgraded; + this.completeTask = new RunOnce(() -> { + if (exception == null) { + markAsCompleted(); + } else { + markAsFailed(exception); + } + }); } @Override @@ -58,13 +70,18 @@ public ReindexDataStreamStatus getStatus() { ); } - public void allReindexesCompleted() { + public void allReindexesCompleted(ThreadPool threadPool, TimeValue timeToLive) { this.complete = true; + if (isCancelled()) { + completeTask.run(); + } else { + threadPool.schedule(completeTask, timeToLive, threadPool.generic()); + } } - public void taskFailed(Exception e) { - this.complete = true; + public void taskFailed(ThreadPool threadPool, TimeValue timeToLive, Exception e) { this.exception = e; + allReindexesCompleted(threadPool, timeToLive); } public void reindexSucceeded() { @@ -84,4 +101,16 @@ public void incrementInProgressIndicesCount() { public void setPendingIndicesCount(int size) { pending.set(size); } + + @Override + public void onCancelled() { + /* + * If the task is complete, but just waiting for its scheduled removal, we go ahead and call markAsCompleted/markAsFailed + * immediately. This results in the running task being removed from the task manager. If the task is not complete, then one of + * allReindexesCompleted or taskFailed will be called in the future, resulting in the same thing. + */ + if (complete) { + completeTask.run(); + } + } } diff --git a/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/action/CancelReindexDataStreamRequestTests.java b/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/action/CancelReindexDataStreamRequestTests.java new file mode 100644 index 0000000000000..187561dae19b0 --- /dev/null +++ b/x-pack/plugin/migrate/src/test/java/org/elasticsearch/xpack/migrate/action/CancelReindexDataStreamRequestTests.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.migrate.action; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.xpack.migrate.action.CancelReindexDataStreamAction.Request; + +import java.io.IOException; + +public class CancelReindexDataStreamRequestTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return Request::new; + } + + @Override + protected Request createTestInstance() { + return new Request(randomAlphaOfLength(30)); + } + + @Override + protected Request mutateInstance(Request instance) throws IOException { + return new Request(instance.getIndex() + randomAlphaOfLength(5)); + } +} diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java index db87fdbcb8f1f..b139d1526ec20 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -639,6 +639,7 @@ public class Constants { "internal:index/metadata/migration_version/update", new FeatureFlag("reindex_data_stream").isEnabled() ? "indices:admin/migration/reindex_status" : null, new FeatureFlag("reindex_data_stream").isEnabled() ? "indices:admin/data_stream/reindex" : null, + new FeatureFlag("reindex_data_stream").isEnabled() ? "indices:admin/data_stream/reindex_cancel" : null, "internal:admin/repository/verify", "internal:admin/repository/verify/coordinate" ).filter(Objects::nonNull).collect(Collectors.toUnmodifiableSet()); diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/migrate/10_reindex.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/migrate/10_reindex.yml index f50a7a65f53d3..9fb33b43f042f 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/migrate/10_reindex.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/migrate/10_reindex.yml @@ -104,14 +104,23 @@ setup: name: my-data-stream - is_true: acknowledged -# Uncomment once the cancel API is in place -# - do: -# migrate.reindex: -# body: | -# { -# "mode": "upgrade", -# "source": { -# "index": "my-data-stream" -# } -# } -# - match: { task: "reindex-data-stream-my-data-stream" } + - do: + migrate.reindex: + body: | + { + "mode": "upgrade", + "source": { + "index": "my-data-stream" + } + } + - match: { acknowledged: true } + + - do: + migrate.cancel_reindex: + index: "my-data-stream" + - match: { acknowledged: true } + + - do: + catch: /resource_not_found_exception/ + migrate.cancel_reindex: + index: "my-data-stream" diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/migrate/20_reindex_status.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/migrate/20_reindex_status.yml index ae343a0b4db95..c94ce8dd211ae 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/migrate/20_reindex_status.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/migrate/20_reindex_status.yml @@ -46,25 +46,39 @@ setup: name: my-data-stream - is_true: acknowledged -# Uncomment once the cancel API is in place -# - do: -# migrate.reindex: -# body: | -# { -# "mode": "upgrade", -# "source": { -# "index": "my-data-stream" -# } -# } -# - match: { acknowledged: true } -# -# - do: -# migrate.get_reindex_status: -# index: "my-data-stream" -# - match: { complete: true } -# - match: { total_indices: 1 } -# - match: { total_indices_requiring_upgrade: 0 } -# - match: { successes: 0 } -# - match: { in_progress: 0 } -# - match: { pending: 0 } -# - match: { errors: [] } + - do: + migrate.reindex: + body: | + { + "mode": "upgrade", + "source": { + "index": "my-data-stream" + } + } + - match: { acknowledged: true } + + - do: + migrate.get_reindex_status: + index: "my-data-stream" + - match: { complete: true } + - match: { total_indices: 1 } + - match: { total_indices_requiring_upgrade: 0 } + - match: { successes: 0 } + - match: { in_progress: 0 } + - match: { pending: 0 } + - match: { errors: [] } + + - do: + migrate.cancel_reindex: + index: "my-data-stream" + - match: { acknowledged: true } + + - do: + catch: /resource_not_found_exception/ + migrate.cancel_reindex: + index: "my-data-stream" + + - do: + catch: /resource_not_found_exception/ + migrate.get_reindex_status: + index: "my-data-stream" From 38a34d969d530c4b3954c7adbf80dbe10373c93e Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Fri, 13 Dec 2024 08:27:09 +1100 Subject: [PATCH 67/77] Mute org.elasticsearch.reservedstate.service.FileSettingsServiceTests testInvalidJSON #116521 --- muted-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/muted-tests.yml b/muted-tests.yml index b750c0777ce34..95beeb7aa8f8d 100644 --- a/muted-tests.yml +++ b/muted-tests.yml @@ -314,6 +314,9 @@ tests: - class: org.elasticsearch.packaging.test.DockerTests method: test011SecurityEnabledStatus issue: https://github.com/elastic/elasticsearch/issues/118517 +- class: org.elasticsearch.reservedstate.service.FileSettingsServiceTests + method: testInvalidJSON + issue: https://github.com/elastic/elasticsearch/issues/116521 # Examples: # From 0cc08a9196f75b3bcd630a55331578b1fc335b74 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 12 Dec 2024 22:39:21 +0100 Subject: [PATCH 68/77] Speedup Injector during concurrent node starts (#118588) Lets simplify this logic a little and lock on the injector instance instead of the class. Locking on the class actually wastes lots of time during test runs it turns out, especially with multi-cluster tests. --- .../elasticsearch/injection/guice/Binder.java | 4 +- .../injection/guice/BindingProcessor.java | 1 - .../injection/guice/InjectorBuilder.java | 3 +- .../injection/guice/Provider.java | 2 - .../elasticsearch/injection/guice/Scope.java | 59 -------------- .../elasticsearch/injection/guice/Scopes.java | 78 ++++--------------- .../internal/AbstractBindingBuilder.java | 2 +- .../injection/guice/internal/Scoping.java | 66 ++-------------- 8 files changed, 24 insertions(+), 191 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/injection/guice/Scope.java diff --git a/server/src/main/java/org/elasticsearch/injection/guice/Binder.java b/server/src/main/java/org/elasticsearch/injection/guice/Binder.java index c34bebd10c2e1..d59edfce89183 100644 --- a/server/src/main/java/org/elasticsearch/injection/guice/Binder.java +++ b/server/src/main/java/org/elasticsearch/injection/guice/Binder.java @@ -65,9 +65,7 @@ * *

The {@link Provider} you use here does not have to be a "factory"; that * is, a provider which always creates each instance it provides. - * However, this is generally a good practice to follow. You can then use - * Guice's concept of {@link Scope scopes} to guide when creation should happen - * -- "letting Guice work for you". + * However, this is generally a good practice to follow. * *

  *     bind(Service.class).annotatedWith(Red.class).to(ServiceImpl.class);
diff --git a/server/src/main/java/org/elasticsearch/injection/guice/BindingProcessor.java b/server/src/main/java/org/elasticsearch/injection/guice/BindingProcessor.java index 9223261ec2dd5..677f111c764a4 100644 --- a/server/src/main/java/org/elasticsearch/injection/guice/BindingProcessor.java +++ b/server/src/main/java/org/elasticsearch/injection/guice/BindingProcessor.java @@ -218,7 +218,6 @@ private void putBinding(BindingImpl binding) { MembersInjector.class, Module.class, Provider.class, - Scope.class, TypeLiteral.class ); // TODO(jessewilson): fix BuiltInModule, then add Stage diff --git a/server/src/main/java/org/elasticsearch/injection/guice/InjectorBuilder.java b/server/src/main/java/org/elasticsearch/injection/guice/InjectorBuilder.java index 99d42faf6a803..fe9ac309e23f4 100644 --- a/server/src/main/java/org/elasticsearch/injection/guice/InjectorBuilder.java +++ b/server/src/main/java/org/elasticsearch/injection/guice/InjectorBuilder.java @@ -20,6 +20,7 @@ import org.elasticsearch.injection.guice.internal.Errors; import org.elasticsearch.injection.guice.internal.ErrorsException; import org.elasticsearch.injection.guice.internal.InternalContext; +import org.elasticsearch.injection.guice.internal.Scoping; import org.elasticsearch.injection.guice.internal.Stopwatch; import org.elasticsearch.injection.guice.spi.Dependency; @@ -154,7 +155,7 @@ public static void loadEagerSingletons(InjectorImpl injector, Errors errors) { } private static void loadEagerSingletons(InjectorImpl injector, final Errors errors, BindingImpl binding) { - if (binding.getScoping().isEagerSingleton()) { + if (binding.getScoping() == Scoping.EAGER_SINGLETON) { try { injector.callInContext(new ContextualCallable() { final Dependency dependency = Dependency.get(binding.getKey()); diff --git a/server/src/main/java/org/elasticsearch/injection/guice/Provider.java b/server/src/main/java/org/elasticsearch/injection/guice/Provider.java index 692617239ea74..6de9d8ff9dc85 100644 --- a/server/src/main/java/org/elasticsearch/injection/guice/Provider.java +++ b/server/src/main/java/org/elasticsearch/injection/guice/Provider.java @@ -28,8 +28,6 @@ * instances, instances you wish to safely mutate and discard, instances which are out of scope * (e.g. using a {@code @RequestScoped} object from within a {@code @SessionScoped} object), or * instances that will be initialized lazily. - *
  • A custom {@link Scope} is implemented as a decorator of {@code Provider}, which decides - * when to delegate to the backing provider and when to provide the instance some other way. *
  • The {@link Injector} offers access to the {@code Provider} it uses to fulfill requests * for a given key, via the {@link Injector#getProvider} methods. * diff --git a/server/src/main/java/org/elasticsearch/injection/guice/Scope.java b/server/src/main/java/org/elasticsearch/injection/guice/Scope.java deleted file mode 100644 index 681fc17bc6353..0000000000000 --- a/server/src/main/java/org/elasticsearch/injection/guice/Scope.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2006 Google Inc. - * - * Licensed 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.injection.guice; - -/** - * A scope is a level of visibility that instances provided by Guice may have. - * By default, an instance created by the {@link Injector} has no scope, - * meaning it has no state from the framework's perspective -- the - * {@code Injector} creates it, injects it once into the class that required it, - * and then immediately forgets it. Associating a scope with a particular - * binding allows the created instance to be "remembered" and possibly used - * again for other injections. - *

    - * An example of a scope is {@link Scopes#SINGLETON}. - * - * @author crazybob@google.com (Bob Lee) - */ -public interface Scope { - - /** - * Scopes a provider. The returned provider returns objects from this scope. - * If an object does not exist in this scope, the provider can use the given - * unscoped provider to retrieve one. - *

    - * Scope implementations are strongly encouraged to override - * {@link Object#toString} in the returned provider and include the backing - * provider's {@code toString()} output. - * - * @param unscoped locates an instance when one doesn't already exist in this - * scope. - * @return a new provider which only delegates to the given unscoped provider - * when an instance of the requested object doesn't already exist in this - * scope - */ - Provider scope(Provider unscoped); - - /** - * A short but useful description of this scope. For comparison, the standard - * scopes that ship with guice use the descriptions - * {@code "Scopes.SINGLETON"}, {@code "ServletScopes.SESSION"} and - * {@code "ServletScopes.REQUEST"}. - */ - @Override - String toString(); -} diff --git a/server/src/main/java/org/elasticsearch/injection/guice/Scopes.java b/server/src/main/java/org/elasticsearch/injection/guice/Scopes.java index d5b61407b4975..5f05d0337654c 100644 --- a/server/src/main/java/org/elasticsearch/injection/guice/Scopes.java +++ b/server/src/main/java/org/elasticsearch/injection/guice/Scopes.java @@ -19,8 +19,6 @@ import org.elasticsearch.injection.guice.internal.InternalFactory; import org.elasticsearch.injection.guice.internal.Scoping; -import java.util.Locale; - /** * Built-in scope implementations. * @@ -31,29 +29,27 @@ public class Scopes { private Scopes() {} /** - * One instance per {@link Injector}. + * Scopes an internal factory. */ - public static final Scope SINGLETON = new Scope() { - @Override - public Provider scope(final Provider creator) { - return new Provider() { + static InternalFactory scope(InjectorImpl injector, InternalFactory creator, Scoping scoping) { + return switch (scoping) { + case UNSCOPED -> creator; + case EAGER_SINGLETON -> new InternalFactoryToProviderAdapter<>(Initializables.of(new Provider<>() { private volatile T instance; - // DCL on a volatile is safe as of Java 5, which we obviously require. @Override - @SuppressWarnings("DoubleCheckedLocking") public T get() { if (instance == null) { /* - * Use a pretty coarse lock. We don't want to run into deadlocks - * when two threads try to load circularly-dependent objects. - * Maybe one of these days we will identify independent graphs of - * objects and offer to load them in parallel. - */ - synchronized (InjectorImpl.class) { + * Use a pretty coarse lock. We don't want to run into deadlocks + * when two threads try to load circularly-dependent objects. + * Maybe one of these days we will identify independent graphs of + * objects and offer to load them in parallel. + */ + synchronized (injector) { if (instance == null) { - instance = creator.get(); + instance = new ProviderToInternalFactoryAdapter<>(injector, creator).get(); } } } @@ -62,54 +58,10 @@ public T get() { @Override public String toString() { - return String.format(Locale.ROOT, "%s[%s]", creator, SINGLETON); + return creator + "[SINGLETON]"; } - }; - } - - @Override - public String toString() { - return "Scopes.SINGLETON"; - } - }; - - /** - * No scope; the same as not applying any scope at all. Each time the - * Injector obtains an instance of an object with "no scope", it injects this - * instance then immediately forgets it. When the next request for the same - * binding arrives it will need to obtain the instance over again. - *

    - * This exists only in case a class has been annotated with a scope - * annotation and you need to override this to "no scope" in your binding. - * - * @since 2.0 - */ - public static final Scope NO_SCOPE = new Scope() { - @Override - public Provider scope(Provider unscoped) { - return unscoped; - } - - @Override - public String toString() { - return "Scopes.NO_SCOPE"; - } - }; - - /** - * Scopes an internal factory. - */ - static InternalFactory scope(InjectorImpl injector, InternalFactory creator, Scoping scoping) { - - if (scoping.isNoScope()) { - return creator; - } - - Scope scope = scoping.getScopeInstance(); - - // TODO: use diamond operator once JI-9019884 is fixed - Provider scoped = scope.scope(new ProviderToInternalFactoryAdapter(injector, creator)); - return new InternalFactoryToProviderAdapter<>(Initializables.of(scoped)); + })); + }; } } diff --git a/server/src/main/java/org/elasticsearch/injection/guice/internal/AbstractBindingBuilder.java b/server/src/main/java/org/elasticsearch/injection/guice/internal/AbstractBindingBuilder.java index 28053c5f1d557..ee54c8aa93520 100644 --- a/server/src/main/java/org/elasticsearch/injection/guice/internal/AbstractBindingBuilder.java +++ b/server/src/main/java/org/elasticsearch/injection/guice/internal/AbstractBindingBuilder.java @@ -77,7 +77,7 @@ protected void checkNotScoped() { return; } - if (binding.getScoping().isExplicitlyScoped()) { + if (binding.getScoping() != Scoping.UNSCOPED) { binder.addError(SCOPE_ALREADY_SET); } } diff --git a/server/src/main/java/org/elasticsearch/injection/guice/internal/Scoping.java b/server/src/main/java/org/elasticsearch/injection/guice/internal/Scoping.java index fcb03f34f4204..e1c04ea8e348f 100644 --- a/server/src/main/java/org/elasticsearch/injection/guice/internal/Scoping.java +++ b/server/src/main/java/org/elasticsearch/injection/guice/internal/Scoping.java @@ -16,8 +16,7 @@ package org.elasticsearch.injection.guice.internal; -import org.elasticsearch.injection.guice.Scope; -import org.elasticsearch.injection.guice.Scopes; +import org.elasticsearch.injection.guice.Injector; /** * References a scope, either directly (as a scope instance), or indirectly (as a scope annotation). @@ -25,69 +24,14 @@ * * @author jessewilson@google.com (Jesse Wilson) */ -public abstract class Scoping { - +public enum Scoping { /** * No scoping annotation has been applied. Note that this is different from {@code * in(Scopes.NO_SCOPE)}, where the 'NO_SCOPE' has been explicitly applied. */ - public static final Scoping UNSCOPED = new Scoping() { - - @Override - public Scope getScopeInstance() { - return Scopes.NO_SCOPE; - } - - @Override - public String toString() { - return Scopes.NO_SCOPE.toString(); - } - - }; - - public static final Scoping EAGER_SINGLETON = new Scoping() { - - @Override - public Scope getScopeInstance() { - return Scopes.SINGLETON; - } - - @Override - public String toString() { - return "eager singleton"; - } - - }; - + UNSCOPED, /** - * Returns true if this scope was explicitly applied. If no scope was explicitly applied then the - * scoping annotation will be used. + * One instance per {@link Injector}. */ - public boolean isExplicitlyScoped() { - return this != UNSCOPED; - } - - /** - * Returns true if this is the default scope. In this case a new instance will be provided for - * each injection. - */ - public boolean isNoScope() { - return getScopeInstance() == Scopes.NO_SCOPE; - } - - /** - * Returns true if this scope is a singleton that should be loaded eagerly in {@code stage}. - */ - public boolean isEagerSingleton() { - return this == EAGER_SINGLETON; - } - - /** - * Returns the scope instance, or {@code null} if that isn't known for this instance. - */ - public Scope getScopeInstance() { - return null; - } - - private Scoping() {} + EAGER_SINGLETON } From c449da8a8a955c127e832f4730a66109be0ed017 Mon Sep 17 00:00:00 2001 From: Lee Hinman Date: Thu, 12 Dec 2024 15:14:56 -0700 Subject: [PATCH 69/77] Include hidden indices in DeprecationInfoAction (#118035) This fixes an issue where the deprecation API wouldn't include hidden indices by default. Resolves #118020 --- docs/changelog/118035.yaml | 6 ++++++ .../xpack/deprecation/DeprecationInfoAction.java | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/118035.yaml diff --git a/docs/changelog/118035.yaml b/docs/changelog/118035.yaml new file mode 100644 index 0000000000000..fdeaa184723b9 --- /dev/null +++ b/docs/changelog/118035.yaml @@ -0,0 +1,6 @@ +pr: 118035 +summary: Include hidden indices in `DeprecationInfoAction` +area: Indices APIs +type: bug +issues: + - 118020 diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationInfoAction.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationInfoAction.java index 87d0bfb93e18c..7ad0758d99832 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationInfoAction.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/DeprecationInfoAction.java @@ -366,7 +366,7 @@ private static ClusterState removeSkippedSettings(ClusterState state, String[] i public static class Request extends MasterNodeReadRequest implements IndicesRequest.Replaceable { - private static final IndicesOptions INDICES_OPTIONS = IndicesOptions.fromOptions(false, true, true, true); + private static final IndicesOptions INDICES_OPTIONS = IndicesOptions.fromOptions(false, true, true, true, true); private String[] indices; public Request(TimeValue masterNodeTimeout, String... indices) { From 2ab4d3d5ee27443deed6b71185bb7edb9d06b4da Mon Sep 17 00:00:00 2001 From: Svilen Mihaylov Date: Thu, 12 Dec 2024 21:25:57 -0500 Subject: [PATCH 70/77] Remove "use_field_mapping" in FieldFormat (#118513) The method in which it was parsed was unused. --- .../search/fetch/subphase/FieldAndFormat.java | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/FieldAndFormat.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/FieldAndFormat.java index f623b3040f1c5..ef8769b688c64 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/FieldAndFormat.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/FieldAndFormat.java @@ -12,8 +12,6 @@ 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.logging.DeprecationLogger; -import org.elasticsearch.core.CheckedFunction; import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; @@ -30,9 +28,6 @@ * display values of this field. */ public final class FieldAndFormat implements Writeable, ToXContentObject { - private static final String USE_DEFAULT_FORMAT = "use_field_mapping"; - private static final DeprecationLogger DEPRECATION_LOGGER = DeprecationLogger.getLogger(FetchDocValuesPhase.class); - public static final ParseField FIELD_FIELD = new ParseField("field"); public static final ParseField FORMAT_FIELD = new ParseField("format"); public static final ParseField INCLUDE_UNMAPPED_FIELD = new ParseField("include_unmapped"); @@ -48,28 +43,6 @@ public final class FieldAndFormat implements Writeable, ToXContentObject { PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), INCLUDE_UNMAPPED_FIELD); } - private static CheckedFunction ignoreUseFieldMappingStringParser() { - return (p) -> { - if (p.currentToken() == XContentParser.Token.VALUE_NULL) { - return null; - } else { - String text = p.text(); - if (text.equals(USE_DEFAULT_FORMAT)) { - DEPRECATION_LOGGER.compatibleCritical( - "explicit_default_format", - "[" - + USE_DEFAULT_FORMAT - + "] is a special format that was only used to " - + "ease the transition to 7.x. It has become the default and shouldn't be set explicitly anymore." - ); - return null; - } else { - return text; - } - } - }; - } - /** * Parse a {@link FieldAndFormat} from some {@link XContent}. */ From 344cf15fb146ac7adb9189e54782bc38fd0e1bd9 Mon Sep 17 00:00:00 2001 From: Nick Tindall Date: Fri, 13 Dec 2024 15:40:40 +1100 Subject: [PATCH 71/77] Add undeclared Azure settings, modify test to exercise them (#118634) --- docs/changelog/118634.yaml | 5 ++++ .../azure/AzureBlobStoreRepositoryTests.java | 23 +++++++++++++++++++ .../azure/AzureClientProvider.java | 5 ++++ .../azure/AzureRepositoryPlugin.java | 4 ++++ 4 files changed, 37 insertions(+) create mode 100644 docs/changelog/118634.yaml diff --git a/docs/changelog/118634.yaml b/docs/changelog/118634.yaml new file mode 100644 index 0000000000000..d798d94b72075 --- /dev/null +++ b/docs/changelog/118634.yaml @@ -0,0 +1,5 @@ +pr: 118634 +summary: "Add undeclared Azure settings, modify test to exercise them" +area: Snapshot/Restore +type: bug +issues: [] diff --git a/modules/repository-azure/src/internalClusterTest/java/org/elasticsearch/repositories/azure/AzureBlobStoreRepositoryTests.java b/modules/repository-azure/src/internalClusterTest/java/org/elasticsearch/repositories/azure/AzureBlobStoreRepositoryTests.java index bc1f07fda6240..f3101890d8185 100644 --- a/modules/repository-azure/src/internalClusterTest/java/org/elasticsearch/repositories/azure/AzureBlobStoreRepositoryTests.java +++ b/modules/repository-azure/src/internalClusterTest/java/org/elasticsearch/repositories/azure/AzureBlobStoreRepositoryTests.java @@ -31,6 +31,7 @@ import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.repositories.RepositoriesService; @@ -41,6 +42,7 @@ import org.elasticsearch.telemetry.Measurement; import org.elasticsearch.telemetry.TestTelemetryPlugin; import org.elasticsearch.test.BackgroundIndexer; +import org.elasticsearch.threadpool.ThreadPool; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -53,6 +55,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -75,6 +78,8 @@ public class AzureBlobStoreRepositoryTests extends ESMockAPIBasedRepositoryInteg protected static final String DEFAULT_ACCOUNT_NAME = "account"; protected static final Predicate LIST_PATTERN = Pattern.compile("GET /[a-zA-Z0-9]+/[a-zA-Z0-9]+\\?.+").asMatchPredicate(); protected static final Predicate GET_BLOB_PATTERN = Pattern.compile("GET /[a-zA-Z0-9]+/[a-zA-Z0-9]+/.+").asMatchPredicate(); + private static final AtomicInteger MAX_CONNECTION_SETTING = new AtomicInteger(-1); + private static final AtomicInteger EVENT_LOOP_THREAD_COUNT_SETTING = new AtomicInteger(-1); @Override protected String repositoryType() { @@ -132,9 +137,17 @@ protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { // see com.azure.storage.blob.BlobUrlParts.parseIpUrl final String endpoint = "ignored;DefaultEndpointsProtocol=http;BlobEndpoint=" + httpServerUrl() + "/" + accountName; + + // The first node configured sets these for all nodes + MAX_CONNECTION_SETTING.compareAndSet(-1, randomIntBetween(10, 30)); + EVENT_LOOP_THREAD_COUNT_SETTING.compareAndSet(-1, randomIntBetween(1, 3)); return Settings.builder() .put(super.nodeSettings(nodeOrdinal, otherSettings)) .put(AzureStorageSettings.ENDPOINT_SUFFIX_SETTING.getConcreteSettingForNamespace("test").getKey(), endpoint) + .put(AzureClientProvider.EVENT_LOOP_THREAD_COUNT.getKey(), EVENT_LOOP_THREAD_COUNT_SETTING.get()) + .put(AzureClientProvider.MAX_OPEN_CONNECTIONS.getKey(), MAX_CONNECTION_SETTING.get()) + .put(AzureClientProvider.MAX_IDLE_TIME.getKey(), TimeValue.timeValueSeconds(randomIntBetween(10, 30))) + .put(AzureClientProvider.OPEN_CONNECTION_TIMEOUT.getKey(), TimeValue.timeValueSeconds(randomIntBetween(10, 30))) .setSecureSettings(secureSettings) .build(); } @@ -262,6 +275,16 @@ private boolean isPutBlockList(String request) { } } + public void testSettingsTakeEffect() { + AzureClientProvider azureClientProvider = internalCluster().getInstance(AzureClientProvider.class); + assertEquals(MAX_CONNECTION_SETTING.get(), azureClientProvider.getConnectionProvider().maxConnections()); + ThreadPool nodeThreadPool = internalCluster().getInstance(ThreadPool.class); + assertEquals( + EVENT_LOOP_THREAD_COUNT_SETTING.get(), + nodeThreadPool.info(AzureRepositoryPlugin.NETTY_EVENT_LOOP_THREAD_POOL_NAME).getMax() + ); + } + public void testLargeBlobCountDeletion() throws Exception { int numberOfBlobs = randomIntBetween(257, 2000); try (BlobStore store = newBlobStore()) { diff --git a/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureClientProvider.java b/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureClientProvider.java index f92bbcbdd716d..a9ae9db19a613 100644 --- a/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureClientProvider.java +++ b/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureClientProvider.java @@ -265,6 +265,11 @@ protected void doStop() { @Override protected void doClose() {} + // visible for testing + ConnectionProvider getConnectionProvider() { + return connectionProvider; + } + static class RequestMetrics { private volatile long totalRequestTimeNanos = 0; private volatile int requestCount; diff --git a/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java b/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java index 4556e63378fea..3b945c8118804 100644 --- a/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java +++ b/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepositoryPlugin.java @@ -97,6 +97,10 @@ AzureStorageService createAzureStorageService(Settings settingsToUse, AzureClien @Override public List> getSettings() { return Arrays.asList( + AzureClientProvider.EVENT_LOOP_THREAD_COUNT, + AzureClientProvider.MAX_OPEN_CONNECTIONS, + AzureClientProvider.OPEN_CONNECTION_TIMEOUT, + AzureClientProvider.MAX_IDLE_TIME, AzureStorageSettings.ACCOUNT_SETTING, AzureStorageSettings.KEY_SETTING, AzureStorageSettings.SAS_TOKEN_SETTING, From 1b4f5eb36d06bd0ef8f517b4f8653e5dc253ecd7 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Fri, 13 Dec 2024 08:55:43 +0100 Subject: [PATCH 72/77] [Build] Fix Concurrency issue in buildparams access (#117552) Also provide caching support for buildparams provider * Extract BuildParameterExtension public api into interface * Make tests better readable * Fix test flakyness --- .../internal/PublishPluginFuncTest.groovy | 2 +- .../internal/ElasticsearchJavaBasePlugin.java | 2 +- .../internal/ElasticsearchTestBasePlugin.java | 5 +- .../InternalDistributionBwcSetupPlugin.java | 2 +- .../InternalDistributionDownloadPlugin.java | 5 +- .../internal/InternalTestClustersPlugin.java | 3 +- .../info/BuildParameterExtension.java | 211 ++++----------- .../info/DefaultBuildParameterExtension.java | 245 ++++++++++++++++++ .../internal/info/GlobalBuildInfoPlugin.java | 7 +- .../ThirdPartyAuditPrecommitPlugin.java | 4 +- .../SnykDependencyMonitoringGradlePlugin.java | 4 +- .../internal/test/TestWithSslPlugin.java | 2 +- .../AbstractYamlRestCompatTestPlugin.java | 3 +- .../info/BuildParameterExtensionSpec.groovy | 112 ++++++++ .../fixtures/AbstractGradleFuncTest.groovy | 2 +- 15 files changed, 419 insertions(+), 190 deletions(-) create mode 100644 build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/DefaultBuildParameterExtension.java create mode 100644 build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/info/BuildParameterExtensionSpec.groovy diff --git a/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/PublishPluginFuncTest.groovy b/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/PublishPluginFuncTest.groovy index a199ff9d3eac5..65f124e5f88e8 100644 --- a/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/PublishPluginFuncTest.groovy +++ b/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/PublishPluginFuncTest.groovy @@ -439,7 +439,7 @@ class PublishPluginFuncTest extends AbstractGradleFuncTest { // scm info only added for internal builds internalBuild() buildFile << """ - buildParams.getGitOriginProperty().set("https://some-repo.com/repo.git") + buildParams.setGitOrigin("https://some-repo.com/repo.git") apply plugin:'elasticsearch.java' apply plugin:'elasticsearch.publish' diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java index c897b142da2fb..ee0eb3f6eb2bf 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchJavaBasePlugin.java @@ -132,7 +132,7 @@ private static void disableTransitiveDependenciesForSourceSet(Project project, S public void configureCompile(Project project) { project.getExtensions().getExtraProperties().set("compactProfile", "full"); JavaPluginExtension java = project.getExtensions().getByType(JavaPluginExtension.class); - if (buildParams.getJavaToolChainSpec().isPresent()) { + if (buildParams.getJavaToolChainSpec().getOrNull() != null) { java.toolchain(buildParams.getJavaToolChainSpec().get()); } java.setSourceCompatibility(buildParams.getMinimumRuntimeVersion()); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchTestBasePlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchTestBasePlugin.java index 720d6a7c2efb6..240b55dedf7ce 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchTestBasePlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchTestBasePlugin.java @@ -13,7 +13,6 @@ import org.elasticsearch.gradle.OS; import org.elasticsearch.gradle.internal.conventions.util.Util; -import org.elasticsearch.gradle.internal.info.BuildParameterExtension; import org.elasticsearch.gradle.internal.info.GlobalBuildInfoPlugin; import org.elasticsearch.gradle.internal.test.ErrorReportingTestListener; import org.elasticsearch.gradle.internal.test.SimpleCommandLineArgumentProvider; @@ -27,7 +26,6 @@ import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.FileCollection; import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.provider.Property; import org.gradle.api.provider.ProviderFactory; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; @@ -56,8 +54,7 @@ public abstract class ElasticsearchTestBasePlugin implements Plugin { @Override public void apply(Project project) { project.getRootProject().getPlugins().apply(GlobalBuildInfoPlugin.class); - Property buildParams = loadBuildParams(project); - + var buildParams = loadBuildParams(project); project.getPluginManager().apply(GradleTestPolicySetupPlugin.class); // for fips mode check project.getRootProject().getPluginManager().apply(GlobalBuildInfoPlugin.class); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPlugin.java index c17127f9bbfcf..da26cb66122ad 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionBwcSetupPlugin.java @@ -66,7 +66,7 @@ public void apply(Project project) { project.getRootProject().getPluginManager().apply(GlobalBuildInfoPlugin.class); project.getPlugins().apply(JvmToolchainsPlugin.class); toolChainService = project.getExtensions().getByType(JavaToolchainService.class); - BuildParameterExtension buildParams = loadBuildParams(project).get(); + var buildParams = loadBuildParams(project).get(); Boolean isCi = buildParams.isCi(); buildParams.getBwcVersions().forPreviousUnreleased((BwcVersions.UnreleasedVersionInfo unreleasedVersion) -> { configureBwcProject( diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionDownloadPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionDownloadPlugin.java index ec694de8ec597..ba587aa4bd979 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionDownloadPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionDownloadPlugin.java @@ -20,7 +20,6 @@ import org.elasticsearch.gradle.internal.distribution.InternalElasticsearchDistributionTypes; import org.elasticsearch.gradle.internal.docker.DockerSupportPlugin; import org.elasticsearch.gradle.internal.docker.DockerSupportService; -import org.elasticsearch.gradle.internal.info.BuildParameterExtension; import org.elasticsearch.gradle.internal.info.GlobalBuildInfoPlugin; import org.elasticsearch.gradle.util.GradleUtils; import org.gradle.api.GradleException; @@ -49,7 +48,7 @@ public void apply(Project project) { // this is needed for isInternal project.getRootProject().getPluginManager().apply(GlobalBuildInfoPlugin.class); project.getRootProject().getPluginManager().apply(DockerSupportPlugin.class); - BuildParameterExtension buildParams = loadBuildParams(project).get(); + var buildParams = loadBuildParams(project).get(); DistributionDownloadPlugin distributionDownloadPlugin = project.getPlugins().apply(DistributionDownloadPlugin.class); Provider dockerSupport = GradleUtils.getBuildService( @@ -61,7 +60,7 @@ public void apply(Project project) { ); registerInternalDistributionResolutions( DistributionDownloadPlugin.getRegistrationsContainer(project), - buildParams.getBwcVersionsProperty() + buildParams.getBwcVersionsProvider() ); } diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalTestClustersPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalTestClustersPlugin.java index 7e7ffad12a9a5..c618fe6c2e1bf 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalTestClustersPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/InternalTestClustersPlugin.java @@ -10,7 +10,6 @@ package org.elasticsearch.gradle.internal; import org.elasticsearch.gradle.VersionProperties; -import org.elasticsearch.gradle.internal.info.BuildParameterExtension; import org.elasticsearch.gradle.internal.info.GlobalBuildInfoPlugin; import org.elasticsearch.gradle.testclusters.ElasticsearchCluster; import org.elasticsearch.gradle.testclusters.TestClustersPlugin; @@ -26,7 +25,7 @@ public class InternalTestClustersPlugin implements Plugin { public void apply(Project project) { project.getPlugins().apply(InternalDistributionDownloadPlugin.class); project.getRootProject().getRootProject().getPlugins().apply(GlobalBuildInfoPlugin.class); - BuildParameterExtension buildParams = loadBuildParams(project).get(); + var buildParams = loadBuildParams(project).get(); project.getRootProject().getPluginManager().apply(InternalReaperPlugin.class); TestClustersPlugin testClustersPlugin = project.getPlugins().apply(TestClustersPlugin.class); testClustersPlugin.setRuntimeJava(buildParams.getRuntimeJavaHome()); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/BuildParameterExtension.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/BuildParameterExtension.java index 5531194e0abde..e80dc6ef1b44c 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/BuildParameterExtension.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/BuildParameterExtension.java @@ -13,175 +13,58 @@ import org.gradle.api.Action; import org.gradle.api.JavaVersion; import org.gradle.api.Task; -import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; -import org.gradle.api.provider.ProviderFactory; import org.gradle.jvm.toolchain.JavaToolchainSpec; import java.io.File; -import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.List; import java.util.Random; -import java.util.concurrent.atomic.AtomicReference; - -public abstract class BuildParameterExtension { - private final Provider inFipsJvm; - private final Provider runtimeJavaHome; - private final Boolean isRuntimeJavaHomeSet; - private final List javaVersions; - private final JavaVersion minimumCompilerVersion; - private final JavaVersion minimumRuntimeVersion; - private final JavaVersion gradleJavaVersion; - private final Provider runtimeJavaVersion; - private final Provider> javaToolChainSpec; - private final Provider runtimeJavaDetails; - private final String gitRevision; - private transient AtomicReference buildDate = new AtomicReference<>(); - private final String testSeed; - private final Boolean isCi; - private final Integer defaultParallel; - private final Boolean isSnapshotBuild; - - public BuildParameterExtension( - ProviderFactory providers, - Provider runtimeJavaHome, - Provider> javaToolChainSpec, - Provider runtimeJavaVersion, - boolean isRuntimeJavaHomeSet, - Provider runtimeJavaDetails, - List javaVersions, - JavaVersion minimumCompilerVersion, - JavaVersion minimumRuntimeVersion, - JavaVersion gradleJavaVersion, - String gitRevision, - String gitOrigin, - ZonedDateTime buildDate, - String testSeed, - boolean isCi, - int defaultParallel, - final boolean isSnapshotBuild, - Provider bwcVersions - ) { - this.inFipsJvm = providers.systemProperty("tests.fips.enabled").map(BuildParameterExtension::parseBoolean); - this.runtimeJavaHome = runtimeJavaHome; - this.javaToolChainSpec = javaToolChainSpec; - this.runtimeJavaVersion = runtimeJavaVersion; - this.isRuntimeJavaHomeSet = isRuntimeJavaHomeSet; - this.runtimeJavaDetails = runtimeJavaDetails; - this.javaVersions = javaVersions; - this.minimumCompilerVersion = minimumCompilerVersion; - this.minimumRuntimeVersion = minimumRuntimeVersion; - this.gradleJavaVersion = gradleJavaVersion; - this.gitRevision = gitRevision; - this.testSeed = testSeed; - this.isCi = isCi; - this.defaultParallel = defaultParallel; - this.isSnapshotBuild = isSnapshotBuild; - this.getBwcVersionsProperty().set(bwcVersions); - this.getGitOriginProperty().set(gitOrigin); - } - - private static boolean parseBoolean(String s) { - if (s == null) { - return false; - } - return Boolean.parseBoolean(s); - } - - public boolean getInFipsJvm() { - return inFipsJvm.getOrElse(false); - } - - public Provider getRuntimeJavaHome() { - return runtimeJavaHome; - } - - public void withFipsEnabledOnly(Task task) { - task.onlyIf("FIPS mode disabled", task1 -> getInFipsJvm() == false); - } - - public Boolean getIsRuntimeJavaHomeSet() { - return isRuntimeJavaHomeSet; - } - - public List getJavaVersions() { - return javaVersions; - } - - public JavaVersion getMinimumCompilerVersion() { - return minimumCompilerVersion; - } - - public JavaVersion getMinimumRuntimeVersion() { - return minimumRuntimeVersion; - } - - public JavaVersion getGradleJavaVersion() { - return gradleJavaVersion; - } - - public Provider getRuntimeJavaVersion() { - return runtimeJavaVersion; - } - - public Provider> getJavaToolChainSpec() { - return javaToolChainSpec; - } - - public Provider getRuntimeJavaDetails() { - return runtimeJavaDetails; - } - - public String getGitRevision() { - return gitRevision; - } - - public String getGitOrigin() { - return getGitOriginProperty().get(); - } - - public ZonedDateTime getBuildDate() { - ZonedDateTime value = buildDate.get(); - if (value == null) { - value = ZonedDateTime.now(ZoneOffset.UTC); - if (buildDate.compareAndSet(null, value) == false) { - // If another thread initialized it first, return the initialized value - value = buildDate.get(); - } - } - return value; - } - - public String getTestSeed() { - return testSeed; - } - - public Boolean isCi() { - return isCi; - } - - public Integer getDefaultParallel() { - return defaultParallel; - } - - public Boolean isSnapshotBuild() { - return isSnapshotBuild; - } - - public BwcVersions getBwcVersions() { - return getBwcVersionsProperty().get(); - } - - public abstract Property getBwcVersionsProperty(); - - public abstract Property getGitOriginProperty(); - - public Random getRandom() { - return new Random(Long.parseUnsignedLong(testSeed.split(":")[0], 16)); - } - - public Boolean isGraalVmRuntime() { - return runtimeJavaDetails.get().toLowerCase().contains("graalvm"); - } + +public interface BuildParameterExtension { + String EXTENSION_NAME = "buildParams"; + + boolean getInFipsJvm(); + + Provider getRuntimeJavaHome(); + + void withFipsEnabledOnly(Task task); + + Boolean getIsRuntimeJavaHomeSet(); + + List getJavaVersions(); + + JavaVersion getMinimumCompilerVersion(); + + JavaVersion getMinimumRuntimeVersion(); + + JavaVersion getGradleJavaVersion(); + + Provider getRuntimeJavaVersion(); + + Provider> getJavaToolChainSpec(); + + Provider getRuntimeJavaDetails(); + + String getGitRevision(); + + String getGitOrigin(); + + ZonedDateTime getBuildDate(); + + String getTestSeed(); + + Boolean isCi(); + + Integer getDefaultParallel(); + + Boolean isSnapshotBuild(); + + BwcVersions getBwcVersions(); + + Provider getBwcVersionsProvider(); + + Random getRandom(); + + Boolean isGraalVmRuntime(); } diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/DefaultBuildParameterExtension.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/DefaultBuildParameterExtension.java new file mode 100644 index 0000000000000..faac406d974c6 --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/DefaultBuildParameterExtension.java @@ -0,0 +1,245 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.gradle.internal.info; + +import org.elasticsearch.gradle.internal.BwcVersions; +import org.gradle.api.Action; +import org.gradle.api.JavaVersion; +import org.gradle.api.Task; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; +import org.gradle.jvm.toolchain.JavaToolchainSpec; + +import java.io.File; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +public abstract class DefaultBuildParameterExtension implements BuildParameterExtension { + private final Provider inFipsJvm; + private final Provider runtimeJavaHome; + private final Boolean isRuntimeJavaHomeSet; + private final List javaVersions; + private final JavaVersion minimumCompilerVersion; + private final JavaVersion minimumRuntimeVersion; + private final JavaVersion gradleJavaVersion; + private final Provider runtimeJavaVersion; + private final Provider> javaToolChainSpec; + private final Provider runtimeJavaDetails; + private final String gitRevision; + + private transient AtomicReference buildDate = new AtomicReference<>(); + private final String testSeed; + private final Boolean isCi; + private final Integer defaultParallel; + private final Boolean isSnapshotBuild; + + // not final for testing + private Provider bwcVersions; + private String gitOrigin; + + public DefaultBuildParameterExtension( + ProviderFactory providers, + Provider runtimeJavaHome, + Provider> javaToolChainSpec, + Provider runtimeJavaVersion, + boolean isRuntimeJavaHomeSet, + Provider runtimeJavaDetails, + List javaVersions, + JavaVersion minimumCompilerVersion, + JavaVersion minimumRuntimeVersion, + JavaVersion gradleJavaVersion, + String gitRevision, + String gitOrigin, + String testSeed, + boolean isCi, + int defaultParallel, + final boolean isSnapshotBuild, + Provider bwcVersions + ) { + this.inFipsJvm = providers.systemProperty("tests.fips.enabled").map(DefaultBuildParameterExtension::parseBoolean); + this.runtimeJavaHome = cache(providers, runtimeJavaHome); + this.javaToolChainSpec = cache(providers, javaToolChainSpec); + this.runtimeJavaVersion = cache(providers, runtimeJavaVersion); + this.isRuntimeJavaHomeSet = isRuntimeJavaHomeSet; + this.runtimeJavaDetails = cache(providers, runtimeJavaDetails); + this.javaVersions = javaVersions; + this.minimumCompilerVersion = minimumCompilerVersion; + this.minimumRuntimeVersion = minimumRuntimeVersion; + this.gradleJavaVersion = gradleJavaVersion; + this.gitRevision = gitRevision; + this.testSeed = testSeed; + this.isCi = isCi; + this.defaultParallel = defaultParallel; + this.isSnapshotBuild = isSnapshotBuild; + this.bwcVersions = cache(providers, bwcVersions); + this.gitOrigin = gitOrigin; + } + + // This is a workaround for https://github.com/gradle/gradle/issues/25550 + private Provider cache(ProviderFactory providerFactory, Provider incomingProvider) { + SingleObjectCache cache = new SingleObjectCache<>(); + return providerFactory.provider(() -> cache.computeIfAbsent(() -> incomingProvider.getOrNull())); + } + + private static boolean parseBoolean(String s) { + if (s == null) { + return false; + } + return Boolean.parseBoolean(s); + } + + @Override + public boolean getInFipsJvm() { + return inFipsJvm.getOrElse(false); + } + + @Override + public Provider getRuntimeJavaHome() { + return runtimeJavaHome; + } + + @Override + public void withFipsEnabledOnly(Task task) { + task.onlyIf("FIPS mode disabled", task1 -> getInFipsJvm() == false); + } + + @Override + public Boolean getIsRuntimeJavaHomeSet() { + return isRuntimeJavaHomeSet; + } + + @Override + public List getJavaVersions() { + return javaVersions; + } + + @Override + public JavaVersion getMinimumCompilerVersion() { + return minimumCompilerVersion; + } + + @Override + public JavaVersion getMinimumRuntimeVersion() { + return minimumRuntimeVersion; + } + + @Override + public JavaVersion getGradleJavaVersion() { + return gradleJavaVersion; + } + + @Override + public Provider getRuntimeJavaVersion() { + return runtimeJavaVersion; + } + + @Override + public Provider> getJavaToolChainSpec() { + return javaToolChainSpec; + } + + @Override + public Provider getRuntimeJavaDetails() { + return runtimeJavaDetails; + } + + @Override + public String getGitRevision() { + return gitRevision; + } + + @Override + public String getGitOrigin() { + return gitOrigin; + } + + @Override + public ZonedDateTime getBuildDate() { + ZonedDateTime value = buildDate.get(); + if (value == null) { + value = ZonedDateTime.now(ZoneOffset.UTC); + if (buildDate.compareAndSet(null, value) == false) { + // If another thread initialized it first, return the initialized value + value = buildDate.get(); + } + } + return value; + } + + @Override + public String getTestSeed() { + return testSeed; + } + + @Override + public Boolean isCi() { + return isCi; + } + + @Override + public Integer getDefaultParallel() { + return defaultParallel; + } + + @Override + public Boolean isSnapshotBuild() { + return isSnapshotBuild; + } + + @Override + public BwcVersions getBwcVersions() { + return bwcVersions.get(); + } + + @Override + public Random getRandom() { + return new Random(Long.parseUnsignedLong(testSeed.split(":")[0], 16)); + } + + @Override + public Boolean isGraalVmRuntime() { + return runtimeJavaDetails.get().toLowerCase().contains("graalvm"); + } + + private static class SingleObjectCache { + private T instance; + + public T computeIfAbsent(Supplier supplier) { + synchronized (this) { + if (instance == null) { + instance = supplier.get(); + } + return instance; + } + } + + public T get() { + return instance; + } + } + + public Provider getBwcVersionsProvider() { + return bwcVersions; + } + + // for testing; not part of public api + public void setBwcVersions(Provider bwcVersions) { + this.bwcVersions = bwcVersions; + } + + // for testing; not part of public api + public void setGitOrigin(String gitOrigin) { + this.gitOrigin = gitOrigin; + } +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java index 27d2a66feb206..86f59aa0ab41e 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/info/GlobalBuildInfoPlugin.java @@ -51,8 +51,6 @@ import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.nio.file.Files; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -124,8 +122,10 @@ public void apply(Project project) { ); BuildParameterExtension buildParams = project.getExtensions() .create( - "buildParams", BuildParameterExtension.class, + BuildParameterExtension.EXTENSION_NAME, + DefaultBuildParameterExtension.class, + providers, actualRuntimeJavaHome, resolveToolchainSpecFromEnv(), actualRuntimeJavaHome.map( @@ -145,7 +145,6 @@ public void apply(Project project) { Jvm.current().getJavaVersion(), gitInfo.getRevision(), gitInfo.getOrigin(), - ZonedDateTime.now(ZoneOffset.UTC), getTestSeed(), System.getenv("JENKINS_URL") != null || System.getenv("BUILDKITE_BUILD_URL") != null || System.getProperty("isCI") != null, ParallelDetector.findDefaultParallel(project), diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ThirdPartyAuditPrecommitPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ThirdPartyAuditPrecommitPlugin.java index f70e25a57e331..e45a1d3dd25b1 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ThirdPartyAuditPrecommitPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/precommit/ThirdPartyAuditPrecommitPlugin.java @@ -12,12 +12,10 @@ import org.elasticsearch.gradle.dependencies.CompileOnlyResolvePlugin; import org.elasticsearch.gradle.internal.ExportElasticsearchBuildResourcesTask; import org.elasticsearch.gradle.internal.conventions.precommit.PrecommitPlugin; -import org.elasticsearch.gradle.internal.info.BuildParameterExtension; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.component.ModuleComponentIdentifier; -import org.gradle.api.provider.Property; import org.gradle.api.tasks.TaskProvider; import java.io.File; @@ -34,7 +32,7 @@ public class ThirdPartyAuditPrecommitPlugin extends PrecommitPlugin { @Override public TaskProvider createTask(Project project) { project.getRootProject().getPlugins().apply(CompileOnlyResolvePlugin.class); - Property buildParams = loadBuildParams(project); + var buildParams = loadBuildParams(project); project.getPlugins().apply(CompileOnlyResolvePlugin.class); project.getConfigurations().create("forbiddenApisCliJar"); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/snyk/SnykDependencyMonitoringGradlePlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/snyk/SnykDependencyMonitoringGradlePlugin.java index fa10daf8dfaaa..704394b4f01a9 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/snyk/SnykDependencyMonitoringGradlePlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/snyk/SnykDependencyMonitoringGradlePlugin.java @@ -10,7 +10,6 @@ package org.elasticsearch.gradle.internal.snyk; import org.elasticsearch.gradle.internal.conventions.info.GitInfo; -import org.elasticsearch.gradle.internal.info.BuildParameterExtension; import org.elasticsearch.gradle.internal.info.GlobalBuildInfoPlugin; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -18,7 +17,6 @@ import org.gradle.api.file.ProjectLayout; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; -import org.gradle.api.provider.Property; import org.gradle.api.provider.ProviderFactory; import org.gradle.api.tasks.SourceSet; @@ -41,7 +39,7 @@ public SnykDependencyMonitoringGradlePlugin(ProjectLayout projectLayout, Provide @Override public void apply(Project project) { project.getRootProject().getPlugins().apply(GlobalBuildInfoPlugin.class); - Property buildParams = loadBuildParams(project); + var buildParams = loadBuildParams(project); var generateTaskProvider = project.getTasks() .register("generateSnykDependencyGraph", GenerateSnykDependencyGraph.class, generateSnykDependencyGraph -> { diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/TestWithSslPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/TestWithSslPlugin.java index 68711881b02f4..94018d1501e0b 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/TestWithSslPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/TestWithSslPlugin.java @@ -35,7 +35,7 @@ public class TestWithSslPlugin implements Plugin { @Override public void apply(Project project) { File keyStoreDir = new File(project.getBuildDir(), "keystore"); - BuildParameterExtension buildParams = project.getRootProject().getExtensions().getByType(BuildParameterExtension.class); + var buildParams = project.getRootProject().getExtensions().getByType(BuildParameterExtension.class); TaskProvider exportKeyStore = project.getTasks() .register("copyTestCertificates", ExportElasticsearchBuildResourcesTask.class, (t) -> { t.copy("test/ssl/test-client.crt"); diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/AbstractYamlRestCompatTestPlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/AbstractYamlRestCompatTestPlugin.java index ca669276123b3..b511702d1c7c3 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/AbstractYamlRestCompatTestPlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/rest/compat/compat/AbstractYamlRestCompatTestPlugin.java @@ -11,7 +11,6 @@ import org.elasticsearch.gradle.Version; import org.elasticsearch.gradle.internal.ElasticsearchJavaBasePlugin; -import org.elasticsearch.gradle.internal.info.BuildParameterExtension; import org.elasticsearch.gradle.internal.info.GlobalBuildInfoPlugin; import org.elasticsearch.gradle.internal.test.rest.CopyRestApiTask; import org.elasticsearch.gradle.internal.test.rest.CopyRestTestsTask; @@ -78,7 +77,7 @@ public AbstractYamlRestCompatTestPlugin(ProjectLayout projectLayout, FileOperati @Override public void apply(Project project) { project.getRootProject().getRootProject().getPlugins().apply(GlobalBuildInfoPlugin.class); - BuildParameterExtension buildParams = loadBuildParams(project).get(); + var buildParams = loadBuildParams(project).get(); final Path compatRestResourcesDir = Path.of("restResources").resolve("compat"); final Path compatSpecsDir = compatRestResourcesDir.resolve("yamlSpecs"); diff --git a/build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/info/BuildParameterExtensionSpec.groovy b/build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/info/BuildParameterExtensionSpec.groovy new file mode 100644 index 0000000000000..343268b9b4d47 --- /dev/null +++ b/build-tools-internal/src/test/groovy/org/elasticsearch/gradle/internal/info/BuildParameterExtensionSpec.groovy @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.gradle.internal.info + +import spock.lang.Specification + +import org.elasticsearch.gradle.internal.BwcVersions +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.api.provider.Provider +import org.gradle.api.provider.ProviderFactory +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger + +import static org.junit.Assert.fail + +class BuildParameterExtensionSpec extends Specification { + + ProjectBuilder projectBuilder = new ProjectBuilder() + + def "#getterName is cached anc concurrently accessible"() { + given: + def project = projectBuilder.build() + def providers = project.getProviders(); + def buildParams = extension(project, providers) + int numberOfThreads = 10; + when: + var service = Executors.newFixedThreadPool(numberOfThreads) + var latch = new CountDownLatch(numberOfThreads) + def testedProvider = buildParams."$getterName"() + def futures = (1..numberOfThreads).collect { + service.submit( + () -> { + try { + testedProvider.get() + } catch (AssertionError e) { + latch.countDown() + Assert.fail("Accessing cached provider more than once") + } + latch.countDown() + } + ) + } + latch.await(10, TimeUnit.SECONDS) + + then: + futures.collect { it.state() }.any() { it == Future.State.FAILED } == false + + where: + getterName << [ + "getRuntimeJavaHome", + "getJavaToolChainSpec", + "getRuntimeJavaDetails", + "getRuntimeJavaVersion", + "getBwcVersionsProvider" + ] + } + + private BuildParameterExtension extension(Project project, ProviderFactory providers) { + return project.getExtensions().create( + BuildParameterExtension.class, "buildParameters", DefaultBuildParameterExtension.class, + providers, + providerMock(), + providerMock(), + providerMock(), + true, + providerMock(), + [ + Mock(JavaHome), + Mock(JavaHome), + ], + JavaVersion.VERSION_11, + JavaVersion.VERSION_11, + JavaVersion.VERSION_11, + "gitRevision", + "gitOrigin", + "testSeed", + false, + 5, + true, + // cannot use Mock here because of the way the provider is used by gradle internal property api + providerMock() + ) + } + + private Provider providerMock() { + Provider provider = Mock(Provider) + AtomicInteger counter = new AtomicInteger(0) + provider.getOrNull() >> { + println "accessing provider" + return counter.get() == 1 ? fail("Accessing cached provider more than once") : counter.incrementAndGet() + } + provider.get() >> { + fail("Accessing cached provider directly") + } + return provider + + } +} diff --git a/build-tools/src/testFixtures/groovy/org/elasticsearch/gradle/fixtures/AbstractGradleFuncTest.groovy b/build-tools/src/testFixtures/groovy/org/elasticsearch/gradle/fixtures/AbstractGradleFuncTest.groovy index 07214b5fbf845..fe23204d5601c 100644 --- a/build-tools/src/testFixtures/groovy/org/elasticsearch/gradle/fixtures/AbstractGradleFuncTest.groovy +++ b/build-tools/src/testFixtures/groovy/org/elasticsearch/gradle/fixtures/AbstractGradleFuncTest.groovy @@ -183,7 +183,7 @@ abstract class AbstractGradleFuncTest extends Specification { ] BwcVersions versions = new BwcVersions(currentVersion, versionList, ['main', '8.x', '8.3', '8.2', '8.1', '7.16']) - buildParams.getBwcVersionsProperty().set(versions) + buildParams.setBwcVersions(project.provider { versions} ) """ } From 23008be7fba1af1c1ea038dda914530d795fa3c3 Mon Sep 17 00:00:00 2001 From: Valeriy Khakhutskyy <1292899+valeriy42@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:33:33 +0100 Subject: [PATCH 73/77] [ML] Simplify minimum supported snapshot version handling for Machine Learning jobs (#118549) Since in 9.0 we don't need to support snapshots prior to 7.17, we can simplify the changes made in #81039 and re-introduce a single contant to manage the minimum supported snapshot version. --- .../elasticsearch/xpack/core/ml/MachineLearningField.java | 8 ++------ .../elasticsearch/xpack/deprecation/MlDeprecationIT.java | 2 +- .../xpack/deprecation/MlDeprecationChecker.java | 7 +++---- .../xpack/ml/integration/AnomalyJobCRUDIT.java | 2 +- .../xpack/ml/action/TransportOpenJobAction.java | 7 +++---- .../xpack/ml/job/task/OpenJobPersistentTasksExecutor.java | 7 +++---- 6 files changed, 13 insertions(+), 20 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MachineLearningField.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MachineLearningField.java index 3a37f94e6b2d4..a40babb2760fb 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MachineLearningField.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MachineLearningField.java @@ -64,12 +64,8 @@ public final class MachineLearningField { License.OperationMode.PLATINUM ); - // Ideally this would be 7.0.0, but it has to be 6.4.0 because due to an oversight it's impossible - // for the Java code to distinguish the model states for versions 6.4.0 to 7.9.3 inclusive. - public static final MlConfigVersion MIN_CHECKED_SUPPORTED_SNAPSHOT_VERSION = MlConfigVersion.fromString("6.4.0"); - // We tell the user we support model snapshots newer than 7.0.0 as that's the major version - // boundary, even though behind the scenes we have to support back to 6.4.0. - public static final MlConfigVersion MIN_REPORTED_SUPPORTED_SNAPSHOT_VERSION = MlConfigVersion.V_7_0_0; + // This is the last version when we changed the ML job snapshot format. + public static final MlConfigVersion MIN_SUPPORTED_SNAPSHOT_VERSION = MlConfigVersion.V_8_3_0; private MachineLearningField() {} diff --git a/x-pack/plugin/deprecation/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/deprecation/MlDeprecationIT.java b/x-pack/plugin/deprecation/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/deprecation/MlDeprecationIT.java index 6d95038e2cbcc..54a48ab34e991 100644 --- a/x-pack/plugin/deprecation/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/deprecation/MlDeprecationIT.java +++ b/x-pack/plugin/deprecation/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/deprecation/MlDeprecationIT.java @@ -63,7 +63,7 @@ public void testMlDeprecationChecks() throws Exception { indexDoc( ".ml-anomalies-.write-" + jobId, jobId + "_model_snapshot_2", - "{\"job_id\":\"deprecation_check_job\",\"snapshot_id\":\"2\",\"snapshot_doc_count\":1,\"min_version\":\"8.0.0\"}" + "{\"job_id\":\"deprecation_check_job\",\"snapshot_id\":\"2\",\"snapshot_doc_count\":1,\"min_version\":\"8.3.0\"}" ); client().performRequest(new Request("POST", "/.ml-anomalies-*/_refresh")); diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/MlDeprecationChecker.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/MlDeprecationChecker.java index c0e1c054f7a13..88adfe5157418 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/MlDeprecationChecker.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/MlDeprecationChecker.java @@ -26,8 +26,7 @@ import java.util.Map; import java.util.Optional; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.MIN_CHECKED_SUPPORTED_SNAPSHOT_VERSION; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.MIN_REPORTED_SUPPORTED_SNAPSHOT_VERSION; +import static org.elasticsearch.xpack.core.ml.MachineLearningField.MIN_SUPPORTED_SNAPSHOT_VERSION; public class MlDeprecationChecker implements DeprecationChecker { @@ -69,13 +68,13 @@ static Optional checkDataFeedAggregations(DatafeedConfig dataf } static Optional checkModelSnapshot(ModelSnapshot modelSnapshot) { - if (modelSnapshot.getMinVersion().before(MIN_CHECKED_SUPPORTED_SNAPSHOT_VERSION)) { + if (modelSnapshot.getMinVersion().before(MIN_SUPPORTED_SNAPSHOT_VERSION)) { StringBuilder details = new StringBuilder( String.format( Locale.ROOT, "Delete model snapshot [%s] or update it to %s or greater.", modelSnapshot.getSnapshotId(), - MIN_REPORTED_SUPPORTED_SNAPSHOT_VERSION + MIN_SUPPORTED_SNAPSHOT_VERSION ) ); if (modelSnapshot.getLatestRecordTimeStamp() != null) { diff --git a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/AnomalyJobCRUDIT.java b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/AnomalyJobCRUDIT.java index 08fda90f9fd73..8fe87b043c78b 100644 --- a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/AnomalyJobCRUDIT.java +++ b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/AnomalyJobCRUDIT.java @@ -195,7 +195,7 @@ public void testOpenJobWithOldSnapshot() { assertThat( ex.getMessage(), containsString( - "[open-job-with-old-model-snapshot] job model snapshot [snap_1] has min version before [7.0.0], " + "[open-job-with-old-model-snapshot] job model snapshot [snap_1] has min version before [8.3.0], " + "please revert to a newer model snapshot or reset the job" ) ); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportOpenJobAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportOpenJobAction.java index bd628c4e04ac6..6da5a110defbf 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportOpenJobAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportOpenJobAction.java @@ -58,8 +58,7 @@ import static org.elasticsearch.core.Strings.format; import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.MIN_CHECKED_SUPPORTED_SNAPSHOT_VERSION; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.MIN_REPORTED_SUPPORTED_SNAPSHOT_VERSION; +import static org.elasticsearch.xpack.core.ml.MachineLearningField.MIN_SUPPORTED_SNAPSHOT_VERSION; import static org.elasticsearch.xpack.ml.job.task.OpenJobPersistentTasksExecutor.checkAssignmentState; /* @@ -214,7 +213,7 @@ public void onFailure(Exception e) { return; } assert modelSnapshot.getPage().results().size() == 1; - if (modelSnapshot.getPage().results().get(0).getMinVersion().onOrAfter(MIN_CHECKED_SUPPORTED_SNAPSHOT_VERSION)) { + if (modelSnapshot.getPage().results().get(0).getMinVersion().onOrAfter(MIN_SUPPORTED_SNAPSHOT_VERSION)) { modelSnapshotValidationListener.onResponse(true); return; } @@ -224,7 +223,7 @@ public void onFailure(Exception e) { + "please revert to a newer model snapshot or reset the job", jobParams.getJobId(), jobParams.getJob().getModelSnapshotId(), - MIN_REPORTED_SUPPORTED_SNAPSHOT_VERSION.toString() + MIN_SUPPORTED_SNAPSHOT_VERSION.toString() ) ); }, failure -> { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/task/OpenJobPersistentTasksExecutor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/task/OpenJobPersistentTasksExecutor.java index 89180cba77dfd..9c37ebc0abfd8 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/task/OpenJobPersistentTasksExecutor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/task/OpenJobPersistentTasksExecutor.java @@ -73,8 +73,7 @@ import static org.elasticsearch.core.Strings.format; import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.MIN_CHECKED_SUPPORTED_SNAPSHOT_VERSION; -import static org.elasticsearch.xpack.core.ml.MachineLearningField.MIN_REPORTED_SUPPORTED_SNAPSHOT_VERSION; +import static org.elasticsearch.xpack.core.ml.MachineLearningField.MIN_SUPPORTED_SNAPSHOT_VERSION; import static org.elasticsearch.xpack.core.ml.MlTasks.AWAITING_UPGRADE; import static org.elasticsearch.xpack.core.ml.MlTasks.PERSISTENT_TASK_MASTER_NODE_TIMEOUT; import static org.elasticsearch.xpack.ml.job.JobNodeSelector.AWAITING_LAZY_ASSIGNMENT; @@ -436,7 +435,7 @@ private void verifyCurrentSnapshotVersion(String jobId, ActionListener } assert snapshot.getPage().results().size() == 1; ModelSnapshot snapshotObj = snapshot.getPage().results().get(0); - if (snapshotObj.getMinVersion().onOrAfter(MIN_CHECKED_SUPPORTED_SNAPSHOT_VERSION)) { + if (snapshotObj.getMinVersion().onOrAfter(MIN_SUPPORTED_SNAPSHOT_VERSION)) { listener.onResponse(true); return; } @@ -446,7 +445,7 @@ private void verifyCurrentSnapshotVersion(String jobId, ActionListener + "please revert to a newer model snapshot or reset the job", jobId, jobSnapshotId, - MIN_REPORTED_SUPPORTED_SNAPSHOT_VERSION.toString() + MIN_SUPPORTED_SNAPSHOT_VERSION.toString() ) ); }, snapshotFailure -> { From 4ff5acccbed76e154758de49c4b1866f781d721a Mon Sep 17 00:00:00 2001 From: Alexander Spies Date: Fri, 13 Dec 2024 10:51:58 +0100 Subject: [PATCH 74/77] ESQL: push down LIMIT past LOOKUP JOIN (#118495) Fix https://github.com/elastic/elasticsearch/issues/117698 by enabling push down of `LIMIT` past `LEFT JOIN`s. There is a subtle point here: our `LOOKUP JOIN` currently _exactly preserves the number of rows from the left hand side_. This is different from SQL, where `LEFT JOIN` will return _at least one row for each row from the left_, but may return multiple rows in case of multiple matches. We, instead, throw multiple matches into multi-values, instead. (C.f. [tests that I'm about to add](https://github.com/elastic/elasticsearch/pull/118471/files#diff-334f3328c5f066a093ed8a5ea4a62cd6bcdb304b660b15763bb4f64d0e87ed7cR365-R369) that demonstrate this.) If we were to change our semantics to match SQL's, we'd have to adjust the pushdown, too. --- .../esql/qa/mixed/MixedClusterEsqlSpecIT.java | 4 +- .../xpack/esql/ccq/MultiClusterSpecIT.java | 8 +-- .../src/main/resources/lookup-join.csv-spec | 59 ++++++++++++------- .../xpack/esql/action/EsqlCapabilities.java | 2 +- .../logical/PushDownAndCombineLimits.java | 7 ++- .../elasticsearch/xpack/esql/CsvTests.java | 2 +- .../xpack/esql/analysis/AnalyzerTests.java | 4 +- .../xpack/esql/analysis/VerifierTests.java | 2 +- .../optimizer/LogicalPlanOptimizerTests.java | 24 ++++++++ 9 files changed, 77 insertions(+), 35 deletions(-) diff --git a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java index 81070b3155f2e..1120a69cc5166 100644 --- a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java +++ b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/MixedClusterEsqlSpecIT.java @@ -21,7 +21,7 @@ import java.util.List; import static org.elasticsearch.xpack.esql.CsvTestUtils.isEnabled; -import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V4; +import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V5; import static org.elasticsearch.xpack.esql.qa.rest.EsqlSpecTestCase.Mode.ASYNC; public class MixedClusterEsqlSpecIT extends EsqlSpecTestCase { @@ -96,7 +96,7 @@ protected boolean supportsInferenceTestService() { @Override protected boolean supportsIndexModeLookup() throws IOException { - return hasCapabilities(List.of(JOIN_LOOKUP_V4.capabilityName())); + return hasCapabilities(List.of(JOIN_LOOKUP_V5.capabilityName())); } @Override diff --git a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java index 2ec75683ab149..5c7f981c93a97 100644 --- a/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java +++ b/x-pack/plugin/esql/qa/server/multi-clusters/src/javaRestTest/java/org/elasticsearch/xpack/esql/ccq/MultiClusterSpecIT.java @@ -48,7 +48,7 @@ import static org.elasticsearch.xpack.esql.EsqlTestUtils.classpathResources; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.INLINESTATS; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.INLINESTATS_V2; -import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V4; +import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_LOOKUP_V5; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.JOIN_PLANNING_V1; import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.METADATA_FIELDS_REMOTE_TEST; import static org.elasticsearch.xpack.esql.qa.rest.EsqlSpecTestCase.Mode.SYNC; @@ -124,7 +124,7 @@ protected void shouldSkipTest(String testName) throws IOException { assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(INLINESTATS.capabilityName())); assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(INLINESTATS_V2.capabilityName())); assumeFalse("INLINESTATS not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_PLANNING_V1.capabilityName())); - assumeFalse("LOOKUP JOIN not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_LOOKUP_V4.capabilityName())); + assumeFalse("LOOKUP JOIN not yet supported in CCS", testCase.requiredCapabilities.contains(JOIN_LOOKUP_V5.capabilityName())); } private TestFeatureService remoteFeaturesService() throws IOException { @@ -283,8 +283,8 @@ protected boolean supportsInferenceTestService() { @Override protected boolean supportsIndexModeLookup() throws IOException { - // CCS does not yet support JOIN_LOOKUP_V4 and clusters falsely report they have this capability - // return hasCapabilities(List.of(JOIN_LOOKUP_V4.capabilityName())); + // CCS does not yet support JOIN_LOOKUP_V5 and clusters falsely report they have this capability + // return hasCapabilities(List.of(JOIN_LOOKUP_V5.capabilityName())); return false; } } diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec index b01e12fa4f470..12e333c0ed9f2 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/lookup-join.csv-spec @@ -5,7 +5,7 @@ //TODO: this sometimes returns null instead of the looked up value (likely related to the execution order) basicOnTheDataNode -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM employees | EVAL language_code = languages @@ -22,7 +22,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; basicRow -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 ROW language_code = 1 | LOOKUP JOIN languages_lookup ON language_code @@ -33,7 +33,7 @@ language_code:integer | language_name:keyword ; basicOnTheCoordinator -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM employees | SORT emp_no @@ -50,7 +50,7 @@ emp_no:integer | language_code:integer | language_name:keyword ; subsequentEvalOnTheDataNode -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM employees | EVAL language_code = languages @@ -68,7 +68,7 @@ emp_no:integer | language_code:integer | language_name:keyword | language_code_x ; subsequentEvalOnTheCoordinator -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM employees | SORT emp_no @@ -85,8 +85,25 @@ emp_no:integer | language_code:integer | language_name:keyword | language_code_x 10003 | 4 | german | 8 ; +sortEvalBeforeLookup +required_capability: join_lookup_v5 + +FROM employees +| SORT emp_no +| EVAL language_code = (emp_no % 10) + 1 +| LOOKUP JOIN languages_lookup ON language_code +| KEEP emp_no, language_code, language_name +| LIMIT 3 +; + +emp_no:integer | language_code:integer | language_name:keyword +10001 | 2 | French +10002 | 3 | Spanish +10003 | 4 | German +; + lookupIPFromRow -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 ROW left = "left", client_ip = "172.21.0.5", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -97,7 +114,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromRowWithShadowing -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right" | LOOKUP JOIN clientips_lookup ON client_ip @@ -108,7 +125,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromRowWithShadowingKeep -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -121,7 +138,7 @@ left | 172.21.0.5 | right | Development ; lookupIPFromRowWithShadowingKeepReordered -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 ROW left = "left", client_ip = "172.21.0.5", env = "env", right = "right" | EVAL client_ip = client_ip::keyword @@ -134,7 +151,7 @@ right | Development | 172.21.0.5 ; lookupIPFromIndex -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -153,7 +170,7 @@ ignoreOrder:true ; lookupIPFromIndexKeep -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -173,7 +190,7 @@ ignoreOrder:true ; lookupIPFromIndexStats -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -189,7 +206,7 @@ count:long | env:keyword ; lookupIPFromIndexStatsKeep -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM sample_data | EVAL client_ip = client_ip::keyword @@ -206,7 +223,7 @@ count:long | env:keyword ; lookupMessageFromRow -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 ROW left = "left", message = "Connected to 10.1.0.1", right = "right" | LOOKUP JOIN message_types_lookup ON message @@ -217,7 +234,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromRowWithShadowing -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 ROW left = "left", message = "Connected to 10.1.0.1", type = "unknown", right = "right" | LOOKUP JOIN message_types_lookup ON message @@ -228,7 +245,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromRowWithShadowingKeep -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 ROW left = "left", message = "Connected to 10.1.0.1", type = "unknown", right = "right" | LOOKUP JOIN message_types_lookup ON message @@ -240,7 +257,7 @@ left | Connected to 10.1.0.1 | right | Success ; lookupMessageFromIndex -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -258,7 +275,7 @@ ignoreOrder:true ; lookupMessageFromIndexKeep -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -277,7 +294,7 @@ ignoreOrder:true ; lookupMessageFromIndexKeepReordered -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -296,7 +313,7 @@ Success | 172.21.2.162 | 3450233 | Connected to 10.1.0.3 ; lookupMessageFromIndexStats -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM sample_data | LOOKUP JOIN message_types_lookup ON message @@ -311,7 +328,7 @@ count:long | type:keyword ; lookupMessageFromIndexStatsKeep -required_capability: join_lookup_v4 +required_capability: join_lookup_v5 FROM sample_data | LOOKUP JOIN message_types_lookup ON message diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 6436e049c7dd8..ddabb3e937dc2 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -539,7 +539,7 @@ public enum Cap { /** * LOOKUP JOIN */ - JOIN_LOOKUP_V4(Build.current().isSnapshot()), + JOIN_LOOKUP_V5(Build.current().isSnapshot()), /** * Fix for https://github.com/elastic/elasticsearch/issues/117054 diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineLimits.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineLimits.java index fb9d3f7e2f91e..1cacebdf27cd2 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineLimits.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineLimits.java @@ -19,7 +19,6 @@ import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan; import org.elasticsearch.xpack.esql.plan.logical.join.Join; import org.elasticsearch.xpack.esql.plan.logical.join.JoinTypes; -import org.elasticsearch.xpack.esql.plan.logical.local.LocalRelation; public final class PushDownAndCombineLimits extends OptimizerRules.OptimizerRule { @@ -63,8 +62,10 @@ public LogicalPlan rule(Limit limit) { } } } else if (limit.child() instanceof Join join) { - if (join.config().type() == JoinTypes.LEFT && join.right() instanceof LocalRelation) { - // This is a hash join from something like a lookup. + if (join.config().type() == JoinTypes.LEFT) { + // NOTE! This is only correct because our LEFT JOINs preserve the number of rows from the left hand side. + // This deviates from SQL semantics. In SQL, multiple matches on the right hand side lead to multiple rows in the output. + // For us, multiple matches on the right hand side are collected into multi-values. return join.replaceChildren(limit.replaceChild(join.left()), join.right()); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java index 2834e5f3f8358..c11ef8615eb72 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java @@ -263,7 +263,7 @@ public final void test() throws Throwable { ); assumeFalse( "lookup join disabled for csv tests", - testCase.requiredCapabilities.contains(EsqlCapabilities.Cap.JOIN_LOOKUP_V4.capabilityName()) + testCase.requiredCapabilities.contains(EsqlCapabilities.Cap.JOIN_LOOKUP_V5.capabilityName()) ); assumeFalse( "can't use TERM function in csv tests", diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java index 30aec707df541..cfff245b19244 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java @@ -2139,7 +2139,7 @@ public void testLookupMatchTypeWrong() { } public void testLookupJoinUnknownIndex() { - assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V4.isEnabled()); + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); String errorMessage = "Unknown index [foobar]"; IndexResolution missingLookupIndex = IndexResolution.invalid(errorMessage); @@ -2168,7 +2168,7 @@ public void testLookupJoinUnknownIndex() { } public void testLookupJoinUnknownField() { - assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V4.isEnabled()); + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); String query = "FROM test | LOOKUP JOIN languages_lookup ON last_name"; String errorMessage = "1:45: Unknown column [last_name] in right side of join"; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java index e20f0d8bbc8ff..4b916106165fb 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java @@ -1964,7 +1964,7 @@ public void testSortByAggregate() { } public void testLookupJoinDataTypeMismatch() { - assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V4.isEnabled()); + assumeTrue("requires LOOKUP JOIN capability", EsqlCapabilities.Cap.JOIN_LOOKUP_V5.isEnabled()); query("FROM test | EVAL language_code = languages | LOOKUP JOIN languages_lookup ON language_code"); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java index 87bc11d8388bc..0cb805b05d845 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java @@ -40,6 +40,7 @@ import org.elasticsearch.xpack.esql.core.expression.predicate.logical.Or; import org.elasticsearch.xpack.esql.core.expression.predicate.nulls.IsNotNull; import org.elasticsearch.xpack.esql.core.expression.predicate.operator.comparison.BinaryComparison; +import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.core.type.EsField; import org.elasticsearch.xpack.esql.core.util.Holder; @@ -112,7 +113,9 @@ import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan; import org.elasticsearch.xpack.esql.plan.logical.join.InlineJoin; import org.elasticsearch.xpack.esql.plan.logical.join.Join; +import org.elasticsearch.xpack.esql.plan.logical.join.JoinConfig; import org.elasticsearch.xpack.esql.plan.logical.join.JoinTypes; +import org.elasticsearch.xpack.esql.plan.logical.join.LookupJoin; import org.elasticsearch.xpack.esql.plan.logical.local.EsqlProject; import org.elasticsearch.xpack.esql.plan.logical.local.LocalRelation; import org.elasticsearch.xpack.esql.plan.logical.local.LocalSupplier; @@ -138,6 +141,7 @@ import static org.elasticsearch.xpack.esql.EsqlTestUtils.TWO; import static org.elasticsearch.xpack.esql.EsqlTestUtils.as; import static org.elasticsearch.xpack.esql.EsqlTestUtils.emptySource; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.fieldAttribute; import static org.elasticsearch.xpack.esql.EsqlTestUtils.getFieldAttribute; import static org.elasticsearch.xpack.esql.EsqlTestUtils.loadMapping; import static org.elasticsearch.xpack.esql.EsqlTestUtils.localSource; @@ -1291,6 +1295,26 @@ public void testCombineLimits() { ); } + public void testPushdownLimitsPastLeftJoin() { + var leftChild = emptySource(); + var rightChild = new LocalRelation(Source.EMPTY, List.of(fieldAttribute()), LocalSupplier.EMPTY); + assertNotEquals(leftChild, rightChild); + + var joinConfig = new JoinConfig(JoinTypes.LEFT, List.of(), List.of(), List.of()); + var join = switch (randomIntBetween(0, 2)) { + case 0 -> new Join(EMPTY, leftChild, rightChild, joinConfig); + case 1 -> new LookupJoin(EMPTY, leftChild, rightChild, joinConfig); + case 2 -> new InlineJoin(EMPTY, leftChild, rightChild, joinConfig); + default -> throw new IllegalArgumentException(); + }; + + var limit = new Limit(EMPTY, L(10), join); + + var optimizedPlan = new PushDownAndCombineLimits().rule(limit); + + assertEquals(join.replaceChildren(limit.replaceChild(join.left()), join.right()), optimizedPlan); + } + public void testMultipleCombineLimits() { var numberOfLimits = randomIntBetween(3, 10); var minimum = randomIntBetween(10, 99); From 67e3302bb404a00d92416bbc35f6166fc362b0e7 Mon Sep 17 00:00:00 2001 From: Jedr Blaszyk Date: Fri, 13 Dec 2024 11:09:58 +0100 Subject: [PATCH 75/77] [Connector APIs] Update yaml rest tests for Connector APIs (#118260) * [Connector API] Update yaml tests * Update tests --------- Co-authored-by: Elastic Machine --- .../entsearch/connector/10_connector_put.yml | 5 +- .../130_connector_update_index_name.yml | 26 +++++++ .../connector/140_connector_update_native.yml | 4 +- .../entsearch/connector/15_connector_post.yml | 5 +- .../entsearch/connector/20_connector_list.yml | 70 +++++++++---------- 5 files changed, 68 insertions(+), 42 deletions(-) diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/10_connector_put.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/10_connector_put.yml index b0f850d09f76d..094d9cbf43089 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/10_connector_put.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/10_connector_put.yml @@ -58,7 +58,7 @@ setup: connector.put: connector_id: test-connector-native body: - index_name: search-test + index_name: content-search-test is_native: true - match: { result: 'created' } @@ -68,7 +68,7 @@ setup: connector_id: test-connector-native - match: { id: test-connector-native } - - match: { index_name: search-test } + - match: { index_name: content-search-test } - match: { is_native: true } - match: { sync_now: false } - match: { status: needs_configuration } @@ -151,6 +151,7 @@ setup: is_native: false service_type: super-connector + --- 'Create Connector - Id returned as part of response': - do: diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/130_connector_update_index_name.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/130_connector_update_index_name.yml index 4ffa5435a3d7b..f804dc02a9e01 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/130_connector_update_index_name.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/130_connector_update_index_name.yml @@ -125,3 +125,29 @@ setup: connector_id: test-connector - match: { index_name: search-1-test } + + +--- +"Update Managed Connector Index Name": + - do: + connector.put: + connector_id: test-connector-1 + body: + is_native: true + service_type: super-connector + + - do: + connector.update_index_name: + connector_id: test-connector-1 + body: + index_name: content-search-2-test + + + - match: { result: updated } + + - do: + connector.get: + connector_id: test-connector-1 + + - match: { index_name: content-search-2-test } + diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/140_connector_update_native.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/140_connector_update_native.yml index 77c57532ad479..f8cd24d175312 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/140_connector_update_native.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/140_connector_update_native.yml @@ -7,7 +7,7 @@ setup: connector.put: connector_id: test-connector body: - index_name: search-1-test + index_name: content-search-1-test name: my-connector language: pl is_native: false @@ -29,7 +29,6 @@ setup: connector_id: test-connector - match: { is_native: true } - - match: { status: configured } - do: connector.update_native: @@ -44,7 +43,6 @@ setup: connector_id: test-connector - match: { is_native: false } - - match: { status: configured } --- "Update Connector Native - 404 when connector doesn't exist": diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/15_connector_post.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/15_connector_post.yml index 1cbff6a35e18b..634f99cd53fde 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/15_connector_post.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/15_connector_post.yml @@ -71,7 +71,7 @@ setup: - do: connector.post: body: - index_name: search-test + index_name: content-search-test is_native: true - set: { id: id } @@ -82,7 +82,7 @@ setup: connector_id: $id - match: { id: $id } - - match: { index_name: search-test } + - match: { index_name: content-search-test } - match: { is_native: true } - match: { sync_now: false } - match: { status: needs_configuration } @@ -102,6 +102,7 @@ setup: is_native: false service_type: super-connector + --- 'Create Connector - Index name used by another connector': - do: diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/20_connector_list.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/20_connector_list.yml index 10e4620ca5603..697b0ee419181 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/20_connector_list.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/connector/20_connector_list.yml @@ -26,7 +26,7 @@ setup: connector.put: connector_id: connector-b body: - index_name: search-2-test + index_name: content-search-2-test name: my-connector-2 language: en is_native: true @@ -40,13 +40,13 @@ setup: - match: { count: 3 } # Alphabetical order by index_name for results - - match: { results.0.id: "connector-a" } - - match: { results.0.index_name: "search-1-test" } - - match: { results.0.language: "pl" } + - match: { results.0.id: "connector-b" } + - match: { results.0.index_name: "content-search-2-test" } + - match: { results.0.language: "en" } - - match: { results.1.id: "connector-b" } - - match: { results.1.index_name: "search-2-test" } - - match: { results.1.language: "en" } + - match: { results.1.id: "connector-a" } + - match: { results.1.index_name: "search-1-test" } + - match: { results.1.language: "pl" } - match: { results.2.id: "connector-c" } - match: { results.2.index_name: "search-3-test" } @@ -62,9 +62,9 @@ setup: - match: { count: 3 } # Alphabetical order by index_name for results - - match: { results.0.id: "connector-b" } - - match: { results.0.index_name: "search-2-test" } - - match: { results.0.language: "en" } + - match: { results.0.id: "connector-a" } + - match: { results.0.index_name: "search-1-test" } + - match: { results.0.language: "pl" } - match: { results.1.id: "connector-c" } - match: { results.1.index_name: "search-3-test" } @@ -79,13 +79,13 @@ setup: - match: { count: 3 } # Alphabetical order by index_name for results - - match: { results.0.id: "connector-a" } - - match: { results.0.index_name: "search-1-test" } - - match: { results.0.language: "pl" } + - match: { results.0.id: "connector-b" } + - match: { results.0.index_name: "content-search-2-test" } + - match: { results.0.language: "en" } - - match: { results.1.id: "connector-b" } - - match: { results.1.index_name: "search-2-test" } - - match: { results.1.language: "en" } + - match: { results.1.id: "connector-a" } + - match: { results.1.index_name: "search-1-test" } + - match: { results.1.language: "pl" } --- "List Connector - empty": @@ -118,11 +118,11 @@ setup: - do: connector.list: - index_name: search-1-test,search-2-test + index_name: search-1-test,content-search-2-test - match: { count: 2 } - - match: { results.0.index_name: "search-1-test" } - - match: { results.1.index_name: "search-2-test" } + - match: { results.0.index_name: "content-search-2-test" } + - match: { results.1.index_name: "search-1-test" } --- @@ -147,8 +147,8 @@ setup: connector_name: my-connector-1,my-connector-2 - match: { count: 2 } - - match: { results.0.name: "my-connector-1" } - - match: { results.1.name: "my-connector-2" } + - match: { results.0.name: "my-connector-2" } + - match: { results.1.name: "my-connector-1" } --- @@ -156,10 +156,10 @@ setup: - do: connector.list: connector_name: my-connector-1,my-connector-2 - index_name: search-2-test + index_name: content-search-2-test - match: { count: 1 } - - match: { results.0.index_name: "search-2-test" } + - match: { results.0.index_name: "content-search-2-test" } - match: { results.0.name: "my-connector-2" } @@ -230,13 +230,13 @@ setup: - match: { count: 3 } # Alphabetical order by index_name for results - - match: { results.0.id: "connector-a" } - - match: { results.0.index_name: "search-1-test" } - - match: { results.0.language: "pl" } + - match: { results.0.id: "connector-b" } + - match: { results.0.index_name: "content-search-2-test" } + - match: { results.0.language: "en" } - - match: { results.1.id: "connector-b" } - - match: { results.1.index_name: "search-2-test" } - - match: { results.1.language: "en" } + - match: { results.1.id: "connector-a" } + - match: { results.1.index_name: "search-1-test" } + - match: { results.1.language: "pl" } - match: { results.2.id: "connector-c" } - match: { results.2.index_name: "search-3-test" } @@ -255,13 +255,13 @@ setup: - match: { count: 3 } # Alphabetical order by index_name for results - - match: { results.0.id: "connector-a" } - - match: { results.0.index_name: "search-1-test" } - - match: { results.0.language: "pl" } + - match: { results.0.id: "connector-b" } + - match: { results.0.index_name: "content-search-2-test" } + - match: { results.0.language: "en" } - - match: { results.1.id: "connector-b" } - - match: { results.1.index_name: "search-2-test" } - - match: { results.1.language: "en" } + - match: { results.1.id: "connector-a" } + - match: { results.1.index_name: "search-1-test" } + - match: { results.1.language: "pl" } - match: { results.2.id: "connector-c" } - match: { results.2.index_name: "search-3-test" } From 140d88c59a10074c5a0993dd66f31578a25f2360 Mon Sep 17 00:00:00 2001 From: Alexander Spies Date: Fri, 13 Dec 2024 11:38:53 +0100 Subject: [PATCH 76/77] ESQL: Dependency check for binary plans (#118326) Make the dependency checker for query plans take into account binary plans and make sure that fields required from the left hand side are actually obtained from there (and analogously for the right). --- .../functions/description/categorize.asciidoc | 2 +- .../functions/kibana/definition/term.json | 2 +- .../esql/functions/kibana/docs/term.md | 8 +-- .../xpack/esql/index/EsIndex.java | 3 + .../xpack/esql/optimizer/LogicalVerifier.java | 3 +- .../esql/optimizer/PhysicalVerifier.java | 9 +-- .../rules/PlanConsistencyChecker.java | 62 +++++++++++++++---- .../xpack/esql/plan/logical/BinaryPlan.java | 5 ++ .../xpack/esql/plan/logical/join/Join.java | 11 ++++ .../esql/plan/physical/AggregateExec.java | 4 +- .../xpack/esql/plan/physical/BinaryExec.java | 5 ++ .../esql/plan/physical/HashJoinExec.java | 10 +++ .../esql/plan/physical/LookupJoinExec.java | 15 +++++ .../esql/analysis/AnalyzerTestUtils.java | 8 ++- .../optimizer/LogicalPlanOptimizerTests.java | 29 ++++++++- .../optimizer/PhysicalPlanOptimizerTests.java | 59 +++++++++++++++++- 16 files changed, 201 insertions(+), 34 deletions(-) diff --git a/docs/reference/esql/functions/description/categorize.asciidoc b/docs/reference/esql/functions/description/categorize.asciidoc index 32af0051e91c8..c956066ad53f3 100644 --- a/docs/reference/esql/functions/description/categorize.asciidoc +++ b/docs/reference/esql/functions/description/categorize.asciidoc @@ -8,4 +8,4 @@ Groups text messages into categories of similarly formatted text values. * can't be used within other expressions * can't be used with multiple groupings -* can't be used or referenced within aggregations +* can't be used or referenced within aggregate functions diff --git a/docs/reference/esql/functions/kibana/definition/term.json b/docs/reference/esql/functions/kibana/definition/term.json index d8bb61fd596a1..b0f129afd239c 100644 --- a/docs/reference/esql/functions/kibana/definition/term.json +++ b/docs/reference/esql/functions/kibana/definition/term.json @@ -78,7 +78,7 @@ } ], "examples" : [ - "from books \n| where term(author, \"gabriel\") \n| keep book_no, title\n| limit 3;" + "FROM books \n| WHERE TERM(author, \"gabriel\") \n| KEEP book_no, title\n| LIMIT 3;" ], "preview" : true, "snapshot_only" : true diff --git a/docs/reference/esql/functions/kibana/docs/term.md b/docs/reference/esql/functions/kibana/docs/term.md index 83e61a949208d..ffecd26d737f7 100644 --- a/docs/reference/esql/functions/kibana/docs/term.md +++ b/docs/reference/esql/functions/kibana/docs/term.md @@ -6,8 +6,8 @@ This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../READ Performs a Term query on the specified field. Returns true if the provided term matches the row. ``` -from books -| where term(author, "gabriel") -| keep book_no, title -| limit 3; +FROM books +| WHERE TERM(author, "gabriel") +| KEEP book_no, title +| LIMIT 3; ``` diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/index/EsIndex.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/index/EsIndex.java index ee51a6f391a65..d3fc9e15e2e04 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/index/EsIndex.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/index/EsIndex.java @@ -25,6 +25,9 @@ public class EsIndex implements Writeable { private final Map mapping; private final Map indexNameWithModes; + /** + * Intended for tests. Returns an index with an empty index mode map. + */ public EsIndex(String name, Map mapping) { this(name, mapping, Map.of()); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalVerifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalVerifier.java index 5e91425296822..dce828dbf192d 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalVerifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalVerifier.java @@ -14,7 +14,6 @@ public final class LogicalVerifier { - private static final PlanConsistencyChecker DEPENDENCY_CHECK = new PlanConsistencyChecker<>(); public static final LogicalVerifier INSTANCE = new LogicalVerifier(); private LogicalVerifier() {} @@ -25,7 +24,7 @@ public Failures verify(LogicalPlan plan) { Failures dependencyFailures = new Failures(); plan.forEachUp(p -> { - DEPENDENCY_CHECK.checkPlan(p, dependencyFailures); + PlanConsistencyChecker.checkPlan(p, dependencyFailures); if (failures.hasFailures() == false) { p.forEachExpression(ex -> { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PhysicalVerifier.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PhysicalVerifier.java index 9132cf87541bb..4ec90fc1ed50a 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PhysicalVerifier.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/PhysicalVerifier.java @@ -13,7 +13,6 @@ import org.elasticsearch.xpack.esql.core.expression.Expressions; import org.elasticsearch.xpack.esql.optimizer.rules.PlanConsistencyChecker; import org.elasticsearch.xpack.esql.plan.logical.Enrich; -import org.elasticsearch.xpack.esql.plan.physical.AggregateExec; import org.elasticsearch.xpack.esql.plan.physical.EnrichExec; import org.elasticsearch.xpack.esql.plan.physical.FieldExtractExec; import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan; @@ -28,7 +27,6 @@ public final class PhysicalVerifier { public static final PhysicalVerifier INSTANCE = new PhysicalVerifier(); - private static final PlanConsistencyChecker DEPENDENCY_CHECK = new PlanConsistencyChecker<>(); private PhysicalVerifier() {} @@ -44,11 +42,6 @@ public Collection verify(PhysicalPlan plan) { } plan.forEachDown(p -> { - if (p instanceof AggregateExec agg) { - var exclude = Expressions.references(agg.ordinalAttributes()); - DEPENDENCY_CHECK.checkPlan(p, exclude, depFailures); - return; - } if (p instanceof FieldExtractExec fieldExtractExec) { Attribute sourceAttribute = fieldExtractExec.sourceAttribute(); if (sourceAttribute == null) { @@ -62,7 +55,7 @@ public Collection verify(PhysicalPlan plan) { ); } } - DEPENDENCY_CHECK.checkPlan(p, depFailures); + PlanConsistencyChecker.checkPlan(p, depFailures); }); if (depFailures.hasFailures()) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/PlanConsistencyChecker.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/PlanConsistencyChecker.java index 5101e3f73bfdf..d5bd110e8df74 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/PlanConsistencyChecker.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/PlanConsistencyChecker.java @@ -12,31 +12,42 @@ import org.elasticsearch.xpack.esql.core.expression.AttributeSet; import org.elasticsearch.xpack.esql.core.expression.NameId; import org.elasticsearch.xpack.esql.plan.QueryPlan; +import org.elasticsearch.xpack.esql.plan.logical.BinaryPlan; +import org.elasticsearch.xpack.esql.plan.physical.BinaryExec; import java.util.HashSet; import java.util.Set; import static org.elasticsearch.xpack.esql.common.Failure.fail; -public class PlanConsistencyChecker

    > { +public class PlanConsistencyChecker { /** * Check whether a single {@link QueryPlan} produces no duplicate attributes and its children provide all of its required * {@link QueryPlan#references() references}. Otherwise, add * {@link org.elasticsearch.xpack.esql.common.Failure Failure}s to the {@link Failures} object. */ - public void checkPlan(P p, Failures failures) { - checkPlan(p, AttributeSet.EMPTY, failures); - } - - public void checkPlan(P p, AttributeSet exclude, Failures failures) { - AttributeSet refs = p.references(); - AttributeSet input = p.inputSet(); - AttributeSet missing = refs.subtract(input).subtract(exclude); - // TODO: for Joins, we should probably check if the required fields from the left child are actually in the left child, not - // just any child (and analogously for the right child). - if (missing.isEmpty() == false) { - failures.add(fail(p, "Plan [{}] optimized incorrectly due to missing references {}", p.nodeString(), missing)); + public static void checkPlan(QueryPlan p, Failures failures) { + if (p instanceof BinaryPlan binaryPlan) { + checkMissingBinary( + p, + binaryPlan.leftReferences(), + binaryPlan.left().outputSet(), + binaryPlan.rightReferences(), + binaryPlan.right().outputSet(), + failures + ); + } else if (p instanceof BinaryExec binaryExec) { + checkMissingBinary( + p, + binaryExec.leftReferences(), + binaryExec.left().outputSet(), + binaryExec.rightReferences(), + binaryExec.right().outputSet(), + failures + ); + } else { + checkMissing(p, p.references(), p.inputSet(), "missing references", failures); } Set outputAttributeNames = new HashSet<>(); @@ -49,4 +60,29 @@ public void checkPlan(P p, AttributeSet exclude, Failures failures) { } } } + + private static void checkMissingBinary( + QueryPlan plan, + AttributeSet leftReferences, + AttributeSet leftInput, + AttributeSet rightReferences, + AttributeSet rightInput, + Failures failures + ) { + checkMissing(plan, leftReferences, leftInput, "missing references from left hand side", failures); + checkMissing(plan, rightReferences, rightInput, "missing references from right hand side", failures); + } + + private static void checkMissing( + QueryPlan plan, + AttributeSet references, + AttributeSet input, + String detailErrorMessage, + Failures failures + ) { + AttributeSet missing = references.subtract(input); + if (missing.isEmpty() == false) { + failures.add(fail(plan, "Plan [{}] optimized incorrectly due to {} {}", plan.nodeString(), detailErrorMessage, missing)); + } + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/BinaryPlan.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/BinaryPlan.java index 91cd7f7a15840..dbd22dd297f88 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/BinaryPlan.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/BinaryPlan.java @@ -6,6 +6,7 @@ */ package org.elasticsearch.xpack.esql.plan.logical; +import org.elasticsearch.xpack.esql.core.expression.AttributeSet; import org.elasticsearch.xpack.esql.core.tree.Source; import java.util.Arrays; @@ -30,6 +31,10 @@ public LogicalPlan right() { return right; } + public abstract AttributeSet leftReferences(); + + public abstract AttributeSet rightReferences(); + @Override public final BinaryPlan replaceChildren(List newChildren) { return replaceChildren(newChildren.get(0), newChildren.get(1)); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/Join.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/Join.java index 6af29fb23b3bb..a2c159e506880 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/Join.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/join/Join.java @@ -12,6 +12,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.xpack.esql.core.expression.Attribute; import org.elasticsearch.xpack.esql.core.expression.AttributeSet; +import org.elasticsearch.xpack.esql.core.expression.Expressions; import org.elasticsearch.xpack.esql.core.expression.ReferenceAttribute; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; @@ -97,6 +98,16 @@ public List output() { return lazyOutput; } + @Override + public AttributeSet leftReferences() { + return Expressions.references(config().leftFields()); + } + + @Override + public AttributeSet rightReferences() { + return Expressions.references(config().rightFields()); + } + public List rightOutputFields() { AttributeSet leftInputs = left().outputSet(); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/AggregateExec.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/AggregateExec.java index 35f45250ed270..3c2d49567813c 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/AggregateExec.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/AggregateExec.java @@ -184,7 +184,9 @@ public List output() { @Override protected AttributeSet computeReferences() { - return mode.isInputPartial() ? new AttributeSet(intermediateAttributes) : Aggregate.computeReferences(aggregates, groupings); + return mode.isInputPartial() + ? new AttributeSet(intermediateAttributes) + : Aggregate.computeReferences(aggregates, groupings).subtract(new AttributeSet(ordinalAttributes())); } /** Returns the attributes that can be loaded from ordinals -- no explicit extraction is needed */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/BinaryExec.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/BinaryExec.java index 6f200bad17a72..9a1b76205b595 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/BinaryExec.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/BinaryExec.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.esql.plan.physical; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xpack.esql.core.expression.AttributeSet; import org.elasticsearch.xpack.esql.core.tree.Source; import java.io.IOException; @@ -40,6 +41,10 @@ public PhysicalPlan right() { return right; } + public abstract AttributeSet leftReferences(); + + public abstract AttributeSet rightReferences(); + @Override public void writeTo(StreamOutput out) throws IOException { Source.EMPTY.writeTo(out); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/HashJoinExec.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/HashJoinExec.java index 5ae3702993fcb..362c83bf76213 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/HashJoinExec.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/HashJoinExec.java @@ -119,6 +119,16 @@ protected AttributeSet computeReferences() { return Expressions.references(leftFields); } + @Override + public AttributeSet leftReferences() { + return Expressions.references(leftFields); + } + + @Override + public AttributeSet rightReferences() { + return Expressions.references(rightFields); + } + @Override public HashJoinExec replaceChildren(PhysicalPlan left, PhysicalPlan right) { return new HashJoinExec(source(), left, right, matchFields, leftFields, rightFields, output); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExec.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExec.java index 26fd12447e664..2aff38993aa98 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExec.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LookupJoinExec.java @@ -119,6 +119,21 @@ protected AttributeSet computeReferences() { return Expressions.references(leftFields); } + @Override + public AttributeSet leftReferences() { + return Expressions.references(leftFields); + } + + @Override + public AttributeSet rightReferences() { + // TODO: currently it's hard coded that we add all fields from the lookup index. But the output we "officially" get from the right + // hand side is inconsistent: + // - After logical optimization, there's a FragmentExec with an EsRelation on the right hand side with all the fields. + // - After local physical optimization, there's just an EsQueryExec here, with no fields other than _doc mentioned and we don't + // insert field extractions in the plan, either. + return AttributeSet.EMPTY; + } + @Override public LookupJoinExec replaceChildren(PhysicalPlan left, PhysicalPlan right) { return new LookupJoinExec(source(), left, right, leftFields, rightFields, addedFields); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTestUtils.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTestUtils.java index 4e89a09db9ed4..5e79e40b7e938 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTestUtils.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTestUtils.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.analysis; +import org.elasticsearch.index.IndexMode; import org.elasticsearch.xpack.core.enrich.EnrichPolicy; import org.elasticsearch.xpack.esql.EsqlTestUtils; import org.elasticsearch.xpack.esql.enrich.ResolvedEnrichPolicy; @@ -104,6 +105,11 @@ public static LogicalPlan analyze(String query, String mapping, QueryParams para return analyzer.analyze(plan); } + public static IndexResolution loadMapping(String resource, String indexName, IndexMode indexMode) { + EsIndex test = new EsIndex(indexName, EsqlTestUtils.loadMapping(resource), Map.of(indexName, indexMode)); + return IndexResolution.valid(test); + } + public static IndexResolution loadMapping(String resource, String indexName) { EsIndex test = new EsIndex(indexName, EsqlTestUtils.loadMapping(resource)); return IndexResolution.valid(test); @@ -118,7 +124,7 @@ public static IndexResolution expandedDefaultIndexResolution() { } public static IndexResolution defaultLookupResolution() { - return loadMapping("mapping-languages.json", "languages_lookup"); + return loadMapping("mapping-languages.json", "languages_lookup", IndexMode.LOOKUP); } public static EnrichResolution defaultEnrichResolution() { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java index 0cb805b05d845..7e498eb6654b9 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerTests.java @@ -149,6 +149,7 @@ import static org.elasticsearch.xpack.esql.EsqlTestUtils.withDefaultLimitWarning; import static org.elasticsearch.xpack.esql.analysis.Analyzer.NO_FIELDS; import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.analyze; +import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.defaultLookupResolution; import static org.elasticsearch.xpack.esql.core.expression.Literal.NULL; import static org.elasticsearch.xpack.esql.core.tree.Source.EMPTY; import static org.elasticsearch.xpack.esql.core.type.DataType.DOUBLE; @@ -221,7 +222,13 @@ public static void init() { EsIndex test = new EsIndex("test", mapping, Map.of("test", IndexMode.STANDARD)); IndexResolution getIndexResult = IndexResolution.valid(test); analyzer = new Analyzer( - new AnalyzerContext(EsqlTestUtils.TEST_CFG, new EsqlFunctionRegistry(), getIndexResult, enrichResolution), + new AnalyzerContext( + EsqlTestUtils.TEST_CFG, + new EsqlFunctionRegistry(), + getIndexResult, + defaultLookupResolution(), + enrichResolution + ), TEST_VERIFIER ); @@ -4896,6 +4903,26 @@ public void testPlanSanityCheck() throws Exception { assertThat(e.getMessage(), containsString(" optimized incorrectly due to missing references [salary")); } + public void testPlanSanityCheckWithBinaryPlans() throws Exception { + var plan = optimizedPlan(""" + FROM test + | RENAME languages AS language_code + | LOOKUP JOIN languages_lookup ON language_code + """); + + var project = as(plan, Project.class); + var limit = as(project.child(), Limit.class); + var join = as(limit.child(), Join.class); + + var joinWithInvalidLeftPlan = join.replaceChildren(join.right(), join.right()); + IllegalStateException e = expectThrows(IllegalStateException.class, () -> logicalOptimizer.optimize(joinWithInvalidLeftPlan)); + assertThat(e.getMessage(), containsString(" optimized incorrectly due to missing references from left hand side [language_code")); + + var joinWithInvalidRightPlan = join.replaceChildren(join.left(), join.left()); + e = expectThrows(IllegalStateException.class, () -> logicalOptimizer.optimize(joinWithInvalidRightPlan)); + assertThat(e.getMessage(), containsString(" optimized incorrectly due to missing references from right hand side [language_code")); + } + // https://github.com/elastic/elasticsearch/issues/104995 public void testNoWrongIsNotNullPruning() { var plan = optimizedPlan(""" diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java index 9682bb1c8b076..ec1d55a0fc58f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java @@ -115,6 +115,7 @@ import org.elasticsearch.xpack.esql.plan.physical.HashJoinExec; import org.elasticsearch.xpack.esql.plan.physical.LimitExec; import org.elasticsearch.xpack.esql.plan.physical.LocalSourceExec; +import org.elasticsearch.xpack.esql.plan.physical.LookupJoinExec; import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan; import org.elasticsearch.xpack.esql.plan.physical.ProjectExec; import org.elasticsearch.xpack.esql.plan.physical.TopNExec; @@ -155,6 +156,7 @@ import static org.elasticsearch.xpack.esql.EsqlTestUtils.withDefaultLimitWarning; import static org.elasticsearch.xpack.esql.SerializationTestUtils.assertSerialization; import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.analyze; +import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.defaultLookupResolution; import static org.elasticsearch.xpack.esql.core.expression.Expressions.name; import static org.elasticsearch.xpack.esql.core.expression.Expressions.names; import static org.elasticsearch.xpack.esql.core.expression.function.scalar.FunctionTestUtils.l; @@ -281,16 +283,30 @@ TestDataSource makeTestDataSource( String indexName, String mappingFileName, EsqlFunctionRegistry functionRegistry, + IndexResolution lookupResolution, EnrichResolution enrichResolution, SearchStats stats ) { Map mapping = loadMapping(mappingFileName); EsIndex index = new EsIndex(indexName, mapping, Map.of("test", IndexMode.STANDARD)); IndexResolution getIndexResult = IndexResolution.valid(index); - Analyzer analyzer = new Analyzer(new AnalyzerContext(config, functionRegistry, getIndexResult, enrichResolution), TEST_VERIFIER); + Analyzer analyzer = new Analyzer( + new AnalyzerContext(config, functionRegistry, getIndexResult, lookupResolution, enrichResolution), + TEST_VERIFIER + ); return new TestDataSource(mapping, index, analyzer, stats); } + TestDataSource makeTestDataSource( + String indexName, + String mappingFileName, + EsqlFunctionRegistry functionRegistry, + EnrichResolution enrichResolution, + SearchStats stats + ) { + return makeTestDataSource(indexName, mappingFileName, functionRegistry, defaultLookupResolution(), enrichResolution, stats); + } + TestDataSource makeTestDataSource( String indexName, String mappingFileName, @@ -2312,6 +2328,39 @@ public void testVerifierOnMissingReferences() { assertThat(e.getMessage(), containsString(" > 10[INTEGER]]] optimized incorrectly due to missing references [emp_no{f}#")); } + public void testVerifierOnMissingReferencesWithBinaryPlans() throws Exception { + // Do not assert serialization: + // This will have a LookupJoinExec, which is not serializable because it doesn't leave the coordinator. + var plan = physicalPlan(""" + FROM test + | RENAME languages AS language_code + | SORT language_code + | LOOKUP JOIN languages_lookup ON language_code + """, testData, false); + + var planWithInvalidJoinLeftSide = plan.transformUp(LookupJoinExec.class, join -> join.replaceChildren(join.right(), join.right())); + + var e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(planWithInvalidJoinLeftSide)); + assertThat(e.getMessage(), containsString(" optimized incorrectly due to missing references from left hand side [language_code")); + + var planWithInvalidJoinRightSide = plan.transformUp( + LookupJoinExec.class, + // LookupJoinExec.rightReferences() is currently EMPTY (hack); use a HashJoinExec instead. + join -> new HashJoinExec( + join.source(), + join.left(), + join.left(), + join.leftFields(), + join.leftFields(), + join.rightFields(), + join.output() + ) + ); + + e = expectThrows(IllegalStateException.class, () -> physicalPlanOptimizer.verify(planWithInvalidJoinRightSide)); + assertThat(e.getMessage(), containsString(" optimized incorrectly due to missing references from right hand side [language_code")); + } + public void testVerifierOnDuplicateOutputAttributes() { var plan = physicalPlan(""" from test @@ -6863,11 +6912,17 @@ private PhysicalPlan physicalPlan(String query) { } private PhysicalPlan physicalPlan(String query, TestDataSource dataSource) { + return physicalPlan(query, dataSource, true); + } + + private PhysicalPlan physicalPlan(String query, TestDataSource dataSource, boolean assertSerialization) { var logical = logicalOptimizer.optimize(dataSource.analyzer.analyze(parser.createStatement(query))); // System.out.println("Logical\n" + logical); var physical = mapper.map(logical); // System.out.println(physical); - assertSerialization(physical); + if (assertSerialization) { + assertSerialization(physical); + } return physical; } From 2be4cd983fb13a2903ad61ea2c6212aa31e39364 Mon Sep 17 00:00:00 2001 From: Gal Lalouche Date: Fri, 13 Dec 2024 12:41:24 +0200 Subject: [PATCH 77/77] ESQL: Support ST_EXTENT_AGG (#117451) This PR adds support for ST_EXTENT_AGG aggregation, i.e., computing a bounding box over a set of points/shapes (Cartesian or geo). Note the difference between this aggregation and the already implemented scalar function ST_EXTENT. This isn't a very efficient implementation, and future PRs will attempt to read these extents directly from the doc values. We currently always use longitude wrapping, i.e., we may wrap around the dateline for a smaller bounding box. Future PRs will let the user control this behavior. Fixes #104659. --- docs/changelog/117451.yaml | 6 + .../functions/aggregation-functions.asciidoc | 2 + .../description/st_extent_agg.asciidoc | 5 + .../functions/examples/st_extent_agg.asciidoc | 13 + .../kibana/definition/st_extent_agg.json | 61 +++++ .../functions/kibana/docs/st_extent_agg.md | 12 + .../functions/layout/st_extent_agg.asciidoc | 15 ++ .../parameters/st_extent_agg.asciidoc | 6 + .../functions/signature/st_extent_agg.svg | 1 + .../functions/types/st_extent_agg.asciidoc | 12 + .../utils/SpatialEnvelopeVisitor.java | 72 ++--- .../utils/SpatialEnvelopeVisitorTests.java | 12 +- ...esianPointDocValuesAggregatorFunction.java | 187 +++++++++++++ ...ntDocValuesAggregatorFunctionSupplier.java | 41 +++ ...ntDocValuesGroupingAggregatorFunction.java | 230 ++++++++++++++++ ...anPointSourceValuesAggregatorFunction.java | 192 ++++++++++++++ ...ourceValuesAggregatorFunctionSupplier.java | 41 +++ ...ourceValuesGroupingAggregatorFunction.java | 235 +++++++++++++++++ ...xtentCartesianShapeAggregatorFunction.java | 192 ++++++++++++++ ...tesianShapeAggregatorFunctionSupplier.java | 40 +++ ...tesianShapeGroupingAggregatorFunction.java | 235 +++++++++++++++++ ...ntGeoPointDocValuesAggregatorFunction.java | 201 ++++++++++++++ ...ntDocValuesAggregatorFunctionSupplier.java | 40 +++ ...ntDocValuesGroupingAggregatorFunction.java | 242 +++++++++++++++++ ...eoPointSourceValuesAggregatorFunction.java | 206 +++++++++++++++ ...ourceValuesAggregatorFunctionSupplier.java | 41 +++ ...ourceValuesGroupingAggregatorFunction.java | 247 ++++++++++++++++++ ...atialExtentGeoShapeAggregatorFunction.java | 206 +++++++++++++++ ...entGeoShapeAggregatorFunctionSupplier.java | 40 +++ ...entGeoShapeGroupingAggregatorFunction.java | 247 ++++++++++++++++++ .../aggregation/AbstractArrayState.java | 4 +- .../spatial/CentroidPointAggregator.java | 7 + .../spatial/GeoPointEnvelopeVisitor.java | 63 +++++ .../aggregation/spatial/PointType.java | 107 ++++++++ .../spatial/SpatialAggregationUtils.java | 88 +++++++ ...roidCartesianPointDocValuesAggregator.java | 22 +- ...dCartesianPointSourceValuesAggregator.java | 20 +- ...alCentroidGeoPointDocValuesAggregator.java | 26 +- ...entroidGeoPointSourceValuesAggregator.java | 20 +- .../spatial/SpatialExtentAggregator.java | 36 +++ ...tentCartesianPointDocValuesAggregator.java | 42 +++ ...tCartesianPointSourceValuesAggregator.java | 45 ++++ ...SpatialExtentCartesianShapeAggregator.java | 43 +++ ...tialExtentGeoPointDocValuesAggregator.java | 45 ++++ ...lExtentGeoPointSourceValuesAggregator.java | 48 ++++ .../SpatialExtentGeoShapeAggregator.java | 46 ++++ .../spatial/SpatialExtentGroupingState.java | 154 +++++++++++ ...entGroupingStateWrappedLongitudeState.java | 182 +++++++++++++ ...tialExtentLongitudeWrappingAggregator.java | 62 +++++ .../spatial/SpatialExtentState.java | 82 ++++++ ...atialExtentStateWrappedLongitudeState.java | 91 +++++++ .../mapping-airports_no_doc_values.json | 4 +- .../src/main/resources/spatial.csv-spec | 99 +++++++ .../xpack/esql/action/EsqlCapabilities.java | 3 + .../function/EsqlFunctionRegistry.java | 2 + .../aggregate/AggregateWritables.java | 1 + .../aggregate/SpatialAggregateFunction.java | 31 ++- .../function/aggregate/SpatialCentroid.java | 43 ++- .../function/aggregate/SpatialExtent.java | 119 +++++++++ .../function/scalar/spatial/StEnvelope.java | 3 +- .../function/scalar/spatial/StXMax.java | 3 +- .../function/scalar/spatial/StXMin.java | 3 +- .../function/scalar/spatial/StYMax.java | 3 +- .../function/scalar/spatial/StYMin.java | 3 +- .../xpack/esql/planner/AggregateMapper.java | 52 +++- .../esql/expression/RectangleMatcher.java | 61 +++++ .../WellKnownBinaryBytesRefMatcher.java | 45 ++++ .../function/AbstractAggregationTestCase.java | 8 +- .../function/MultiRowTestCaseSupplier.java | 85 +++--- .../function/aggregate/CountTests.java | 5 +- .../aggregate/SpatialCentroidTests.java | 5 +- .../aggregate/SpatialExtentTests.java | 102 ++++++++ .../scalar/spatial/StEnvelopeTests.java | 5 +- .../function/scalar/spatial/StXMaxTests.java | 5 +- .../function/scalar/spatial/StXMinTests.java | 5 +- .../function/scalar/spatial/StYMaxTests.java | 5 +- .../function/scalar/spatial/StYMinTests.java | 5 +- .../optimizer/PhysicalPlanOptimizerTests.java | 135 +++++++++- .../rest-api-spec/test/esql/60_usage.yml | 4 +- 79 files changed, 4923 insertions(+), 234 deletions(-) create mode 100644 docs/changelog/117451.yaml create mode 100644 docs/reference/esql/functions/description/st_extent_agg.asciidoc create mode 100644 docs/reference/esql/functions/examples/st_extent_agg.asciidoc create mode 100644 docs/reference/esql/functions/kibana/definition/st_extent_agg.json create mode 100644 docs/reference/esql/functions/kibana/docs/st_extent_agg.md create mode 100644 docs/reference/esql/functions/layout/st_extent_agg.asciidoc create mode 100644 docs/reference/esql/functions/parameters/st_extent_agg.asciidoc create mode 100644 docs/reference/esql/functions/signature/st_extent_agg.svg create mode 100644 docs/reference/esql/functions/types/st_extent_agg.asciidoc create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesAggregatorFunction.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesAggregatorFunctionSupplier.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesGroupingAggregatorFunction.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesAggregatorFunction.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesAggregatorFunctionSupplier.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesGroupingAggregatorFunction.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeAggregatorFunction.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeAggregatorFunctionSupplier.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeGroupingAggregatorFunction.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesAggregatorFunction.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesAggregatorFunctionSupplier.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesGroupingAggregatorFunction.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesAggregatorFunction.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesAggregatorFunctionSupplier.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesGroupingAggregatorFunction.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeAggregatorFunction.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeAggregatorFunctionSupplier.java create mode 100644 x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeGroupingAggregatorFunction.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/GeoPointEnvelopeVisitor.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/PointType.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialAggregationUtils.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentAggregator.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesAggregator.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesAggregator.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeAggregator.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesAggregator.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesAggregator.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeAggregator.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGroupingState.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGroupingStateWrappedLongitudeState.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentLongitudeWrappingAggregator.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentState.java create mode 100644 x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentStateWrappedLongitudeState.java create mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialExtent.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/RectangleMatcher.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/WellKnownBinaryBytesRefMatcher.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialExtentTests.java diff --git a/docs/changelog/117451.yaml b/docs/changelog/117451.yaml new file mode 100644 index 0000000000000..bda0ca59e0953 --- /dev/null +++ b/docs/changelog/117451.yaml @@ -0,0 +1,6 @@ +pr: 117451 +summary: ST_EXTENT aggregation +area: ES|QL +type: feature +issues: + - 104659 diff --git a/docs/reference/esql/functions/aggregation-functions.asciidoc b/docs/reference/esql/functions/aggregation-functions.asciidoc index c2c2508ad5de2..24b42a6efd831 100644 --- a/docs/reference/esql/functions/aggregation-functions.asciidoc +++ b/docs/reference/esql/functions/aggregation-functions.asciidoc @@ -17,6 +17,7 @@ The <> command supports these aggregate functions: * <> * <> * experimental:[] <> +* experimental:[] <> * <> * <> * <> @@ -33,6 +34,7 @@ include::layout/median_absolute_deviation.asciidoc[] include::layout/min.asciidoc[] include::layout/percentile.asciidoc[] include::layout/st_centroid_agg.asciidoc[] +include::layout/st_extent_agg.asciidoc[] include::layout/std_dev.asciidoc[] include::layout/sum.asciidoc[] include::layout/top.asciidoc[] diff --git a/docs/reference/esql/functions/description/st_extent_agg.asciidoc b/docs/reference/esql/functions/description/st_extent_agg.asciidoc new file mode 100644 index 0000000000000..a9e1acfb0e6fb --- /dev/null +++ b/docs/reference/esql/functions/description/st_extent_agg.asciidoc @@ -0,0 +1,5 @@ +// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +*Description* + +Calculate the spatial extent over a field with geometry type. Returns a bounding box for all values of the field. diff --git a/docs/reference/esql/functions/examples/st_extent_agg.asciidoc b/docs/reference/esql/functions/examples/st_extent_agg.asciidoc new file mode 100644 index 0000000000000..179be82103641 --- /dev/null +++ b/docs/reference/esql/functions/examples/st_extent_agg.asciidoc @@ -0,0 +1,13 @@ +// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +*Example* + +[source.merge.styled,esql] +---- +include::{esql-specs}/spatial.csv-spec[tag=st_extent_agg-airports] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/spatial.csv-spec[tag=st_extent_agg-airports-result] +|=== + diff --git a/docs/reference/esql/functions/kibana/definition/st_extent_agg.json b/docs/reference/esql/functions/kibana/definition/st_extent_agg.json new file mode 100644 index 0000000000000..19afcc59e38a4 --- /dev/null +++ b/docs/reference/esql/functions/kibana/definition/st_extent_agg.json @@ -0,0 +1,61 @@ +{ + "comment" : "This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it.", + "type" : "agg", + "name" : "st_extent_agg", + "description" : "Calculate the spatial extent over a field with geometry type. Returns a bounding box for all values of the field.", + "signatures" : [ + { + "params" : [ + { + "name" : "field", + "type" : "cartesian_point", + "optional" : false, + "description" : "" + } + ], + "variadic" : false, + "returnType" : "cartesian_shape" + }, + { + "params" : [ + { + "name" : "field", + "type" : "cartesian_shape", + "optional" : false, + "description" : "" + } + ], + "variadic" : false, + "returnType" : "cartesian_shape" + }, + { + "params" : [ + { + "name" : "field", + "type" : "geo_point", + "optional" : false, + "description" : "" + } + ], + "variadic" : false, + "returnType" : "geo_shape" + }, + { + "params" : [ + { + "name" : "field", + "type" : "geo_shape", + "optional" : false, + "description" : "" + } + ], + "variadic" : false, + "returnType" : "geo_shape" + } + ], + "examples" : [ + "FROM airports\n| WHERE country == \"India\"\n| STATS extent = ST_EXTENT_AGG(location)" + ], + "preview" : false, + "snapshot_only" : false +} diff --git a/docs/reference/esql/functions/kibana/docs/st_extent_agg.md b/docs/reference/esql/functions/kibana/docs/st_extent_agg.md new file mode 100644 index 0000000000000..a2e307c5b2c55 --- /dev/null +++ b/docs/reference/esql/functions/kibana/docs/st_extent_agg.md @@ -0,0 +1,12 @@ + + +### ST_EXTENT_AGG +Calculate the spatial extent over a field with geometry type. Returns a bounding box for all values of the field. + +``` +FROM airports +| WHERE country == "India" +| STATS extent = ST_EXTENT_AGG(location) +``` diff --git a/docs/reference/esql/functions/layout/st_extent_agg.asciidoc b/docs/reference/esql/functions/layout/st_extent_agg.asciidoc new file mode 100644 index 0000000000000..946bef661e70c --- /dev/null +++ b/docs/reference/esql/functions/layout/st_extent_agg.asciidoc @@ -0,0 +1,15 @@ +// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +[discrete] +[[esql-st_extent_agg]] +=== `ST_EXTENT_AGG` + +*Syntax* + +[.text-center] +image::esql/functions/signature/st_extent_agg.svg[Embedded,opts=inline] + +include::../parameters/st_extent_agg.asciidoc[] +include::../description/st_extent_agg.asciidoc[] +include::../types/st_extent_agg.asciidoc[] +include::../examples/st_extent_agg.asciidoc[] diff --git a/docs/reference/esql/functions/parameters/st_extent_agg.asciidoc b/docs/reference/esql/functions/parameters/st_extent_agg.asciidoc new file mode 100644 index 0000000000000..8903aa1a472a3 --- /dev/null +++ b/docs/reference/esql/functions/parameters/st_extent_agg.asciidoc @@ -0,0 +1,6 @@ +// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +*Parameters* + +`field`:: + diff --git a/docs/reference/esql/functions/signature/st_extent_agg.svg b/docs/reference/esql/functions/signature/st_extent_agg.svg new file mode 100644 index 0000000000000..bb19b68bfb08b --- /dev/null +++ b/docs/reference/esql/functions/signature/st_extent_agg.svg @@ -0,0 +1 @@ +ST_EXTENT_AGG(field) \ No newline at end of file diff --git a/docs/reference/esql/functions/types/st_extent_agg.asciidoc b/docs/reference/esql/functions/types/st_extent_agg.asciidoc new file mode 100644 index 0000000000000..c836aa1896f07 --- /dev/null +++ b/docs/reference/esql/functions/types/st_extent_agg.asciidoc @@ -0,0 +1,12 @@ +// This is generated by ESQL's AbstractFunctionTestCase. Do no edit it. See ../README.md for how to regenerate it. + +*Supported types* + +[%header.monospaced.styled,format=dsv,separator=|] +|=== +field | result +cartesian_point | cartesian_shape +cartesian_shape | cartesian_shape +geo_point | geo_shape +geo_shape | geo_shape +|=== diff --git a/libs/geo/src/main/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitor.java b/libs/geo/src/main/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitor.java index eee4a62c7d588..696be2808ed1f 100644 --- a/libs/geo/src/main/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitor.java +++ b/libs/geo/src/main/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitor.java @@ -83,10 +83,15 @@ public static Optional visitCartesian(Geometry geometry) { return Optional.empty(); } + public enum WrapLongitude { + NO_WRAP, + WRAP + } + /** * Determine the BBOX assuming the CRS is geographic (eg WGS84) and optionally wrapping the longitude around the dateline. */ - public static Optional visitGeo(Geometry geometry, boolean wrapLongitude) { + public static Optional visitGeo(Geometry geometry, WrapLongitude wrapLongitude) { var visitor = new SpatialEnvelopeVisitor(new GeoPointVisitor(wrapLongitude)); if (geometry.visit(visitor)) { return Optional.of(visitor.getResult()); @@ -181,40 +186,16 @@ public Rectangle getResult() { * */ public static class GeoPointVisitor implements PointVisitor { - private double minY = Double.POSITIVE_INFINITY; - private double maxY = Double.NEGATIVE_INFINITY; - private double minNegX = Double.POSITIVE_INFINITY; - private double maxNegX = Double.NEGATIVE_INFINITY; - private double minPosX = Double.POSITIVE_INFINITY; - private double maxPosX = Double.NEGATIVE_INFINITY; - - public double getMinY() { - return minY; - } - - public double getMaxY() { - return maxY; - } - - public double getMinNegX() { - return minNegX; - } + protected double minY = Double.POSITIVE_INFINITY; + protected double maxY = Double.NEGATIVE_INFINITY; + protected double minNegX = Double.POSITIVE_INFINITY; + protected double maxNegX = Double.NEGATIVE_INFINITY; + protected double minPosX = Double.POSITIVE_INFINITY; + protected double maxPosX = Double.NEGATIVE_INFINITY; - public double getMaxNegX() { - return maxNegX; - } - - public double getMinPosX() { - return minPosX; - } + private final WrapLongitude wrapLongitude; - public double getMaxPosX() { - return maxPosX; - } - - private final boolean wrapLongitude; - - public GeoPointVisitor(boolean wrapLongitude) { + public GeoPointVisitor(WrapLongitude wrapLongitude) { this.wrapLongitude = wrapLongitude; } @@ -253,32 +234,35 @@ public Rectangle getResult() { return getResult(minNegX, minPosX, maxNegX, maxPosX, maxY, minY, wrapLongitude); } - private static Rectangle getResult( + protected static Rectangle getResult( double minNegX, double minPosX, double maxNegX, double maxPosX, double maxY, double minY, - boolean wrapLongitude + WrapLongitude wrapLongitude ) { assert Double.isFinite(maxY); if (Double.isInfinite(minPosX)) { return new Rectangle(minNegX, maxNegX, maxY, minY); } else if (Double.isInfinite(minNegX)) { return new Rectangle(minPosX, maxPosX, maxY, minY); - } else if (wrapLongitude) { - double unwrappedWidth = maxPosX - minNegX; - double wrappedWidth = (180 - minPosX) - (-180 - maxNegX); - if (unwrappedWidth <= wrappedWidth) { - return new Rectangle(minNegX, maxPosX, maxY, minY); - } else { - return new Rectangle(minPosX, maxNegX, maxY, minY); - } } else { - return new Rectangle(minNegX, maxPosX, maxY, minY); + return switch (wrapLongitude) { + case NO_WRAP -> new Rectangle(minNegX, maxPosX, maxY, minY); + case WRAP -> maybeWrap(minNegX, minPosX, maxNegX, maxPosX, maxY, minY); + }; } } + + private static Rectangle maybeWrap(double minNegX, double minPosX, double maxNegX, double maxPosX, double maxY, double minY) { + double unwrappedWidth = maxPosX - minNegX; + double wrappedWidth = 360 + maxNegX - minPosX; + return unwrappedWidth <= wrappedWidth + ? new Rectangle(minNegX, maxPosX, maxY, minY) + : new Rectangle(minPosX, maxNegX, maxY, minY); + } } private boolean isValid() { diff --git a/libs/geo/src/test/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitorTests.java b/libs/geo/src/test/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitorTests.java index fc35df295e566..893a1700616a6 100644 --- a/libs/geo/src/test/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitorTests.java +++ b/libs/geo/src/test/java/org/elasticsearch/geometry/utils/SpatialEnvelopeVisitorTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.geo.ShapeTestUtils; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; import org.elasticsearch.test.ESTestCase; import static org.hamcrest.Matchers.equalTo; @@ -36,7 +37,7 @@ public void testVisitCartesianShape() { public void testVisitGeoShapeNoWrap() { for (int i = 0; i < 1000; i++) { var geometry = GeometryTestUtils.randomGeometryWithoutCircle(0, false); - var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, false); + var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.NO_WRAP); assertNotNull(bbox); assertTrue(i + ": " + geometry, bbox.isPresent()); var result = bbox.get(); @@ -48,7 +49,8 @@ public void testVisitGeoShapeNoWrap() { public void testVisitGeoShapeWrap() { for (int i = 0; i < 1000; i++) { var geometry = GeometryTestUtils.randomGeometryWithoutCircle(0, true); - var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, false); + // TODO this should be WRAP instead + var bbox = SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.NO_WRAP); assertNotNull(bbox); assertTrue(i + ": " + geometry, bbox.isPresent()); var result = bbox.get(); @@ -81,7 +83,7 @@ public void testVisitCartesianPoints() { } public void testVisitGeoPointsNoWrapping() { - var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(false)); + var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(WrapLongitude.NO_WRAP)); double minY = Double.MAX_VALUE; double maxY = -Double.MAX_VALUE; double minX = Double.MAX_VALUE; @@ -103,7 +105,7 @@ public void testVisitGeoPointsNoWrapping() { } public void testVisitGeoPointsWrapping() { - var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(true)); + var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(WrapLongitude.WRAP)); double minY = Double.POSITIVE_INFINITY; double maxY = Double.NEGATIVE_INFINITY; double minNegX = Double.POSITIVE_INFINITY; @@ -145,7 +147,7 @@ public void testVisitGeoPointsWrapping() { } public void testWillCrossDateline() { - var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(true)); + var visitor = new SpatialEnvelopeVisitor(new SpatialEnvelopeVisitor.GeoPointVisitor(WrapLongitude.WRAP)); visitor.visit(new Point(-90.0, 0.0)); visitor.visit(new Point(90.0, 0.0)); assertCrossesDateline(visitor, false); diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesAggregatorFunction.java new file mode 100644 index 0000000000000..21306036fbf50 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesAggregatorFunction.java @@ -0,0 +1,187 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import org.elasticsearch.compute.aggregation.AggregatorFunction; +import org.elasticsearch.compute.aggregation.IntermediateStateDesc; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BooleanVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.LongBlock; +import org.elasticsearch.compute.data.LongVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link AggregatorFunction} implementation for {@link SpatialExtentCartesianPointDocValuesAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentCartesianPointDocValuesAggregatorFunction implements AggregatorFunction { + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("minX", ElementType.INT), + new IntermediateStateDesc("maxX", ElementType.INT), + new IntermediateStateDesc("maxY", ElementType.INT), + new IntermediateStateDesc("minY", ElementType.INT) ); + + private final DriverContext driverContext; + + private final SpatialExtentState state; + + private final List channels; + + public SpatialExtentCartesianPointDocValuesAggregatorFunction(DriverContext driverContext, + List channels, SpatialExtentState state) { + this.driverContext = driverContext; + this.channels = channels; + this.state = state; + } + + public static SpatialExtentCartesianPointDocValuesAggregatorFunction create( + DriverContext driverContext, List channels) { + return new SpatialExtentCartesianPointDocValuesAggregatorFunction(driverContext, channels, SpatialExtentCartesianPointDocValuesAggregator.initSingle()); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + @Override + public int intermediateBlockCount() { + return INTERMEDIATE_STATE_DESC.size(); + } + + @Override + public void addRawInput(Page page, BooleanVector mask) { + if (mask.allFalse()) { + // Entire page masked away + return; + } + if (mask.allTrue()) { + // No masking + LongBlock block = page.getBlock(channels.get(0)); + LongVector vector = block.asVector(); + if (vector != null) { + addRawVector(vector); + } else { + addRawBlock(block); + } + return; + } + // Some positions masked away, others kept + LongBlock block = page.getBlock(channels.get(0)); + LongVector vector = block.asVector(); + if (vector != null) { + addRawVector(vector, mask); + } else { + addRawBlock(block, mask); + } + } + + private void addRawVector(LongVector vector) { + for (int i = 0; i < vector.getPositionCount(); i++) { + SpatialExtentCartesianPointDocValuesAggregator.combine(state, vector.getLong(i)); + } + } + + private void addRawVector(LongVector vector, BooleanVector mask) { + for (int i = 0; i < vector.getPositionCount(); i++) { + if (mask.getBoolean(i) == false) { + continue; + } + SpatialExtentCartesianPointDocValuesAggregator.combine(state, vector.getLong(i)); + } + } + + private void addRawBlock(LongBlock block) { + for (int p = 0; p < block.getPositionCount(); p++) { + if (block.isNull(p)) { + continue; + } + int start = block.getFirstValueIndex(p); + int end = start + block.getValueCount(p); + for (int i = start; i < end; i++) { + SpatialExtentCartesianPointDocValuesAggregator.combine(state, block.getLong(i)); + } + } + } + + private void addRawBlock(LongBlock block, BooleanVector mask) { + for (int p = 0; p < block.getPositionCount(); p++) { + if (mask.getBoolean(p) == false) { + continue; + } + if (block.isNull(p)) { + continue; + } + int start = block.getFirstValueIndex(p); + int end = start + block.getValueCount(p); + for (int i = start; i < end; i++) { + SpatialExtentCartesianPointDocValuesAggregator.combine(state, block.getLong(i)); + } + } + } + + @Override + public void addIntermediateInput(Page page) { + assert channels.size() == intermediateBlockCount(); + assert page.getBlockCount() >= channels.get(0) + intermediateStateDesc().size(); + Block minXUncast = page.getBlock(channels.get(0)); + if (minXUncast.areAllValuesNull()) { + return; + } + IntVector minX = ((IntBlock) minXUncast).asVector(); + assert minX.getPositionCount() == 1; + Block maxXUncast = page.getBlock(channels.get(1)); + if (maxXUncast.areAllValuesNull()) { + return; + } + IntVector maxX = ((IntBlock) maxXUncast).asVector(); + assert maxX.getPositionCount() == 1; + Block maxYUncast = page.getBlock(channels.get(2)); + if (maxYUncast.areAllValuesNull()) { + return; + } + IntVector maxY = ((IntBlock) maxYUncast).asVector(); + assert maxY.getPositionCount() == 1; + Block minYUncast = page.getBlock(channels.get(3)); + if (minYUncast.areAllValuesNull()) { + return; + } + IntVector minY = ((IntBlock) minYUncast).asVector(); + assert minY.getPositionCount() == 1; + SpatialExtentCartesianPointDocValuesAggregator.combineIntermediate(state, minX.getInt(0), maxX.getInt(0), maxY.getInt(0), minY.getInt(0)); + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, DriverContext driverContext) { + state.toIntermediate(blocks, offset, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, DriverContext driverContext) { + blocks[offset] = SpatialExtentCartesianPointDocValuesAggregator.evaluateFinal(state, driverContext); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesAggregatorFunctionSupplier.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesAggregatorFunctionSupplier.java new file mode 100644 index 0000000000000..751ea3b4c4a9d --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesAggregatorFunctionSupplier.java @@ -0,0 +1,41 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.util.List; +import org.elasticsearch.compute.aggregation.AggregatorFunctionSupplier; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link AggregatorFunctionSupplier} implementation for {@link SpatialExtentCartesianPointDocValuesAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentCartesianPointDocValuesAggregatorFunctionSupplier implements AggregatorFunctionSupplier { + private final List channels; + + public SpatialExtentCartesianPointDocValuesAggregatorFunctionSupplier(List channels) { + this.channels = channels; + } + + @Override + public SpatialExtentCartesianPointDocValuesAggregatorFunction aggregator( + DriverContext driverContext) { + return SpatialExtentCartesianPointDocValuesAggregatorFunction.create(driverContext, channels); + } + + @Override + public SpatialExtentCartesianPointDocValuesGroupingAggregatorFunction groupingAggregator( + DriverContext driverContext) { + return SpatialExtentCartesianPointDocValuesGroupingAggregatorFunction.create(channels, driverContext); + } + + @Override + public String describe() { + return "spatial_extent_cartesian_point_doc of valuess"; + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesGroupingAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesGroupingAggregatorFunction.java new file mode 100644 index 0000000000000..a5191e57959b8 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesGroupingAggregatorFunction.java @@ -0,0 +1,230 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import org.elasticsearch.compute.aggregation.GroupingAggregatorFunction; +import org.elasticsearch.compute.aggregation.IntermediateStateDesc; +import org.elasticsearch.compute.aggregation.SeenGroupIds; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.LongBlock; +import org.elasticsearch.compute.data.LongVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link GroupingAggregatorFunction} implementation for {@link SpatialExtentCartesianPointDocValuesAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentCartesianPointDocValuesGroupingAggregatorFunction implements GroupingAggregatorFunction { + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("minX", ElementType.INT), + new IntermediateStateDesc("maxX", ElementType.INT), + new IntermediateStateDesc("maxY", ElementType.INT), + new IntermediateStateDesc("minY", ElementType.INT) ); + + private final SpatialExtentGroupingState state; + + private final List channels; + + private final DriverContext driverContext; + + public SpatialExtentCartesianPointDocValuesGroupingAggregatorFunction(List channels, + SpatialExtentGroupingState state, DriverContext driverContext) { + this.channels = channels; + this.state = state; + this.driverContext = driverContext; + } + + public static SpatialExtentCartesianPointDocValuesGroupingAggregatorFunction create( + List channels, DriverContext driverContext) { + return new SpatialExtentCartesianPointDocValuesGroupingAggregatorFunction(channels, SpatialExtentCartesianPointDocValuesAggregator.initGrouping(), driverContext); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + @Override + public int intermediateBlockCount() { + return INTERMEDIATE_STATE_DESC.size(); + } + + @Override + public GroupingAggregatorFunction.AddInput prepareProcessPage(SeenGroupIds seenGroupIds, + Page page) { + LongBlock valuesBlock = page.getBlock(channels.get(0)); + LongVector valuesVector = valuesBlock.asVector(); + if (valuesVector == null) { + if (valuesBlock.mayHaveNulls()) { + state.enableGroupIdTracking(seenGroupIds); + } + return new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void close() { + } + }; + } + return new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesVector); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesVector); + } + + @Override + public void close() { + } + }; + } + + private void addRawInput(int positionOffset, IntVector groups, LongBlock values) { + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + if (values.isNull(groupPosition + positionOffset)) { + continue; + } + int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset); + int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset); + for (int v = valuesStart; v < valuesEnd; v++) { + SpatialExtentCartesianPointDocValuesAggregator.combine(state, groupId, values.getLong(v)); + } + } + } + + private void addRawInput(int positionOffset, IntVector groups, LongVector values) { + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + SpatialExtentCartesianPointDocValuesAggregator.combine(state, groupId, values.getLong(groupPosition + positionOffset)); + } + } + + private void addRawInput(int positionOffset, IntBlock groups, LongBlock values) { + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + int groupId = groups.getInt(g); + if (values.isNull(groupPosition + positionOffset)) { + continue; + } + int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset); + int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset); + for (int v = valuesStart; v < valuesEnd; v++) { + SpatialExtentCartesianPointDocValuesAggregator.combine(state, groupId, values.getLong(v)); + } + } + } + } + + private void addRawInput(int positionOffset, IntBlock groups, LongVector values) { + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + int groupId = groups.getInt(g); + SpatialExtentCartesianPointDocValuesAggregator.combine(state, groupId, values.getLong(groupPosition + positionOffset)); + } + } + } + + @Override + public void selectedMayContainUnseenGroups(SeenGroupIds seenGroupIds) { + state.enableGroupIdTracking(seenGroupIds); + } + + @Override + public void addIntermediateInput(int positionOffset, IntVector groups, Page page) { + state.enableGroupIdTracking(new SeenGroupIds.Empty()); + assert channels.size() == intermediateBlockCount(); + Block minXUncast = page.getBlock(channels.get(0)); + if (minXUncast.areAllValuesNull()) { + return; + } + IntVector minX = ((IntBlock) minXUncast).asVector(); + Block maxXUncast = page.getBlock(channels.get(1)); + if (maxXUncast.areAllValuesNull()) { + return; + } + IntVector maxX = ((IntBlock) maxXUncast).asVector(); + Block maxYUncast = page.getBlock(channels.get(2)); + if (maxYUncast.areAllValuesNull()) { + return; + } + IntVector maxY = ((IntBlock) maxYUncast).asVector(); + Block minYUncast = page.getBlock(channels.get(3)); + if (minYUncast.areAllValuesNull()) { + return; + } + IntVector minY = ((IntBlock) minYUncast).asVector(); + assert minX.getPositionCount() == maxX.getPositionCount() && minX.getPositionCount() == maxY.getPositionCount() && minX.getPositionCount() == minY.getPositionCount(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + SpatialExtentCartesianPointDocValuesAggregator.combineIntermediate(state, groupId, minX.getInt(groupPosition + positionOffset), maxX.getInt(groupPosition + positionOffset), maxY.getInt(groupPosition + positionOffset), minY.getInt(groupPosition + positionOffset)); + } + } + + @Override + public void addIntermediateRowInput(int groupId, GroupingAggregatorFunction input, int position) { + if (input.getClass() != getClass()) { + throw new IllegalArgumentException("expected " + getClass() + "; got " + input.getClass()); + } + SpatialExtentGroupingState inState = ((SpatialExtentCartesianPointDocValuesGroupingAggregatorFunction) input).state; + state.enableGroupIdTracking(new SeenGroupIds.Empty()); + SpatialExtentCartesianPointDocValuesAggregator.combineStates(state, groupId, inState, position); + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, IntVector selected) { + state.toIntermediate(blocks, offset, selected, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, IntVector selected, + DriverContext driverContext) { + blocks[offset] = SpatialExtentCartesianPointDocValuesAggregator.evaluateFinal(state, selected, driverContext); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesAggregatorFunction.java new file mode 100644 index 0000000000000..6610168e1df21 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesAggregatorFunction.java @@ -0,0 +1,192 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.aggregation.AggregatorFunction; +import org.elasticsearch.compute.aggregation.IntermediateStateDesc; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BooleanVector; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link AggregatorFunction} implementation for {@link SpatialExtentCartesianPointSourceValuesAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentCartesianPointSourceValuesAggregatorFunction implements AggregatorFunction { + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("minX", ElementType.INT), + new IntermediateStateDesc("maxX", ElementType.INT), + new IntermediateStateDesc("maxY", ElementType.INT), + new IntermediateStateDesc("minY", ElementType.INT) ); + + private final DriverContext driverContext; + + private final SpatialExtentState state; + + private final List channels; + + public SpatialExtentCartesianPointSourceValuesAggregatorFunction(DriverContext driverContext, + List channels, SpatialExtentState state) { + this.driverContext = driverContext; + this.channels = channels; + this.state = state; + } + + public static SpatialExtentCartesianPointSourceValuesAggregatorFunction create( + DriverContext driverContext, List channels) { + return new SpatialExtentCartesianPointSourceValuesAggregatorFunction(driverContext, channels, SpatialExtentCartesianPointSourceValuesAggregator.initSingle()); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + @Override + public int intermediateBlockCount() { + return INTERMEDIATE_STATE_DESC.size(); + } + + @Override + public void addRawInput(Page page, BooleanVector mask) { + if (mask.allFalse()) { + // Entire page masked away + return; + } + if (mask.allTrue()) { + // No masking + BytesRefBlock block = page.getBlock(channels.get(0)); + BytesRefVector vector = block.asVector(); + if (vector != null) { + addRawVector(vector); + } else { + addRawBlock(block); + } + return; + } + // Some positions masked away, others kept + BytesRefBlock block = page.getBlock(channels.get(0)); + BytesRefVector vector = block.asVector(); + if (vector != null) { + addRawVector(vector, mask); + } else { + addRawBlock(block, mask); + } + } + + private void addRawVector(BytesRefVector vector) { + BytesRef scratch = new BytesRef(); + for (int i = 0; i < vector.getPositionCount(); i++) { + SpatialExtentCartesianPointSourceValuesAggregator.combine(state, vector.getBytesRef(i, scratch)); + } + } + + private void addRawVector(BytesRefVector vector, BooleanVector mask) { + BytesRef scratch = new BytesRef(); + for (int i = 0; i < vector.getPositionCount(); i++) { + if (mask.getBoolean(i) == false) { + continue; + } + SpatialExtentCartesianPointSourceValuesAggregator.combine(state, vector.getBytesRef(i, scratch)); + } + } + + private void addRawBlock(BytesRefBlock block) { + BytesRef scratch = new BytesRef(); + for (int p = 0; p < block.getPositionCount(); p++) { + if (block.isNull(p)) { + continue; + } + int start = block.getFirstValueIndex(p); + int end = start + block.getValueCount(p); + for (int i = start; i < end; i++) { + SpatialExtentCartesianPointSourceValuesAggregator.combine(state, block.getBytesRef(i, scratch)); + } + } + } + + private void addRawBlock(BytesRefBlock block, BooleanVector mask) { + BytesRef scratch = new BytesRef(); + for (int p = 0; p < block.getPositionCount(); p++) { + if (mask.getBoolean(p) == false) { + continue; + } + if (block.isNull(p)) { + continue; + } + int start = block.getFirstValueIndex(p); + int end = start + block.getValueCount(p); + for (int i = start; i < end; i++) { + SpatialExtentCartesianPointSourceValuesAggregator.combine(state, block.getBytesRef(i, scratch)); + } + } + } + + @Override + public void addIntermediateInput(Page page) { + assert channels.size() == intermediateBlockCount(); + assert page.getBlockCount() >= channels.get(0) + intermediateStateDesc().size(); + Block minXUncast = page.getBlock(channels.get(0)); + if (minXUncast.areAllValuesNull()) { + return; + } + IntVector minX = ((IntBlock) minXUncast).asVector(); + assert minX.getPositionCount() == 1; + Block maxXUncast = page.getBlock(channels.get(1)); + if (maxXUncast.areAllValuesNull()) { + return; + } + IntVector maxX = ((IntBlock) maxXUncast).asVector(); + assert maxX.getPositionCount() == 1; + Block maxYUncast = page.getBlock(channels.get(2)); + if (maxYUncast.areAllValuesNull()) { + return; + } + IntVector maxY = ((IntBlock) maxYUncast).asVector(); + assert maxY.getPositionCount() == 1; + Block minYUncast = page.getBlock(channels.get(3)); + if (minYUncast.areAllValuesNull()) { + return; + } + IntVector minY = ((IntBlock) minYUncast).asVector(); + assert minY.getPositionCount() == 1; + SpatialExtentCartesianPointSourceValuesAggregator.combineIntermediate(state, minX.getInt(0), maxX.getInt(0), maxY.getInt(0), minY.getInt(0)); + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, DriverContext driverContext) { + state.toIntermediate(blocks, offset, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, DriverContext driverContext) { + blocks[offset] = SpatialExtentCartesianPointSourceValuesAggregator.evaluateFinal(state, driverContext); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesAggregatorFunctionSupplier.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesAggregatorFunctionSupplier.java new file mode 100644 index 0000000000000..7f4d1d69ae928 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesAggregatorFunctionSupplier.java @@ -0,0 +1,41 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.util.List; +import org.elasticsearch.compute.aggregation.AggregatorFunctionSupplier; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link AggregatorFunctionSupplier} implementation for {@link SpatialExtentCartesianPointSourceValuesAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentCartesianPointSourceValuesAggregatorFunctionSupplier implements AggregatorFunctionSupplier { + private final List channels; + + public SpatialExtentCartesianPointSourceValuesAggregatorFunctionSupplier(List channels) { + this.channels = channels; + } + + @Override + public SpatialExtentCartesianPointSourceValuesAggregatorFunction aggregator( + DriverContext driverContext) { + return SpatialExtentCartesianPointSourceValuesAggregatorFunction.create(driverContext, channels); + } + + @Override + public SpatialExtentCartesianPointSourceValuesGroupingAggregatorFunction groupingAggregator( + DriverContext driverContext) { + return SpatialExtentCartesianPointSourceValuesGroupingAggregatorFunction.create(channels, driverContext); + } + + @Override + public String describe() { + return "spatial_extent_cartesian_point_source of valuess"; + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesGroupingAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesGroupingAggregatorFunction.java new file mode 100644 index 0000000000000..4e06158952fc3 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesGroupingAggregatorFunction.java @@ -0,0 +1,235 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.aggregation.GroupingAggregatorFunction; +import org.elasticsearch.compute.aggregation.IntermediateStateDesc; +import org.elasticsearch.compute.aggregation.SeenGroupIds; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link GroupingAggregatorFunction} implementation for {@link SpatialExtentCartesianPointSourceValuesAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentCartesianPointSourceValuesGroupingAggregatorFunction implements GroupingAggregatorFunction { + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("minX", ElementType.INT), + new IntermediateStateDesc("maxX", ElementType.INT), + new IntermediateStateDesc("maxY", ElementType.INT), + new IntermediateStateDesc("minY", ElementType.INT) ); + + private final SpatialExtentGroupingState state; + + private final List channels; + + private final DriverContext driverContext; + + public SpatialExtentCartesianPointSourceValuesGroupingAggregatorFunction(List channels, + SpatialExtentGroupingState state, DriverContext driverContext) { + this.channels = channels; + this.state = state; + this.driverContext = driverContext; + } + + public static SpatialExtentCartesianPointSourceValuesGroupingAggregatorFunction create( + List channels, DriverContext driverContext) { + return new SpatialExtentCartesianPointSourceValuesGroupingAggregatorFunction(channels, SpatialExtentCartesianPointSourceValuesAggregator.initGrouping(), driverContext); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + @Override + public int intermediateBlockCount() { + return INTERMEDIATE_STATE_DESC.size(); + } + + @Override + public GroupingAggregatorFunction.AddInput prepareProcessPage(SeenGroupIds seenGroupIds, + Page page) { + BytesRefBlock valuesBlock = page.getBlock(channels.get(0)); + BytesRefVector valuesVector = valuesBlock.asVector(); + if (valuesVector == null) { + if (valuesBlock.mayHaveNulls()) { + state.enableGroupIdTracking(seenGroupIds); + } + return new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void close() { + } + }; + } + return new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesVector); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesVector); + } + + @Override + public void close() { + } + }; + } + + private void addRawInput(int positionOffset, IntVector groups, BytesRefBlock values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + if (values.isNull(groupPosition + positionOffset)) { + continue; + } + int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset); + int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset); + for (int v = valuesStart; v < valuesEnd; v++) { + SpatialExtentCartesianPointSourceValuesAggregator.combine(state, groupId, values.getBytesRef(v, scratch)); + } + } + } + + private void addRawInput(int positionOffset, IntVector groups, BytesRefVector values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + SpatialExtentCartesianPointSourceValuesAggregator.combine(state, groupId, values.getBytesRef(groupPosition + positionOffset, scratch)); + } + } + + private void addRawInput(int positionOffset, IntBlock groups, BytesRefBlock values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + int groupId = groups.getInt(g); + if (values.isNull(groupPosition + positionOffset)) { + continue; + } + int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset); + int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset); + for (int v = valuesStart; v < valuesEnd; v++) { + SpatialExtentCartesianPointSourceValuesAggregator.combine(state, groupId, values.getBytesRef(v, scratch)); + } + } + } + } + + private void addRawInput(int positionOffset, IntBlock groups, BytesRefVector values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + int groupId = groups.getInt(g); + SpatialExtentCartesianPointSourceValuesAggregator.combine(state, groupId, values.getBytesRef(groupPosition + positionOffset, scratch)); + } + } + } + + @Override + public void selectedMayContainUnseenGroups(SeenGroupIds seenGroupIds) { + state.enableGroupIdTracking(seenGroupIds); + } + + @Override + public void addIntermediateInput(int positionOffset, IntVector groups, Page page) { + state.enableGroupIdTracking(new SeenGroupIds.Empty()); + assert channels.size() == intermediateBlockCount(); + Block minXUncast = page.getBlock(channels.get(0)); + if (minXUncast.areAllValuesNull()) { + return; + } + IntVector minX = ((IntBlock) minXUncast).asVector(); + Block maxXUncast = page.getBlock(channels.get(1)); + if (maxXUncast.areAllValuesNull()) { + return; + } + IntVector maxX = ((IntBlock) maxXUncast).asVector(); + Block maxYUncast = page.getBlock(channels.get(2)); + if (maxYUncast.areAllValuesNull()) { + return; + } + IntVector maxY = ((IntBlock) maxYUncast).asVector(); + Block minYUncast = page.getBlock(channels.get(3)); + if (minYUncast.areAllValuesNull()) { + return; + } + IntVector minY = ((IntBlock) minYUncast).asVector(); + assert minX.getPositionCount() == maxX.getPositionCount() && minX.getPositionCount() == maxY.getPositionCount() && minX.getPositionCount() == minY.getPositionCount(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + SpatialExtentCartesianPointSourceValuesAggregator.combineIntermediate(state, groupId, minX.getInt(groupPosition + positionOffset), maxX.getInt(groupPosition + positionOffset), maxY.getInt(groupPosition + positionOffset), minY.getInt(groupPosition + positionOffset)); + } + } + + @Override + public void addIntermediateRowInput(int groupId, GroupingAggregatorFunction input, int position) { + if (input.getClass() != getClass()) { + throw new IllegalArgumentException("expected " + getClass() + "; got " + input.getClass()); + } + SpatialExtentGroupingState inState = ((SpatialExtentCartesianPointSourceValuesGroupingAggregatorFunction) input).state; + state.enableGroupIdTracking(new SeenGroupIds.Empty()); + SpatialExtentCartesianPointSourceValuesAggregator.combineStates(state, groupId, inState, position); + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, IntVector selected) { + state.toIntermediate(blocks, offset, selected, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, IntVector selected, + DriverContext driverContext) { + blocks[offset] = SpatialExtentCartesianPointSourceValuesAggregator.evaluateFinal(state, selected, driverContext); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeAggregatorFunction.java new file mode 100644 index 0000000000000..19aa4f7ca78a2 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeAggregatorFunction.java @@ -0,0 +1,192 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.aggregation.AggregatorFunction; +import org.elasticsearch.compute.aggregation.IntermediateStateDesc; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BooleanVector; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link AggregatorFunction} implementation for {@link SpatialExtentCartesianShapeAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentCartesianShapeAggregatorFunction implements AggregatorFunction { + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("minX", ElementType.INT), + new IntermediateStateDesc("maxX", ElementType.INT), + new IntermediateStateDesc("maxY", ElementType.INT), + new IntermediateStateDesc("minY", ElementType.INT) ); + + private final DriverContext driverContext; + + private final SpatialExtentState state; + + private final List channels; + + public SpatialExtentCartesianShapeAggregatorFunction(DriverContext driverContext, + List channels, SpatialExtentState state) { + this.driverContext = driverContext; + this.channels = channels; + this.state = state; + } + + public static SpatialExtentCartesianShapeAggregatorFunction create(DriverContext driverContext, + List channels) { + return new SpatialExtentCartesianShapeAggregatorFunction(driverContext, channels, SpatialExtentCartesianShapeAggregator.initSingle()); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + @Override + public int intermediateBlockCount() { + return INTERMEDIATE_STATE_DESC.size(); + } + + @Override + public void addRawInput(Page page, BooleanVector mask) { + if (mask.allFalse()) { + // Entire page masked away + return; + } + if (mask.allTrue()) { + // No masking + BytesRefBlock block = page.getBlock(channels.get(0)); + BytesRefVector vector = block.asVector(); + if (vector != null) { + addRawVector(vector); + } else { + addRawBlock(block); + } + return; + } + // Some positions masked away, others kept + BytesRefBlock block = page.getBlock(channels.get(0)); + BytesRefVector vector = block.asVector(); + if (vector != null) { + addRawVector(vector, mask); + } else { + addRawBlock(block, mask); + } + } + + private void addRawVector(BytesRefVector vector) { + BytesRef scratch = new BytesRef(); + for (int i = 0; i < vector.getPositionCount(); i++) { + SpatialExtentCartesianShapeAggregator.combine(state, vector.getBytesRef(i, scratch)); + } + } + + private void addRawVector(BytesRefVector vector, BooleanVector mask) { + BytesRef scratch = new BytesRef(); + for (int i = 0; i < vector.getPositionCount(); i++) { + if (mask.getBoolean(i) == false) { + continue; + } + SpatialExtentCartesianShapeAggregator.combine(state, vector.getBytesRef(i, scratch)); + } + } + + private void addRawBlock(BytesRefBlock block) { + BytesRef scratch = new BytesRef(); + for (int p = 0; p < block.getPositionCount(); p++) { + if (block.isNull(p)) { + continue; + } + int start = block.getFirstValueIndex(p); + int end = start + block.getValueCount(p); + for (int i = start; i < end; i++) { + SpatialExtentCartesianShapeAggregator.combine(state, block.getBytesRef(i, scratch)); + } + } + } + + private void addRawBlock(BytesRefBlock block, BooleanVector mask) { + BytesRef scratch = new BytesRef(); + for (int p = 0; p < block.getPositionCount(); p++) { + if (mask.getBoolean(p) == false) { + continue; + } + if (block.isNull(p)) { + continue; + } + int start = block.getFirstValueIndex(p); + int end = start + block.getValueCount(p); + for (int i = start; i < end; i++) { + SpatialExtentCartesianShapeAggregator.combine(state, block.getBytesRef(i, scratch)); + } + } + } + + @Override + public void addIntermediateInput(Page page) { + assert channels.size() == intermediateBlockCount(); + assert page.getBlockCount() >= channels.get(0) + intermediateStateDesc().size(); + Block minXUncast = page.getBlock(channels.get(0)); + if (minXUncast.areAllValuesNull()) { + return; + } + IntVector minX = ((IntBlock) minXUncast).asVector(); + assert minX.getPositionCount() == 1; + Block maxXUncast = page.getBlock(channels.get(1)); + if (maxXUncast.areAllValuesNull()) { + return; + } + IntVector maxX = ((IntBlock) maxXUncast).asVector(); + assert maxX.getPositionCount() == 1; + Block maxYUncast = page.getBlock(channels.get(2)); + if (maxYUncast.areAllValuesNull()) { + return; + } + IntVector maxY = ((IntBlock) maxYUncast).asVector(); + assert maxY.getPositionCount() == 1; + Block minYUncast = page.getBlock(channels.get(3)); + if (minYUncast.areAllValuesNull()) { + return; + } + IntVector minY = ((IntBlock) minYUncast).asVector(); + assert minY.getPositionCount() == 1; + SpatialExtentCartesianShapeAggregator.combineIntermediate(state, minX.getInt(0), maxX.getInt(0), maxY.getInt(0), minY.getInt(0)); + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, DriverContext driverContext) { + state.toIntermediate(blocks, offset, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, DriverContext driverContext) { + blocks[offset] = SpatialExtentCartesianShapeAggregator.evaluateFinal(state, driverContext); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeAggregatorFunctionSupplier.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeAggregatorFunctionSupplier.java new file mode 100644 index 0000000000000..9e4b292a0ea29 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeAggregatorFunctionSupplier.java @@ -0,0 +1,40 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.util.List; +import org.elasticsearch.compute.aggregation.AggregatorFunctionSupplier; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link AggregatorFunctionSupplier} implementation for {@link SpatialExtentCartesianShapeAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentCartesianShapeAggregatorFunctionSupplier implements AggregatorFunctionSupplier { + private final List channels; + + public SpatialExtentCartesianShapeAggregatorFunctionSupplier(List channels) { + this.channels = channels; + } + + @Override + public SpatialExtentCartesianShapeAggregatorFunction aggregator(DriverContext driverContext) { + return SpatialExtentCartesianShapeAggregatorFunction.create(driverContext, channels); + } + + @Override + public SpatialExtentCartesianShapeGroupingAggregatorFunction groupingAggregator( + DriverContext driverContext) { + return SpatialExtentCartesianShapeGroupingAggregatorFunction.create(channels, driverContext); + } + + @Override + public String describe() { + return "spatial_extent_cartesian of shapes"; + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeGroupingAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeGroupingAggregatorFunction.java new file mode 100644 index 0000000000000..c55c3d9c66946 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeGroupingAggregatorFunction.java @@ -0,0 +1,235 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.aggregation.GroupingAggregatorFunction; +import org.elasticsearch.compute.aggregation.IntermediateStateDesc; +import org.elasticsearch.compute.aggregation.SeenGroupIds; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link GroupingAggregatorFunction} implementation for {@link SpatialExtentCartesianShapeAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentCartesianShapeGroupingAggregatorFunction implements GroupingAggregatorFunction { + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("minX", ElementType.INT), + new IntermediateStateDesc("maxX", ElementType.INT), + new IntermediateStateDesc("maxY", ElementType.INT), + new IntermediateStateDesc("minY", ElementType.INT) ); + + private final SpatialExtentGroupingState state; + + private final List channels; + + private final DriverContext driverContext; + + public SpatialExtentCartesianShapeGroupingAggregatorFunction(List channels, + SpatialExtentGroupingState state, DriverContext driverContext) { + this.channels = channels; + this.state = state; + this.driverContext = driverContext; + } + + public static SpatialExtentCartesianShapeGroupingAggregatorFunction create(List channels, + DriverContext driverContext) { + return new SpatialExtentCartesianShapeGroupingAggregatorFunction(channels, SpatialExtentCartesianShapeAggregator.initGrouping(), driverContext); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + @Override + public int intermediateBlockCount() { + return INTERMEDIATE_STATE_DESC.size(); + } + + @Override + public GroupingAggregatorFunction.AddInput prepareProcessPage(SeenGroupIds seenGroupIds, + Page page) { + BytesRefBlock valuesBlock = page.getBlock(channels.get(0)); + BytesRefVector valuesVector = valuesBlock.asVector(); + if (valuesVector == null) { + if (valuesBlock.mayHaveNulls()) { + state.enableGroupIdTracking(seenGroupIds); + } + return new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void close() { + } + }; + } + return new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesVector); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesVector); + } + + @Override + public void close() { + } + }; + } + + private void addRawInput(int positionOffset, IntVector groups, BytesRefBlock values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + if (values.isNull(groupPosition + positionOffset)) { + continue; + } + int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset); + int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset); + for (int v = valuesStart; v < valuesEnd; v++) { + SpatialExtentCartesianShapeAggregator.combine(state, groupId, values.getBytesRef(v, scratch)); + } + } + } + + private void addRawInput(int positionOffset, IntVector groups, BytesRefVector values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + SpatialExtentCartesianShapeAggregator.combine(state, groupId, values.getBytesRef(groupPosition + positionOffset, scratch)); + } + } + + private void addRawInput(int positionOffset, IntBlock groups, BytesRefBlock values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + int groupId = groups.getInt(g); + if (values.isNull(groupPosition + positionOffset)) { + continue; + } + int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset); + int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset); + for (int v = valuesStart; v < valuesEnd; v++) { + SpatialExtentCartesianShapeAggregator.combine(state, groupId, values.getBytesRef(v, scratch)); + } + } + } + } + + private void addRawInput(int positionOffset, IntBlock groups, BytesRefVector values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + int groupId = groups.getInt(g); + SpatialExtentCartesianShapeAggregator.combine(state, groupId, values.getBytesRef(groupPosition + positionOffset, scratch)); + } + } + } + + @Override + public void selectedMayContainUnseenGroups(SeenGroupIds seenGroupIds) { + state.enableGroupIdTracking(seenGroupIds); + } + + @Override + public void addIntermediateInput(int positionOffset, IntVector groups, Page page) { + state.enableGroupIdTracking(new SeenGroupIds.Empty()); + assert channels.size() == intermediateBlockCount(); + Block minXUncast = page.getBlock(channels.get(0)); + if (minXUncast.areAllValuesNull()) { + return; + } + IntVector minX = ((IntBlock) minXUncast).asVector(); + Block maxXUncast = page.getBlock(channels.get(1)); + if (maxXUncast.areAllValuesNull()) { + return; + } + IntVector maxX = ((IntBlock) maxXUncast).asVector(); + Block maxYUncast = page.getBlock(channels.get(2)); + if (maxYUncast.areAllValuesNull()) { + return; + } + IntVector maxY = ((IntBlock) maxYUncast).asVector(); + Block minYUncast = page.getBlock(channels.get(3)); + if (minYUncast.areAllValuesNull()) { + return; + } + IntVector minY = ((IntBlock) minYUncast).asVector(); + assert minX.getPositionCount() == maxX.getPositionCount() && minX.getPositionCount() == maxY.getPositionCount() && minX.getPositionCount() == minY.getPositionCount(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + SpatialExtentCartesianShapeAggregator.combineIntermediate(state, groupId, minX.getInt(groupPosition + positionOffset), maxX.getInt(groupPosition + positionOffset), maxY.getInt(groupPosition + positionOffset), minY.getInt(groupPosition + positionOffset)); + } + } + + @Override + public void addIntermediateRowInput(int groupId, GroupingAggregatorFunction input, int position) { + if (input.getClass() != getClass()) { + throw new IllegalArgumentException("expected " + getClass() + "; got " + input.getClass()); + } + SpatialExtentGroupingState inState = ((SpatialExtentCartesianShapeGroupingAggregatorFunction) input).state; + state.enableGroupIdTracking(new SeenGroupIds.Empty()); + SpatialExtentCartesianShapeAggregator.combineStates(state, groupId, inState, position); + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, IntVector selected) { + state.toIntermediate(blocks, offset, selected, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, IntVector selected, + DriverContext driverContext) { + blocks[offset] = SpatialExtentCartesianShapeAggregator.evaluateFinal(state, selected, driverContext); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesAggregatorFunction.java new file mode 100644 index 0000000000000..c883e82d45989 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesAggregatorFunction.java @@ -0,0 +1,201 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import org.elasticsearch.compute.aggregation.AggregatorFunction; +import org.elasticsearch.compute.aggregation.IntermediateStateDesc; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BooleanVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.LongBlock; +import org.elasticsearch.compute.data.LongVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link AggregatorFunction} implementation for {@link SpatialExtentGeoPointDocValuesAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentGeoPointDocValuesAggregatorFunction implements AggregatorFunction { + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("minNegX", ElementType.INT), + new IntermediateStateDesc("minPosX", ElementType.INT), + new IntermediateStateDesc("maxNegX", ElementType.INT), + new IntermediateStateDesc("maxPosX", ElementType.INT), + new IntermediateStateDesc("maxY", ElementType.INT), + new IntermediateStateDesc("minY", ElementType.INT) ); + + private final DriverContext driverContext; + + private final SpatialExtentStateWrappedLongitudeState state; + + private final List channels; + + public SpatialExtentGeoPointDocValuesAggregatorFunction(DriverContext driverContext, + List channels, SpatialExtentStateWrappedLongitudeState state) { + this.driverContext = driverContext; + this.channels = channels; + this.state = state; + } + + public static SpatialExtentGeoPointDocValuesAggregatorFunction create(DriverContext driverContext, + List channels) { + return new SpatialExtentGeoPointDocValuesAggregatorFunction(driverContext, channels, SpatialExtentGeoPointDocValuesAggregator.initSingle()); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + @Override + public int intermediateBlockCount() { + return INTERMEDIATE_STATE_DESC.size(); + } + + @Override + public void addRawInput(Page page, BooleanVector mask) { + if (mask.allFalse()) { + // Entire page masked away + return; + } + if (mask.allTrue()) { + // No masking + LongBlock block = page.getBlock(channels.get(0)); + LongVector vector = block.asVector(); + if (vector != null) { + addRawVector(vector); + } else { + addRawBlock(block); + } + return; + } + // Some positions masked away, others kept + LongBlock block = page.getBlock(channels.get(0)); + LongVector vector = block.asVector(); + if (vector != null) { + addRawVector(vector, mask); + } else { + addRawBlock(block, mask); + } + } + + private void addRawVector(LongVector vector) { + for (int i = 0; i < vector.getPositionCount(); i++) { + SpatialExtentGeoPointDocValuesAggregator.combine(state, vector.getLong(i)); + } + } + + private void addRawVector(LongVector vector, BooleanVector mask) { + for (int i = 0; i < vector.getPositionCount(); i++) { + if (mask.getBoolean(i) == false) { + continue; + } + SpatialExtentGeoPointDocValuesAggregator.combine(state, vector.getLong(i)); + } + } + + private void addRawBlock(LongBlock block) { + for (int p = 0; p < block.getPositionCount(); p++) { + if (block.isNull(p)) { + continue; + } + int start = block.getFirstValueIndex(p); + int end = start + block.getValueCount(p); + for (int i = start; i < end; i++) { + SpatialExtentGeoPointDocValuesAggregator.combine(state, block.getLong(i)); + } + } + } + + private void addRawBlock(LongBlock block, BooleanVector mask) { + for (int p = 0; p < block.getPositionCount(); p++) { + if (mask.getBoolean(p) == false) { + continue; + } + if (block.isNull(p)) { + continue; + } + int start = block.getFirstValueIndex(p); + int end = start + block.getValueCount(p); + for (int i = start; i < end; i++) { + SpatialExtentGeoPointDocValuesAggregator.combine(state, block.getLong(i)); + } + } + } + + @Override + public void addIntermediateInput(Page page) { + assert channels.size() == intermediateBlockCount(); + assert page.getBlockCount() >= channels.get(0) + intermediateStateDesc().size(); + Block minNegXUncast = page.getBlock(channels.get(0)); + if (minNegXUncast.areAllValuesNull()) { + return; + } + IntVector minNegX = ((IntBlock) minNegXUncast).asVector(); + assert minNegX.getPositionCount() == 1; + Block minPosXUncast = page.getBlock(channels.get(1)); + if (minPosXUncast.areAllValuesNull()) { + return; + } + IntVector minPosX = ((IntBlock) minPosXUncast).asVector(); + assert minPosX.getPositionCount() == 1; + Block maxNegXUncast = page.getBlock(channels.get(2)); + if (maxNegXUncast.areAllValuesNull()) { + return; + } + IntVector maxNegX = ((IntBlock) maxNegXUncast).asVector(); + assert maxNegX.getPositionCount() == 1; + Block maxPosXUncast = page.getBlock(channels.get(3)); + if (maxPosXUncast.areAllValuesNull()) { + return; + } + IntVector maxPosX = ((IntBlock) maxPosXUncast).asVector(); + assert maxPosX.getPositionCount() == 1; + Block maxYUncast = page.getBlock(channels.get(4)); + if (maxYUncast.areAllValuesNull()) { + return; + } + IntVector maxY = ((IntBlock) maxYUncast).asVector(); + assert maxY.getPositionCount() == 1; + Block minYUncast = page.getBlock(channels.get(5)); + if (minYUncast.areAllValuesNull()) { + return; + } + IntVector minY = ((IntBlock) minYUncast).asVector(); + assert minY.getPositionCount() == 1; + SpatialExtentGeoPointDocValuesAggregator.combineIntermediate(state, minNegX.getInt(0), minPosX.getInt(0), maxNegX.getInt(0), maxPosX.getInt(0), maxY.getInt(0), minY.getInt(0)); + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, DriverContext driverContext) { + state.toIntermediate(blocks, offset, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, DriverContext driverContext) { + blocks[offset] = SpatialExtentGeoPointDocValuesAggregator.evaluateFinal(state, driverContext); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesAggregatorFunctionSupplier.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesAggregatorFunctionSupplier.java new file mode 100644 index 0000000000000..f72a4cc648ec8 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesAggregatorFunctionSupplier.java @@ -0,0 +1,40 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.util.List; +import org.elasticsearch.compute.aggregation.AggregatorFunctionSupplier; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link AggregatorFunctionSupplier} implementation for {@link SpatialExtentGeoPointDocValuesAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentGeoPointDocValuesAggregatorFunctionSupplier implements AggregatorFunctionSupplier { + private final List channels; + + public SpatialExtentGeoPointDocValuesAggregatorFunctionSupplier(List channels) { + this.channels = channels; + } + + @Override + public SpatialExtentGeoPointDocValuesAggregatorFunction aggregator(DriverContext driverContext) { + return SpatialExtentGeoPointDocValuesAggregatorFunction.create(driverContext, channels); + } + + @Override + public SpatialExtentGeoPointDocValuesGroupingAggregatorFunction groupingAggregator( + DriverContext driverContext) { + return SpatialExtentGeoPointDocValuesGroupingAggregatorFunction.create(channels, driverContext); + } + + @Override + public String describe() { + return "spatial_extent_geo_point_doc of valuess"; + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesGroupingAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesGroupingAggregatorFunction.java new file mode 100644 index 0000000000000..eee5bc5df41a4 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesGroupingAggregatorFunction.java @@ -0,0 +1,242 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import org.elasticsearch.compute.aggregation.GroupingAggregatorFunction; +import org.elasticsearch.compute.aggregation.IntermediateStateDesc; +import org.elasticsearch.compute.aggregation.SeenGroupIds; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.LongBlock; +import org.elasticsearch.compute.data.LongVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link GroupingAggregatorFunction} implementation for {@link SpatialExtentGeoPointDocValuesAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentGeoPointDocValuesGroupingAggregatorFunction implements GroupingAggregatorFunction { + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("minNegX", ElementType.INT), + new IntermediateStateDesc("minPosX", ElementType.INT), + new IntermediateStateDesc("maxNegX", ElementType.INT), + new IntermediateStateDesc("maxPosX", ElementType.INT), + new IntermediateStateDesc("maxY", ElementType.INT), + new IntermediateStateDesc("minY", ElementType.INT) ); + + private final SpatialExtentGroupingStateWrappedLongitudeState state; + + private final List channels; + + private final DriverContext driverContext; + + public SpatialExtentGeoPointDocValuesGroupingAggregatorFunction(List channels, + SpatialExtentGroupingStateWrappedLongitudeState state, DriverContext driverContext) { + this.channels = channels; + this.state = state; + this.driverContext = driverContext; + } + + public static SpatialExtentGeoPointDocValuesGroupingAggregatorFunction create( + List channels, DriverContext driverContext) { + return new SpatialExtentGeoPointDocValuesGroupingAggregatorFunction(channels, SpatialExtentGeoPointDocValuesAggregator.initGrouping(), driverContext); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + @Override + public int intermediateBlockCount() { + return INTERMEDIATE_STATE_DESC.size(); + } + + @Override + public GroupingAggregatorFunction.AddInput prepareProcessPage(SeenGroupIds seenGroupIds, + Page page) { + LongBlock valuesBlock = page.getBlock(channels.get(0)); + LongVector valuesVector = valuesBlock.asVector(); + if (valuesVector == null) { + if (valuesBlock.mayHaveNulls()) { + state.enableGroupIdTracking(seenGroupIds); + } + return new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void close() { + } + }; + } + return new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesVector); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesVector); + } + + @Override + public void close() { + } + }; + } + + private void addRawInput(int positionOffset, IntVector groups, LongBlock values) { + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + if (values.isNull(groupPosition + positionOffset)) { + continue; + } + int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset); + int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset); + for (int v = valuesStart; v < valuesEnd; v++) { + SpatialExtentGeoPointDocValuesAggregator.combine(state, groupId, values.getLong(v)); + } + } + } + + private void addRawInput(int positionOffset, IntVector groups, LongVector values) { + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + SpatialExtentGeoPointDocValuesAggregator.combine(state, groupId, values.getLong(groupPosition + positionOffset)); + } + } + + private void addRawInput(int positionOffset, IntBlock groups, LongBlock values) { + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + int groupId = groups.getInt(g); + if (values.isNull(groupPosition + positionOffset)) { + continue; + } + int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset); + int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset); + for (int v = valuesStart; v < valuesEnd; v++) { + SpatialExtentGeoPointDocValuesAggregator.combine(state, groupId, values.getLong(v)); + } + } + } + } + + private void addRawInput(int positionOffset, IntBlock groups, LongVector values) { + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + int groupId = groups.getInt(g); + SpatialExtentGeoPointDocValuesAggregator.combine(state, groupId, values.getLong(groupPosition + positionOffset)); + } + } + } + + @Override + public void selectedMayContainUnseenGroups(SeenGroupIds seenGroupIds) { + state.enableGroupIdTracking(seenGroupIds); + } + + @Override + public void addIntermediateInput(int positionOffset, IntVector groups, Page page) { + state.enableGroupIdTracking(new SeenGroupIds.Empty()); + assert channels.size() == intermediateBlockCount(); + Block minNegXUncast = page.getBlock(channels.get(0)); + if (minNegXUncast.areAllValuesNull()) { + return; + } + IntVector minNegX = ((IntBlock) minNegXUncast).asVector(); + Block minPosXUncast = page.getBlock(channels.get(1)); + if (minPosXUncast.areAllValuesNull()) { + return; + } + IntVector minPosX = ((IntBlock) minPosXUncast).asVector(); + Block maxNegXUncast = page.getBlock(channels.get(2)); + if (maxNegXUncast.areAllValuesNull()) { + return; + } + IntVector maxNegX = ((IntBlock) maxNegXUncast).asVector(); + Block maxPosXUncast = page.getBlock(channels.get(3)); + if (maxPosXUncast.areAllValuesNull()) { + return; + } + IntVector maxPosX = ((IntBlock) maxPosXUncast).asVector(); + Block maxYUncast = page.getBlock(channels.get(4)); + if (maxYUncast.areAllValuesNull()) { + return; + } + IntVector maxY = ((IntBlock) maxYUncast).asVector(); + Block minYUncast = page.getBlock(channels.get(5)); + if (minYUncast.areAllValuesNull()) { + return; + } + IntVector minY = ((IntBlock) minYUncast).asVector(); + assert minNegX.getPositionCount() == minPosX.getPositionCount() && minNegX.getPositionCount() == maxNegX.getPositionCount() && minNegX.getPositionCount() == maxPosX.getPositionCount() && minNegX.getPositionCount() == maxY.getPositionCount() && minNegX.getPositionCount() == minY.getPositionCount(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + SpatialExtentGeoPointDocValuesAggregator.combineIntermediate(state, groupId, minNegX.getInt(groupPosition + positionOffset), minPosX.getInt(groupPosition + positionOffset), maxNegX.getInt(groupPosition + positionOffset), maxPosX.getInt(groupPosition + positionOffset), maxY.getInt(groupPosition + positionOffset), minY.getInt(groupPosition + positionOffset)); + } + } + + @Override + public void addIntermediateRowInput(int groupId, GroupingAggregatorFunction input, int position) { + if (input.getClass() != getClass()) { + throw new IllegalArgumentException("expected " + getClass() + "; got " + input.getClass()); + } + SpatialExtentGroupingStateWrappedLongitudeState inState = ((SpatialExtentGeoPointDocValuesGroupingAggregatorFunction) input).state; + state.enableGroupIdTracking(new SeenGroupIds.Empty()); + SpatialExtentGeoPointDocValuesAggregator.combineStates(state, groupId, inState, position); + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, IntVector selected) { + state.toIntermediate(blocks, offset, selected, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, IntVector selected, + DriverContext driverContext) { + blocks[offset] = SpatialExtentGeoPointDocValuesAggregator.evaluateFinal(state, selected, driverContext); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesAggregatorFunction.java new file mode 100644 index 0000000000000..cf65fbdde594c --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesAggregatorFunction.java @@ -0,0 +1,206 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.aggregation.AggregatorFunction; +import org.elasticsearch.compute.aggregation.IntermediateStateDesc; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BooleanVector; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link AggregatorFunction} implementation for {@link SpatialExtentGeoPointSourceValuesAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentGeoPointSourceValuesAggregatorFunction implements AggregatorFunction { + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("minNegX", ElementType.INT), + new IntermediateStateDesc("minPosX", ElementType.INT), + new IntermediateStateDesc("maxNegX", ElementType.INT), + new IntermediateStateDesc("maxPosX", ElementType.INT), + new IntermediateStateDesc("maxY", ElementType.INT), + new IntermediateStateDesc("minY", ElementType.INT) ); + + private final DriverContext driverContext; + + private final SpatialExtentStateWrappedLongitudeState state; + + private final List channels; + + public SpatialExtentGeoPointSourceValuesAggregatorFunction(DriverContext driverContext, + List channels, SpatialExtentStateWrappedLongitudeState state) { + this.driverContext = driverContext; + this.channels = channels; + this.state = state; + } + + public static SpatialExtentGeoPointSourceValuesAggregatorFunction create( + DriverContext driverContext, List channels) { + return new SpatialExtentGeoPointSourceValuesAggregatorFunction(driverContext, channels, SpatialExtentGeoPointSourceValuesAggregator.initSingle()); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + @Override + public int intermediateBlockCount() { + return INTERMEDIATE_STATE_DESC.size(); + } + + @Override + public void addRawInput(Page page, BooleanVector mask) { + if (mask.allFalse()) { + // Entire page masked away + return; + } + if (mask.allTrue()) { + // No masking + BytesRefBlock block = page.getBlock(channels.get(0)); + BytesRefVector vector = block.asVector(); + if (vector != null) { + addRawVector(vector); + } else { + addRawBlock(block); + } + return; + } + // Some positions masked away, others kept + BytesRefBlock block = page.getBlock(channels.get(0)); + BytesRefVector vector = block.asVector(); + if (vector != null) { + addRawVector(vector, mask); + } else { + addRawBlock(block, mask); + } + } + + private void addRawVector(BytesRefVector vector) { + BytesRef scratch = new BytesRef(); + for (int i = 0; i < vector.getPositionCount(); i++) { + SpatialExtentGeoPointSourceValuesAggregator.combine(state, vector.getBytesRef(i, scratch)); + } + } + + private void addRawVector(BytesRefVector vector, BooleanVector mask) { + BytesRef scratch = new BytesRef(); + for (int i = 0; i < vector.getPositionCount(); i++) { + if (mask.getBoolean(i) == false) { + continue; + } + SpatialExtentGeoPointSourceValuesAggregator.combine(state, vector.getBytesRef(i, scratch)); + } + } + + private void addRawBlock(BytesRefBlock block) { + BytesRef scratch = new BytesRef(); + for (int p = 0; p < block.getPositionCount(); p++) { + if (block.isNull(p)) { + continue; + } + int start = block.getFirstValueIndex(p); + int end = start + block.getValueCount(p); + for (int i = start; i < end; i++) { + SpatialExtentGeoPointSourceValuesAggregator.combine(state, block.getBytesRef(i, scratch)); + } + } + } + + private void addRawBlock(BytesRefBlock block, BooleanVector mask) { + BytesRef scratch = new BytesRef(); + for (int p = 0; p < block.getPositionCount(); p++) { + if (mask.getBoolean(p) == false) { + continue; + } + if (block.isNull(p)) { + continue; + } + int start = block.getFirstValueIndex(p); + int end = start + block.getValueCount(p); + for (int i = start; i < end; i++) { + SpatialExtentGeoPointSourceValuesAggregator.combine(state, block.getBytesRef(i, scratch)); + } + } + } + + @Override + public void addIntermediateInput(Page page) { + assert channels.size() == intermediateBlockCount(); + assert page.getBlockCount() >= channels.get(0) + intermediateStateDesc().size(); + Block minNegXUncast = page.getBlock(channels.get(0)); + if (minNegXUncast.areAllValuesNull()) { + return; + } + IntVector minNegX = ((IntBlock) minNegXUncast).asVector(); + assert minNegX.getPositionCount() == 1; + Block minPosXUncast = page.getBlock(channels.get(1)); + if (minPosXUncast.areAllValuesNull()) { + return; + } + IntVector minPosX = ((IntBlock) minPosXUncast).asVector(); + assert minPosX.getPositionCount() == 1; + Block maxNegXUncast = page.getBlock(channels.get(2)); + if (maxNegXUncast.areAllValuesNull()) { + return; + } + IntVector maxNegX = ((IntBlock) maxNegXUncast).asVector(); + assert maxNegX.getPositionCount() == 1; + Block maxPosXUncast = page.getBlock(channels.get(3)); + if (maxPosXUncast.areAllValuesNull()) { + return; + } + IntVector maxPosX = ((IntBlock) maxPosXUncast).asVector(); + assert maxPosX.getPositionCount() == 1; + Block maxYUncast = page.getBlock(channels.get(4)); + if (maxYUncast.areAllValuesNull()) { + return; + } + IntVector maxY = ((IntBlock) maxYUncast).asVector(); + assert maxY.getPositionCount() == 1; + Block minYUncast = page.getBlock(channels.get(5)); + if (minYUncast.areAllValuesNull()) { + return; + } + IntVector minY = ((IntBlock) minYUncast).asVector(); + assert minY.getPositionCount() == 1; + SpatialExtentGeoPointSourceValuesAggregator.combineIntermediate(state, minNegX.getInt(0), minPosX.getInt(0), maxNegX.getInt(0), maxPosX.getInt(0), maxY.getInt(0), minY.getInt(0)); + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, DriverContext driverContext) { + state.toIntermediate(blocks, offset, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, DriverContext driverContext) { + blocks[offset] = SpatialExtentGeoPointSourceValuesAggregator.evaluateFinal(state, driverContext); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesAggregatorFunctionSupplier.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesAggregatorFunctionSupplier.java new file mode 100644 index 0000000000000..1af20d72d08b0 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesAggregatorFunctionSupplier.java @@ -0,0 +1,41 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.util.List; +import org.elasticsearch.compute.aggregation.AggregatorFunctionSupplier; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link AggregatorFunctionSupplier} implementation for {@link SpatialExtentGeoPointSourceValuesAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentGeoPointSourceValuesAggregatorFunctionSupplier implements AggregatorFunctionSupplier { + private final List channels; + + public SpatialExtentGeoPointSourceValuesAggregatorFunctionSupplier(List channels) { + this.channels = channels; + } + + @Override + public SpatialExtentGeoPointSourceValuesAggregatorFunction aggregator( + DriverContext driverContext) { + return SpatialExtentGeoPointSourceValuesAggregatorFunction.create(driverContext, channels); + } + + @Override + public SpatialExtentGeoPointSourceValuesGroupingAggregatorFunction groupingAggregator( + DriverContext driverContext) { + return SpatialExtentGeoPointSourceValuesGroupingAggregatorFunction.create(channels, driverContext); + } + + @Override + public String describe() { + return "spatial_extent_geo_point_source of valuess"; + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesGroupingAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesGroupingAggregatorFunction.java new file mode 100644 index 0000000000000..bf8ab2554c7b7 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesGroupingAggregatorFunction.java @@ -0,0 +1,247 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.aggregation.GroupingAggregatorFunction; +import org.elasticsearch.compute.aggregation.IntermediateStateDesc; +import org.elasticsearch.compute.aggregation.SeenGroupIds; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link GroupingAggregatorFunction} implementation for {@link SpatialExtentGeoPointSourceValuesAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentGeoPointSourceValuesGroupingAggregatorFunction implements GroupingAggregatorFunction { + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("minNegX", ElementType.INT), + new IntermediateStateDesc("minPosX", ElementType.INT), + new IntermediateStateDesc("maxNegX", ElementType.INT), + new IntermediateStateDesc("maxPosX", ElementType.INT), + new IntermediateStateDesc("maxY", ElementType.INT), + new IntermediateStateDesc("minY", ElementType.INT) ); + + private final SpatialExtentGroupingStateWrappedLongitudeState state; + + private final List channels; + + private final DriverContext driverContext; + + public SpatialExtentGeoPointSourceValuesGroupingAggregatorFunction(List channels, + SpatialExtentGroupingStateWrappedLongitudeState state, DriverContext driverContext) { + this.channels = channels; + this.state = state; + this.driverContext = driverContext; + } + + public static SpatialExtentGeoPointSourceValuesGroupingAggregatorFunction create( + List channels, DriverContext driverContext) { + return new SpatialExtentGeoPointSourceValuesGroupingAggregatorFunction(channels, SpatialExtentGeoPointSourceValuesAggregator.initGrouping(), driverContext); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + @Override + public int intermediateBlockCount() { + return INTERMEDIATE_STATE_DESC.size(); + } + + @Override + public GroupingAggregatorFunction.AddInput prepareProcessPage(SeenGroupIds seenGroupIds, + Page page) { + BytesRefBlock valuesBlock = page.getBlock(channels.get(0)); + BytesRefVector valuesVector = valuesBlock.asVector(); + if (valuesVector == null) { + if (valuesBlock.mayHaveNulls()) { + state.enableGroupIdTracking(seenGroupIds); + } + return new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void close() { + } + }; + } + return new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesVector); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesVector); + } + + @Override + public void close() { + } + }; + } + + private void addRawInput(int positionOffset, IntVector groups, BytesRefBlock values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + if (values.isNull(groupPosition + positionOffset)) { + continue; + } + int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset); + int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset); + for (int v = valuesStart; v < valuesEnd; v++) { + SpatialExtentGeoPointSourceValuesAggregator.combine(state, groupId, values.getBytesRef(v, scratch)); + } + } + } + + private void addRawInput(int positionOffset, IntVector groups, BytesRefVector values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + SpatialExtentGeoPointSourceValuesAggregator.combine(state, groupId, values.getBytesRef(groupPosition + positionOffset, scratch)); + } + } + + private void addRawInput(int positionOffset, IntBlock groups, BytesRefBlock values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + int groupId = groups.getInt(g); + if (values.isNull(groupPosition + positionOffset)) { + continue; + } + int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset); + int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset); + for (int v = valuesStart; v < valuesEnd; v++) { + SpatialExtentGeoPointSourceValuesAggregator.combine(state, groupId, values.getBytesRef(v, scratch)); + } + } + } + } + + private void addRawInput(int positionOffset, IntBlock groups, BytesRefVector values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + int groupId = groups.getInt(g); + SpatialExtentGeoPointSourceValuesAggregator.combine(state, groupId, values.getBytesRef(groupPosition + positionOffset, scratch)); + } + } + } + + @Override + public void selectedMayContainUnseenGroups(SeenGroupIds seenGroupIds) { + state.enableGroupIdTracking(seenGroupIds); + } + + @Override + public void addIntermediateInput(int positionOffset, IntVector groups, Page page) { + state.enableGroupIdTracking(new SeenGroupIds.Empty()); + assert channels.size() == intermediateBlockCount(); + Block minNegXUncast = page.getBlock(channels.get(0)); + if (minNegXUncast.areAllValuesNull()) { + return; + } + IntVector minNegX = ((IntBlock) minNegXUncast).asVector(); + Block minPosXUncast = page.getBlock(channels.get(1)); + if (minPosXUncast.areAllValuesNull()) { + return; + } + IntVector minPosX = ((IntBlock) minPosXUncast).asVector(); + Block maxNegXUncast = page.getBlock(channels.get(2)); + if (maxNegXUncast.areAllValuesNull()) { + return; + } + IntVector maxNegX = ((IntBlock) maxNegXUncast).asVector(); + Block maxPosXUncast = page.getBlock(channels.get(3)); + if (maxPosXUncast.areAllValuesNull()) { + return; + } + IntVector maxPosX = ((IntBlock) maxPosXUncast).asVector(); + Block maxYUncast = page.getBlock(channels.get(4)); + if (maxYUncast.areAllValuesNull()) { + return; + } + IntVector maxY = ((IntBlock) maxYUncast).asVector(); + Block minYUncast = page.getBlock(channels.get(5)); + if (minYUncast.areAllValuesNull()) { + return; + } + IntVector minY = ((IntBlock) minYUncast).asVector(); + assert minNegX.getPositionCount() == minPosX.getPositionCount() && minNegX.getPositionCount() == maxNegX.getPositionCount() && minNegX.getPositionCount() == maxPosX.getPositionCount() && minNegX.getPositionCount() == maxY.getPositionCount() && minNegX.getPositionCount() == minY.getPositionCount(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + SpatialExtentGeoPointSourceValuesAggregator.combineIntermediate(state, groupId, minNegX.getInt(groupPosition + positionOffset), minPosX.getInt(groupPosition + positionOffset), maxNegX.getInt(groupPosition + positionOffset), maxPosX.getInt(groupPosition + positionOffset), maxY.getInt(groupPosition + positionOffset), minY.getInt(groupPosition + positionOffset)); + } + } + + @Override + public void addIntermediateRowInput(int groupId, GroupingAggregatorFunction input, int position) { + if (input.getClass() != getClass()) { + throw new IllegalArgumentException("expected " + getClass() + "; got " + input.getClass()); + } + SpatialExtentGroupingStateWrappedLongitudeState inState = ((SpatialExtentGeoPointSourceValuesGroupingAggregatorFunction) input).state; + state.enableGroupIdTracking(new SeenGroupIds.Empty()); + SpatialExtentGeoPointSourceValuesAggregator.combineStates(state, groupId, inState, position); + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, IntVector selected) { + state.toIntermediate(blocks, offset, selected, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, IntVector selected, + DriverContext driverContext) { + blocks[offset] = SpatialExtentGeoPointSourceValuesAggregator.evaluateFinal(state, selected, driverContext); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeAggregatorFunction.java new file mode 100644 index 0000000000000..abee9a1cee284 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeAggregatorFunction.java @@ -0,0 +1,206 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.aggregation.AggregatorFunction; +import org.elasticsearch.compute.aggregation.IntermediateStateDesc; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BooleanVector; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link AggregatorFunction} implementation for {@link SpatialExtentGeoShapeAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentGeoShapeAggregatorFunction implements AggregatorFunction { + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("minNegX", ElementType.INT), + new IntermediateStateDesc("minPosX", ElementType.INT), + new IntermediateStateDesc("maxNegX", ElementType.INT), + new IntermediateStateDesc("maxPosX", ElementType.INT), + new IntermediateStateDesc("maxY", ElementType.INT), + new IntermediateStateDesc("minY", ElementType.INT) ); + + private final DriverContext driverContext; + + private final SpatialExtentStateWrappedLongitudeState state; + + private final List channels; + + public SpatialExtentGeoShapeAggregatorFunction(DriverContext driverContext, + List channels, SpatialExtentStateWrappedLongitudeState state) { + this.driverContext = driverContext; + this.channels = channels; + this.state = state; + } + + public static SpatialExtentGeoShapeAggregatorFunction create(DriverContext driverContext, + List channels) { + return new SpatialExtentGeoShapeAggregatorFunction(driverContext, channels, SpatialExtentGeoShapeAggregator.initSingle()); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + @Override + public int intermediateBlockCount() { + return INTERMEDIATE_STATE_DESC.size(); + } + + @Override + public void addRawInput(Page page, BooleanVector mask) { + if (mask.allFalse()) { + // Entire page masked away + return; + } + if (mask.allTrue()) { + // No masking + BytesRefBlock block = page.getBlock(channels.get(0)); + BytesRefVector vector = block.asVector(); + if (vector != null) { + addRawVector(vector); + } else { + addRawBlock(block); + } + return; + } + // Some positions masked away, others kept + BytesRefBlock block = page.getBlock(channels.get(0)); + BytesRefVector vector = block.asVector(); + if (vector != null) { + addRawVector(vector, mask); + } else { + addRawBlock(block, mask); + } + } + + private void addRawVector(BytesRefVector vector) { + BytesRef scratch = new BytesRef(); + for (int i = 0; i < vector.getPositionCount(); i++) { + SpatialExtentGeoShapeAggregator.combine(state, vector.getBytesRef(i, scratch)); + } + } + + private void addRawVector(BytesRefVector vector, BooleanVector mask) { + BytesRef scratch = new BytesRef(); + for (int i = 0; i < vector.getPositionCount(); i++) { + if (mask.getBoolean(i) == false) { + continue; + } + SpatialExtentGeoShapeAggregator.combine(state, vector.getBytesRef(i, scratch)); + } + } + + private void addRawBlock(BytesRefBlock block) { + BytesRef scratch = new BytesRef(); + for (int p = 0; p < block.getPositionCount(); p++) { + if (block.isNull(p)) { + continue; + } + int start = block.getFirstValueIndex(p); + int end = start + block.getValueCount(p); + for (int i = start; i < end; i++) { + SpatialExtentGeoShapeAggregator.combine(state, block.getBytesRef(i, scratch)); + } + } + } + + private void addRawBlock(BytesRefBlock block, BooleanVector mask) { + BytesRef scratch = new BytesRef(); + for (int p = 0; p < block.getPositionCount(); p++) { + if (mask.getBoolean(p) == false) { + continue; + } + if (block.isNull(p)) { + continue; + } + int start = block.getFirstValueIndex(p); + int end = start + block.getValueCount(p); + for (int i = start; i < end; i++) { + SpatialExtentGeoShapeAggregator.combine(state, block.getBytesRef(i, scratch)); + } + } + } + + @Override + public void addIntermediateInput(Page page) { + assert channels.size() == intermediateBlockCount(); + assert page.getBlockCount() >= channels.get(0) + intermediateStateDesc().size(); + Block minNegXUncast = page.getBlock(channels.get(0)); + if (minNegXUncast.areAllValuesNull()) { + return; + } + IntVector minNegX = ((IntBlock) minNegXUncast).asVector(); + assert minNegX.getPositionCount() == 1; + Block minPosXUncast = page.getBlock(channels.get(1)); + if (minPosXUncast.areAllValuesNull()) { + return; + } + IntVector minPosX = ((IntBlock) minPosXUncast).asVector(); + assert minPosX.getPositionCount() == 1; + Block maxNegXUncast = page.getBlock(channels.get(2)); + if (maxNegXUncast.areAllValuesNull()) { + return; + } + IntVector maxNegX = ((IntBlock) maxNegXUncast).asVector(); + assert maxNegX.getPositionCount() == 1; + Block maxPosXUncast = page.getBlock(channels.get(3)); + if (maxPosXUncast.areAllValuesNull()) { + return; + } + IntVector maxPosX = ((IntBlock) maxPosXUncast).asVector(); + assert maxPosX.getPositionCount() == 1; + Block maxYUncast = page.getBlock(channels.get(4)); + if (maxYUncast.areAllValuesNull()) { + return; + } + IntVector maxY = ((IntBlock) maxYUncast).asVector(); + assert maxY.getPositionCount() == 1; + Block minYUncast = page.getBlock(channels.get(5)); + if (minYUncast.areAllValuesNull()) { + return; + } + IntVector minY = ((IntBlock) minYUncast).asVector(); + assert minY.getPositionCount() == 1; + SpatialExtentGeoShapeAggregator.combineIntermediate(state, minNegX.getInt(0), minPosX.getInt(0), maxNegX.getInt(0), maxPosX.getInt(0), maxY.getInt(0), minY.getInt(0)); + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, DriverContext driverContext) { + state.toIntermediate(blocks, offset, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, DriverContext driverContext) { + blocks[offset] = SpatialExtentGeoShapeAggregator.evaluateFinal(state, driverContext); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeAggregatorFunctionSupplier.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeAggregatorFunctionSupplier.java new file mode 100644 index 0000000000000..09f210c7085f8 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeAggregatorFunctionSupplier.java @@ -0,0 +1,40 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.util.List; +import org.elasticsearch.compute.aggregation.AggregatorFunctionSupplier; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link AggregatorFunctionSupplier} implementation for {@link SpatialExtentGeoShapeAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentGeoShapeAggregatorFunctionSupplier implements AggregatorFunctionSupplier { + private final List channels; + + public SpatialExtentGeoShapeAggregatorFunctionSupplier(List channels) { + this.channels = channels; + } + + @Override + public SpatialExtentGeoShapeAggregatorFunction aggregator(DriverContext driverContext) { + return SpatialExtentGeoShapeAggregatorFunction.create(driverContext, channels); + } + + @Override + public SpatialExtentGeoShapeGroupingAggregatorFunction groupingAggregator( + DriverContext driverContext) { + return SpatialExtentGeoShapeGroupingAggregatorFunction.create(channels, driverContext); + } + + @Override + public String describe() { + return "spatial_extent_geo of shapes"; + } +} diff --git a/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeGroupingAggregatorFunction.java b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeGroupingAggregatorFunction.java new file mode 100644 index 0000000000000..1200259ea6c41 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/generated/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeGroupingAggregatorFunction.java @@ -0,0 +1,247 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License +// 2.0; you may not use this file except in compliance with the Elastic License +// 2.0. +package org.elasticsearch.compute.aggregation.spatial; + +import java.lang.Integer; +import java.lang.Override; +import java.lang.String; +import java.lang.StringBuilder; +import java.util.List; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.aggregation.GroupingAggregatorFunction; +import org.elasticsearch.compute.aggregation.IntermediateStateDesc; +import org.elasticsearch.compute.aggregation.SeenGroupIds; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.BytesRefBlock; +import org.elasticsearch.compute.data.BytesRefVector; +import org.elasticsearch.compute.data.ElementType; +import org.elasticsearch.compute.data.IntBlock; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.data.Page; +import org.elasticsearch.compute.operator.DriverContext; + +/** + * {@link GroupingAggregatorFunction} implementation for {@link SpatialExtentGeoShapeAggregator}. + * This class is generated. Do not edit it. + */ +public final class SpatialExtentGeoShapeGroupingAggregatorFunction implements GroupingAggregatorFunction { + private static final List INTERMEDIATE_STATE_DESC = List.of( + new IntermediateStateDesc("minNegX", ElementType.INT), + new IntermediateStateDesc("minPosX", ElementType.INT), + new IntermediateStateDesc("maxNegX", ElementType.INT), + new IntermediateStateDesc("maxPosX", ElementType.INT), + new IntermediateStateDesc("maxY", ElementType.INT), + new IntermediateStateDesc("minY", ElementType.INT) ); + + private final SpatialExtentGroupingStateWrappedLongitudeState state; + + private final List channels; + + private final DriverContext driverContext; + + public SpatialExtentGeoShapeGroupingAggregatorFunction(List channels, + SpatialExtentGroupingStateWrappedLongitudeState state, DriverContext driverContext) { + this.channels = channels; + this.state = state; + this.driverContext = driverContext; + } + + public static SpatialExtentGeoShapeGroupingAggregatorFunction create(List channels, + DriverContext driverContext) { + return new SpatialExtentGeoShapeGroupingAggregatorFunction(channels, SpatialExtentGeoShapeAggregator.initGrouping(), driverContext); + } + + public static List intermediateStateDesc() { + return INTERMEDIATE_STATE_DESC; + } + + @Override + public int intermediateBlockCount() { + return INTERMEDIATE_STATE_DESC.size(); + } + + @Override + public GroupingAggregatorFunction.AddInput prepareProcessPage(SeenGroupIds seenGroupIds, + Page page) { + BytesRefBlock valuesBlock = page.getBlock(channels.get(0)); + BytesRefVector valuesVector = valuesBlock.asVector(); + if (valuesVector == null) { + if (valuesBlock.mayHaveNulls()) { + state.enableGroupIdTracking(seenGroupIds); + } + return new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesBlock); + } + + @Override + public void close() { + } + }; + } + return new GroupingAggregatorFunction.AddInput() { + @Override + public void add(int positionOffset, IntBlock groupIds) { + addRawInput(positionOffset, groupIds, valuesVector); + } + + @Override + public void add(int positionOffset, IntVector groupIds) { + addRawInput(positionOffset, groupIds, valuesVector); + } + + @Override + public void close() { + } + }; + } + + private void addRawInput(int positionOffset, IntVector groups, BytesRefBlock values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + if (values.isNull(groupPosition + positionOffset)) { + continue; + } + int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset); + int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset); + for (int v = valuesStart; v < valuesEnd; v++) { + SpatialExtentGeoShapeAggregator.combine(state, groupId, values.getBytesRef(v, scratch)); + } + } + } + + private void addRawInput(int positionOffset, IntVector groups, BytesRefVector values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + SpatialExtentGeoShapeAggregator.combine(state, groupId, values.getBytesRef(groupPosition + positionOffset, scratch)); + } + } + + private void addRawInput(int positionOffset, IntBlock groups, BytesRefBlock values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + int groupId = groups.getInt(g); + if (values.isNull(groupPosition + positionOffset)) { + continue; + } + int valuesStart = values.getFirstValueIndex(groupPosition + positionOffset); + int valuesEnd = valuesStart + values.getValueCount(groupPosition + positionOffset); + for (int v = valuesStart; v < valuesEnd; v++) { + SpatialExtentGeoShapeAggregator.combine(state, groupId, values.getBytesRef(v, scratch)); + } + } + } + } + + private void addRawInput(int positionOffset, IntBlock groups, BytesRefVector values) { + BytesRef scratch = new BytesRef(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + if (groups.isNull(groupPosition)) { + continue; + } + int groupStart = groups.getFirstValueIndex(groupPosition); + int groupEnd = groupStart + groups.getValueCount(groupPosition); + for (int g = groupStart; g < groupEnd; g++) { + int groupId = groups.getInt(g); + SpatialExtentGeoShapeAggregator.combine(state, groupId, values.getBytesRef(groupPosition + positionOffset, scratch)); + } + } + } + + @Override + public void selectedMayContainUnseenGroups(SeenGroupIds seenGroupIds) { + state.enableGroupIdTracking(seenGroupIds); + } + + @Override + public void addIntermediateInput(int positionOffset, IntVector groups, Page page) { + state.enableGroupIdTracking(new SeenGroupIds.Empty()); + assert channels.size() == intermediateBlockCount(); + Block minNegXUncast = page.getBlock(channels.get(0)); + if (minNegXUncast.areAllValuesNull()) { + return; + } + IntVector minNegX = ((IntBlock) minNegXUncast).asVector(); + Block minPosXUncast = page.getBlock(channels.get(1)); + if (minPosXUncast.areAllValuesNull()) { + return; + } + IntVector minPosX = ((IntBlock) minPosXUncast).asVector(); + Block maxNegXUncast = page.getBlock(channels.get(2)); + if (maxNegXUncast.areAllValuesNull()) { + return; + } + IntVector maxNegX = ((IntBlock) maxNegXUncast).asVector(); + Block maxPosXUncast = page.getBlock(channels.get(3)); + if (maxPosXUncast.areAllValuesNull()) { + return; + } + IntVector maxPosX = ((IntBlock) maxPosXUncast).asVector(); + Block maxYUncast = page.getBlock(channels.get(4)); + if (maxYUncast.areAllValuesNull()) { + return; + } + IntVector maxY = ((IntBlock) maxYUncast).asVector(); + Block minYUncast = page.getBlock(channels.get(5)); + if (minYUncast.areAllValuesNull()) { + return; + } + IntVector minY = ((IntBlock) minYUncast).asVector(); + assert minNegX.getPositionCount() == minPosX.getPositionCount() && minNegX.getPositionCount() == maxNegX.getPositionCount() && minNegX.getPositionCount() == maxPosX.getPositionCount() && minNegX.getPositionCount() == maxY.getPositionCount() && minNegX.getPositionCount() == minY.getPositionCount(); + for (int groupPosition = 0; groupPosition < groups.getPositionCount(); groupPosition++) { + int groupId = groups.getInt(groupPosition); + SpatialExtentGeoShapeAggregator.combineIntermediate(state, groupId, minNegX.getInt(groupPosition + positionOffset), minPosX.getInt(groupPosition + positionOffset), maxNegX.getInt(groupPosition + positionOffset), maxPosX.getInt(groupPosition + positionOffset), maxY.getInt(groupPosition + positionOffset), minY.getInt(groupPosition + positionOffset)); + } + } + + @Override + public void addIntermediateRowInput(int groupId, GroupingAggregatorFunction input, int position) { + if (input.getClass() != getClass()) { + throw new IllegalArgumentException("expected " + getClass() + "; got " + input.getClass()); + } + SpatialExtentGroupingStateWrappedLongitudeState inState = ((SpatialExtentGeoShapeGroupingAggregatorFunction) input).state; + state.enableGroupIdTracking(new SeenGroupIds.Empty()); + SpatialExtentGeoShapeAggregator.combineStates(state, groupId, inState, position); + } + + @Override + public void evaluateIntermediate(Block[] blocks, int offset, IntVector selected) { + state.toIntermediate(blocks, offset, selected, driverContext); + } + + @Override + public void evaluateFinal(Block[] blocks, int offset, IntVector selected, + DriverContext driverContext) { + blocks[offset] = SpatialExtentGeoShapeAggregator.evaluateFinal(state, selected, driverContext); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getSimpleName()).append("["); + sb.append("channels=").append(channels); + sb.append("]"); + return sb.toString(); + } + + @Override + public void close() { + state.close(); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/AbstractArrayState.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/AbstractArrayState.java index 45a45f4337beb..5fa1394e8cf96 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/AbstractArrayState.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/AbstractArrayState.java @@ -28,7 +28,7 @@ public AbstractArrayState(BigArrays bigArrays) { this.bigArrays = bigArrays; } - boolean hasValue(int groupId) { + public boolean hasValue(int groupId) { return seen == null || seen.get(groupId); } @@ -37,7 +37,7 @@ boolean hasValue(int groupId) { * idempotent and fast if already tracking so it's safe to, say, call it once * for every block of values that arrives containing {@code null}. */ - final void enableGroupIdTracking(SeenGroupIds seenGroupIds) { + public final void enableGroupIdTracking(SeenGroupIds seenGroupIds) { if (seen == null) { seen = seenGroupIds.seenGroupIds(bigArrays); } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/CentroidPointAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/CentroidPointAggregator.java index c66c960dd8a99..47d927fda91b5 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/CentroidPointAggregator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/CentroidPointAggregator.java @@ -32,6 +32,13 @@ * This requires that the planner has planned that points are loaded from the index as doc-values. */ abstract class CentroidPointAggregator { + public static CentroidState initSingle() { + return new CentroidState(); + } + + public static GroupingCentroidState initGrouping(BigArrays bigArrays) { + return new GroupingCentroidState(bigArrays); + } public static void combine(CentroidState current, double xVal, double xDel, double yVal, double yDel, long count) { current.add(xVal, xDel, yVal, yDel, count); diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/GeoPointEnvelopeVisitor.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/GeoPointEnvelopeVisitor.java new file mode 100644 index 0000000000000..6bdd028f3d6ee --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/GeoPointEnvelopeVisitor.java @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; + +class GeoPointEnvelopeVisitor extends SpatialEnvelopeVisitor.GeoPointVisitor { + GeoPointEnvelopeVisitor() { + super(WrapLongitude.WRAP); + } + + void reset() { + minY = Double.POSITIVE_INFINITY; + maxY = Double.NEGATIVE_INFINITY; + minNegX = Double.POSITIVE_INFINITY; + maxNegX = Double.NEGATIVE_INFINITY; + minPosX = Double.POSITIVE_INFINITY; + maxPosX = Double.NEGATIVE_INFINITY; + } + + double getMinNegX() { + return minNegX; + } + + double getMinPosX() { + return minPosX; + } + + double getMaxNegX() { + return maxNegX; + } + + double getMaxPosX() { + return maxPosX; + } + + double getMaxY() { + return maxY; + } + + double getMinY() { + return minY; + } + + static Rectangle asRectangle( + double minNegX, + double minPosX, + double maxNegX, + double maxPosX, + double maxY, + double minY, + WrapLongitude wrapLongitude + ) { + return SpatialEnvelopeVisitor.GeoPointVisitor.getResult(minNegX, minPosX, maxNegX, maxPosX, maxY, minY, wrapLongitude); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/PointType.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/PointType.java new file mode 100644 index 0000000000000..5395ca0b85163 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/PointType.java @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.XYEncodingUtils; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; + +import java.util.Optional; + +public enum PointType { + GEO { + @Override + public Optional computeEnvelope(Geometry geo) { + return SpatialEnvelopeVisitor.visitGeo(geo, WrapLongitude.WRAP); + } + + @Override + public double decodeX(int encoded) { + return GeoEncodingUtils.decodeLongitude(encoded); + } + + @Override + public double decodeY(int encoded) { + return GeoEncodingUtils.decodeLatitude(encoded); + } + + @Override + public int encodeX(double decoded) { + return GeoEncodingUtils.encodeLongitude(decoded); + } + + @Override + public int encodeY(double decoded) { + return GeoEncodingUtils.encodeLatitude(decoded); + } + + // Geo encodes the longitude in the lower 32 bits and the latitude in the upper 32 bits. + @Override + public int extractX(long encoded) { + return SpatialAggregationUtils.extractSecond(encoded); + } + + @Override + public int extractY(long encoded) { + return SpatialAggregationUtils.extractFirst(encoded); + } + }, + CARTESIAN { + @Override + public Optional computeEnvelope(Geometry geo) { + return SpatialEnvelopeVisitor.visitCartesian(geo); + } + + @Override + public double decodeX(int encoded) { + return XYEncodingUtils.decode(encoded); + } + + @Override + public double decodeY(int encoded) { + return XYEncodingUtils.decode(encoded); + } + + @Override + public int encodeX(double decoded) { + return XYEncodingUtils.encode((float) decoded); + } + + @Override + public int encodeY(double decoded) { + return XYEncodingUtils.encode((float) decoded); + } + + @Override + public int extractX(long encoded) { + return SpatialAggregationUtils.extractFirst(encoded); + } + + @Override + public int extractY(long encoded) { + return SpatialAggregationUtils.extractSecond(encoded); + } + }; + + public abstract Optional computeEnvelope(Geometry geo); + + public abstract double decodeX(int encoded); + + public abstract double decodeY(int encoded); + + public abstract int encodeX(double decoded); + + public abstract int encodeY(double decoded); + + public abstract int extractX(long encoded); + + public abstract int extractY(long encoded); +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialAggregationUtils.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialAggregationUtils.java new file mode 100644 index 0000000000000..6b29b20601dae --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialAggregationUtils.java @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.apache.lucene.geo.GeoEncodingUtils; +import org.apache.lucene.geo.XYEncodingUtils; +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.utils.GeometryValidator; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; +import org.elasticsearch.geometry.utils.WellKnownBinary; + +class SpatialAggregationUtils { + private SpatialAggregationUtils() { /* Utility class */ } + + public static Geometry decode(BytesRef wkb) { + return WellKnownBinary.fromWKB(GeometryValidator.NOOP, false /* coerce */, wkb.bytes, wkb.offset, wkb.length); + } + + public static Point decodePoint(BytesRef wkb) { + return (Point) decode(wkb); + } + + public static double decodeX(long encoded) { + return XYEncodingUtils.decode(extractFirst(encoded)); + } + + public static int extractFirst(long encoded) { + return (int) (encoded >>> 32); + } + + public static double decodeY(long encoded) { + return XYEncodingUtils.decode(extractSecond(encoded)); + } + + public static int extractSecond(long encoded) { + return (int) (encoded & 0xFFFFFFFFL); + } + + public static double decodeLongitude(long encoded) { + return GeoEncodingUtils.decodeLongitude((int) (encoded & 0xFFFFFFFFL)); + } + + public static double decodeLatitude(long encoded) { + return GeoEncodingUtils.decodeLatitude((int) (encoded >>> 32)); + } + + public static int encodeNegativeLongitude(double d) { + return Double.isFinite(d) ? GeoEncodingUtils.encodeLongitude(d) : DEFAULT_NEG; + } + + public static int encodePositiveLongitude(double d) { + return Double.isFinite(d) ? GeoEncodingUtils.encodeLongitude(d) : DEFAULT_POS; + } + + public static Rectangle asRectangle(int minNegX, int minPosX, int maxNegX, int maxPosX, int maxY, int minY) { + assert minNegX <= 0 == maxNegX <= 0; + assert minPosX >= 0 == maxPosX >= 0; + return GeoPointEnvelopeVisitor.asRectangle( + minNegX <= 0 ? decodeLongitude(minNegX) : Double.POSITIVE_INFINITY, + minPosX >= 0 ? decodeLongitude(minPosX) : Double.POSITIVE_INFINITY, + maxNegX <= 0 ? decodeLongitude(maxNegX) : Double.NEGATIVE_INFINITY, + maxPosX >= 0 ? decodeLongitude(maxPosX) : Double.NEGATIVE_INFINITY, + GeoEncodingUtils.decodeLatitude(maxY), + GeoEncodingUtils.decodeLatitude(minY), + WrapLongitude.WRAP + ); + } + + public static int maxNeg(int a, int b) { + return a <= 0 && b <= 0 ? Math.max(a, b) : Math.min(a, b); + } + + public static int minPos(int a, int b) { + return a >= 0 && b >= 0 ? Math.min(a, b) : Math.max(a, b); + } + + // The default values are intentionally non-negative/non-positive, so we can mark unassigned values. + public static final int DEFAULT_POS = -1; + public static final int DEFAULT_NEG = 1; +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidCartesianPointDocValuesAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidCartesianPointDocValuesAggregator.java index 0bafb6f8112de..891c22b71c7e9 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidCartesianPointDocValuesAggregator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidCartesianPointDocValuesAggregator.java @@ -7,12 +7,13 @@ package org.elasticsearch.compute.aggregation.spatial; -import org.apache.lucene.geo.XYEncodingUtils; -import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.compute.ann.Aggregator; import org.elasticsearch.compute.ann.GroupingAggregator; import org.elasticsearch.compute.ann.IntermediateState; +import static org.elasticsearch.compute.aggregation.spatial.SpatialAggregationUtils.decodeX; +import static org.elasticsearch.compute.aggregation.spatial.SpatialAggregationUtils.decodeY; + /** * This aggregator calculates the centroid of a set of cartesian points. * It is assumes that the cartesian points are encoded as longs. @@ -28,15 +29,6 @@ ) @GroupingAggregator class SpatialCentroidCartesianPointDocValuesAggregator extends CentroidPointAggregator { - - public static CentroidState initSingle() { - return new CentroidState(); - } - - public static GroupingCentroidState initGrouping(BigArrays bigArrays) { - return new GroupingCentroidState(bigArrays); - } - public static void combine(CentroidState current, long v) { current.add(decodeX(v), decodeY(v)); } @@ -44,12 +36,4 @@ public static void combine(CentroidState current, long v) { public static void combine(GroupingCentroidState current, int groupId, long encoded) { current.add(decodeX(encoded), 0d, decodeY(encoded), 0d, 1, groupId); } - - private static double decodeX(long encoded) { - return XYEncodingUtils.decode((int) (encoded >>> 32)); - } - - private static double decodeY(long encoded) { - return XYEncodingUtils.decode((int) (encoded & 0xFFFFFFFFL)); - } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidCartesianPointSourceValuesAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidCartesianPointSourceValuesAggregator.java index 5673892be4bf0..700721e3ea9d4 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidCartesianPointSourceValuesAggregator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidCartesianPointSourceValuesAggregator.java @@ -8,13 +8,10 @@ package org.elasticsearch.compute.aggregation.spatial; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.compute.ann.Aggregator; import org.elasticsearch.compute.ann.GroupingAggregator; import org.elasticsearch.compute.ann.IntermediateState; import org.elasticsearch.geometry.Point; -import org.elasticsearch.geometry.utils.GeometryValidator; -import org.elasticsearch.geometry.utils.WellKnownBinary; /** * This aggregator calculates the centroid of a set of cartesian points. @@ -33,26 +30,13 @@ ) @GroupingAggregator class SpatialCentroidCartesianPointSourceValuesAggregator extends CentroidPointAggregator { - - public static CentroidState initSingle() { - return new CentroidState(); - } - - public static GroupingCentroidState initGrouping(BigArrays bigArrays) { - return new GroupingCentroidState(bigArrays); - } - public static void combine(CentroidState current, BytesRef wkb) { - Point point = decode(wkb); + Point point = SpatialAggregationUtils.decodePoint(wkb); current.add(point.getX(), point.getY()); } public static void combine(GroupingCentroidState current, int groupId, BytesRef wkb) { - Point point = decode(wkb); + Point point = SpatialAggregationUtils.decodePoint(wkb); current.add(point.getX(), 0d, point.getY(), 0d, 1, groupId); } - - private static Point decode(BytesRef wkb) { - return (Point) WellKnownBinary.fromWKB(GeometryValidator.NOOP, false, wkb.bytes, wkb.offset, wkb.length); - } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidGeoPointDocValuesAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidGeoPointDocValuesAggregator.java index ee5ab0e292547..431e25a03779e 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidGeoPointDocValuesAggregator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidGeoPointDocValuesAggregator.java @@ -7,12 +7,13 @@ package org.elasticsearch.compute.aggregation.spatial; -import org.apache.lucene.geo.GeoEncodingUtils; -import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.compute.ann.Aggregator; import org.elasticsearch.compute.ann.GroupingAggregator; import org.elasticsearch.compute.ann.IntermediateState; +import static org.elasticsearch.compute.aggregation.spatial.SpatialAggregationUtils.decodeLatitude; +import static org.elasticsearch.compute.aggregation.spatial.SpatialAggregationUtils.decodeLongitude; + /** * This aggregator calculates the centroid of a set of geo points. It is assumes that the geo points are encoded as longs. * This requires that the planner has planned that points are loaded from the index as doc-values. @@ -27,28 +28,11 @@ ) @GroupingAggregator class SpatialCentroidGeoPointDocValuesAggregator extends CentroidPointAggregator { - - public static CentroidState initSingle() { - return new CentroidState(); - } - - public static GroupingCentroidState initGrouping(BigArrays bigArrays) { - return new GroupingCentroidState(bigArrays); - } - public static void combine(CentroidState current, long v) { - current.add(decodeX(v), decodeY(v)); + current.add(decodeLongitude(v), decodeLatitude(v)); } public static void combine(GroupingCentroidState current, int groupId, long encoded) { - current.add(decodeX(encoded), 0d, decodeY(encoded), 0d, 1, groupId); - } - - private static double decodeX(long encoded) { - return GeoEncodingUtils.decodeLongitude((int) (encoded & 0xFFFFFFFFL)); - } - - private static double decodeY(long encoded) { - return GeoEncodingUtils.decodeLatitude((int) (encoded >>> 32)); + current.add(decodeLongitude(encoded), 0d, decodeLatitude(encoded), 0d, 1, groupId); } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidGeoPointSourceValuesAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidGeoPointSourceValuesAggregator.java index caf55dcc2f4e1..90563b33b8abb 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidGeoPointSourceValuesAggregator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialCentroidGeoPointSourceValuesAggregator.java @@ -8,13 +8,10 @@ package org.elasticsearch.compute.aggregation.spatial; import org.apache.lucene.util.BytesRef; -import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.compute.ann.Aggregator; import org.elasticsearch.compute.ann.GroupingAggregator; import org.elasticsearch.compute.ann.IntermediateState; import org.elasticsearch.geometry.Point; -import org.elasticsearch.geometry.utils.GeometryValidator; -import org.elasticsearch.geometry.utils.WellKnownBinary; /** * This aggregator calculates the centroid of a set of geo points. @@ -33,26 +30,13 @@ ) @GroupingAggregator class SpatialCentroidGeoPointSourceValuesAggregator extends CentroidPointAggregator { - - public static CentroidState initSingle() { - return new CentroidState(); - } - - public static GroupingCentroidState initGrouping(BigArrays bigArrays) { - return new GroupingCentroidState(bigArrays); - } - public static void combine(CentroidState current, BytesRef wkb) { - Point point = decode(wkb); + Point point = SpatialAggregationUtils.decodePoint(wkb); current.add(point.getX(), point.getY()); } public static void combine(GroupingCentroidState current, int groupId, BytesRef wkb) { - Point point = decode(wkb); + Point point = SpatialAggregationUtils.decodePoint(wkb); current.add(point.getX(), 0d, point.getY(), 0d, 1, groupId); } - - private static Point decode(BytesRef wkb) { - return (Point) WellKnownBinary.fromWKB(GeometryValidator.NOOP, false, wkb.bytes, wkb.offset, wkb.length); - } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentAggregator.java new file mode 100644 index 0000000000000..91e0f098d795e --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentAggregator.java @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.operator.DriverContext; + +// A bit of abuse of notation here, since we're extending this class to "inherit" its static methods. +// Unfortunately, this is the way it has to be done, since the generated code invokes these methods statically. +abstract class SpatialExtentAggregator { + public static void combineIntermediate(SpatialExtentState current, int minX, int maxX, int maxY, int minY) { + current.add(minX, maxX, maxY, minY); + } + + public static void combineIntermediate(SpatialExtentGroupingState current, int groupId, int minX, int maxX, int maxY, int minY) { + current.add(groupId, minX, maxX, maxY, minY); + } + + public static Block evaluateFinal(SpatialExtentState state, DriverContext driverContext) { + return state.toBlock(driverContext); + } + + public static Block evaluateFinal(SpatialExtentGroupingState state, IntVector selected, DriverContext driverContext) { + return state.toBlock(selected, driverContext); + } + + public static void combineStates(SpatialExtentGroupingState current, int groupId, SpatialExtentGroupingState inState, int inPosition) { + current.add(groupId, inState, inPosition); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesAggregator.java new file mode 100644 index 0000000000000..f64949b77707c --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointDocValuesAggregator.java @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.elasticsearch.compute.ann.Aggregator; +import org.elasticsearch.compute.ann.GroupingAggregator; +import org.elasticsearch.compute.ann.IntermediateState; + +/** + * Computes the extent of a set of cartesian points. It is assumed the points are encoded as longs. + * This requires that the planner has planned that points are loaded from the index as doc-values. + */ +@Aggregator( + { + @IntermediateState(name = "minX", type = "INT"), + @IntermediateState(name = "maxX", type = "INT"), + @IntermediateState(name = "maxY", type = "INT"), + @IntermediateState(name = "minY", type = "INT") } +) +@GroupingAggregator +class SpatialExtentCartesianPointDocValuesAggregator extends SpatialExtentAggregator { + public static SpatialExtentState initSingle() { + return new SpatialExtentState(PointType.CARTESIAN); + } + + public static SpatialExtentGroupingState initGrouping() { + return new SpatialExtentGroupingState(PointType.CARTESIAN); + } + + public static void combine(SpatialExtentState current, long v) { + current.add(v); + } + + public static void combine(SpatialExtentGroupingState current, int groupId, long v) { + current.add(groupId, v); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesAggregator.java new file mode 100644 index 0000000000000..3488af4525dcb --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianPointSourceValuesAggregator.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.ann.Aggregator; +import org.elasticsearch.compute.ann.GroupingAggregator; +import org.elasticsearch.compute.ann.IntermediateState; + +/** + * Computes the extent of a set of cartesian points. It is assumed that the cartesian points are encoded as WKB BytesRef. + * This requires that the planner has NOT planned that points are loaded from the index as doc-values, but from source instead. + * This is also used for final aggregations and aggregations in the coordinator node, + * even if the local node partial aggregation is done with {@link SpatialExtentCartesianPointDocValuesAggregator}. + */ +@Aggregator( + { + @IntermediateState(name = "minX", type = "INT"), + @IntermediateState(name = "maxX", type = "INT"), + @IntermediateState(name = "maxY", type = "INT"), + @IntermediateState(name = "minY", type = "INT") } +) +@GroupingAggregator +class SpatialExtentCartesianPointSourceValuesAggregator extends SpatialExtentAggregator { + public static SpatialExtentState initSingle() { + return new SpatialExtentState(PointType.CARTESIAN); + } + + public static SpatialExtentGroupingState initGrouping() { + return new SpatialExtentGroupingState(PointType.CARTESIAN); + } + + public static void combine(SpatialExtentState current, BytesRef bytes) { + current.add(SpatialAggregationUtils.decode(bytes)); + } + + public static void combine(SpatialExtentGroupingState current, int groupId, BytesRef bytes) { + current.add(groupId, SpatialAggregationUtils.decode(bytes)); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeAggregator.java new file mode 100644 index 0000000000000..6d50d27aa5a2d --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentCartesianShapeAggregator.java @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.ann.Aggregator; +import org.elasticsearch.compute.ann.GroupingAggregator; +import org.elasticsearch.compute.ann.IntermediateState; + +/** + * Computes the extent of a set of cartesian shapes. It is assumed that the cartesian shapes are encoded as WKB BytesRef. + * We do not currently support reading shape values or extents from doc values. + */ +@Aggregator( + { + @IntermediateState(name = "minX", type = "INT"), + @IntermediateState(name = "maxX", type = "INT"), + @IntermediateState(name = "maxY", type = "INT"), + @IntermediateState(name = "minY", type = "INT") } +) +@GroupingAggregator +class SpatialExtentCartesianShapeAggregator extends SpatialExtentAggregator { + public static SpatialExtentState initSingle() { + return new SpatialExtentState(PointType.CARTESIAN); + } + + public static SpatialExtentGroupingState initGrouping() { + return new SpatialExtentGroupingState(PointType.CARTESIAN); + } + + public static void combine(SpatialExtentState current, BytesRef bytes) { + current.add(SpatialAggregationUtils.decode(bytes)); + } + + public static void combine(SpatialExtentGroupingState current, int groupId, BytesRef bytes) { + current.add(groupId, SpatialAggregationUtils.decode(bytes)); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesAggregator.java new file mode 100644 index 0000000000000..b9b8bf65e116b --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointDocValuesAggregator.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.elasticsearch.compute.ann.Aggregator; +import org.elasticsearch.compute.ann.GroupingAggregator; +import org.elasticsearch.compute.ann.IntermediateState; + +/** + * Computes the extent of a set of geo points. It is assumed the points are encoded as longs. + * This requires that the planner has planned that points are loaded from the index as doc-values. + */ +@Aggregator( + { + @IntermediateState(name = "minNegX", type = "INT"), + @IntermediateState(name = "minPosX", type = "INT"), + @IntermediateState(name = "maxNegX", type = "INT"), + @IntermediateState(name = "maxPosX", type = "INT"), + @IntermediateState(name = "maxY", type = "INT"), + @IntermediateState(name = "minY", type = "INT") } +) +@GroupingAggregator +class SpatialExtentGeoPointDocValuesAggregator extends SpatialExtentLongitudeWrappingAggregator { + // TODO support non-longitude wrapped geo shapes. + public static SpatialExtentStateWrappedLongitudeState initSingle() { + return new SpatialExtentStateWrappedLongitudeState(); + } + + public static SpatialExtentGroupingStateWrappedLongitudeState initGrouping() { + return new SpatialExtentGroupingStateWrappedLongitudeState(); + } + + public static void combine(SpatialExtentStateWrappedLongitudeState current, long encoded) { + current.add(encoded); + } + + public static void combine(SpatialExtentGroupingStateWrappedLongitudeState current, int groupId, long encoded) { + current.add(groupId, encoded); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesAggregator.java new file mode 100644 index 0000000000000..36a4e359f23fc --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoPointSourceValuesAggregator.java @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.ann.Aggregator; +import org.elasticsearch.compute.ann.GroupingAggregator; +import org.elasticsearch.compute.ann.IntermediateState; + +/** + * Computes the extent of a set of geo points. It is assumed that the geo points are encoded as WKB BytesRef. + * This requires that the planner has NOT planned that points are loaded from the index as doc-values, but from source instead. + * This is also used for final aggregations and aggregations in the coordinator node, + * even if the local node partial aggregation is done with {@link SpatialExtentGeoPointDocValuesAggregator}. + */ +@Aggregator( + { + @IntermediateState(name = "minNegX", type = "INT"), + @IntermediateState(name = "minPosX", type = "INT"), + @IntermediateState(name = "maxNegX", type = "INT"), + @IntermediateState(name = "maxPosX", type = "INT"), + @IntermediateState(name = "maxY", type = "INT"), + @IntermediateState(name = "minY", type = "INT") } +) +@GroupingAggregator +class SpatialExtentGeoPointSourceValuesAggregator extends SpatialExtentLongitudeWrappingAggregator { + // TODO support non-longitude wrapped geo shapes. + public static SpatialExtentStateWrappedLongitudeState initSingle() { + return new SpatialExtentStateWrappedLongitudeState(); + } + + public static SpatialExtentGroupingStateWrappedLongitudeState initGrouping() { + return new SpatialExtentGroupingStateWrappedLongitudeState(); + } + + public static void combine(SpatialExtentStateWrappedLongitudeState current, BytesRef bytes) { + current.add(SpatialAggregationUtils.decode(bytes)); + } + + public static void combine(SpatialExtentGroupingStateWrappedLongitudeState current, int groupId, BytesRef bytes) { + current.add(groupId, SpatialAggregationUtils.decode(bytes)); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeAggregator.java new file mode 100644 index 0000000000000..3d1b9b6300c9d --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGeoShapeAggregator.java @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.ann.Aggregator; +import org.elasticsearch.compute.ann.GroupingAggregator; +import org.elasticsearch.compute.ann.IntermediateState; + +/** + * Computes the extent of a set of geo shapes. It is assumed that the geo shapes are encoded as WKB BytesRef. + * We do not currently support reading shape values or extents from doc values. + */ +@Aggregator( + { + @IntermediateState(name = "minNegX", type = "INT"), + @IntermediateState(name = "minPosX", type = "INT"), + @IntermediateState(name = "maxNegX", type = "INT"), + @IntermediateState(name = "maxPosX", type = "INT"), + @IntermediateState(name = "maxY", type = "INT"), + @IntermediateState(name = "minY", type = "INT") } +) +@GroupingAggregator +class SpatialExtentGeoShapeAggregator extends SpatialExtentLongitudeWrappingAggregator { + // TODO support non-longitude wrapped geo shapes. + public static SpatialExtentStateWrappedLongitudeState initSingle() { + return new SpatialExtentStateWrappedLongitudeState(); + } + + public static SpatialExtentGroupingStateWrappedLongitudeState initGrouping() { + return new SpatialExtentGroupingStateWrappedLongitudeState(); + } + + public static void combine(SpatialExtentStateWrappedLongitudeState current, BytesRef bytes) { + current.add(SpatialAggregationUtils.decode(bytes)); + } + + public static void combine(SpatialExtentGroupingStateWrappedLongitudeState current, int groupId, BytesRef bytes) { + current.add(groupId, SpatialAggregationUtils.decode(bytes)); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGroupingState.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGroupingState.java new file mode 100644 index 0000000000000..9ce0ccdda0ff5 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGroupingState.java @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.IntArray; +import org.elasticsearch.compute.aggregation.AbstractArrayState; +import org.elasticsearch.compute.aggregation.SeenGroupIds; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.utils.WellKnownBinary; + +import java.nio.ByteOrder; + +final class SpatialExtentGroupingState extends AbstractArrayState { + private final PointType pointType; + private IntArray minXs; + private IntArray maxXs; + private IntArray maxYs; + private IntArray minYs; + + SpatialExtentGroupingState(PointType pointType) { + this(pointType, BigArrays.NON_RECYCLING_INSTANCE); + } + + SpatialExtentGroupingState(PointType pointType, BigArrays bigArrays) { + super(bigArrays); + this.pointType = pointType; + this.minXs = bigArrays.newIntArray(0, false); + this.maxXs = bigArrays.newIntArray(0, false); + this.maxYs = bigArrays.newIntArray(0, false); + this.minYs = bigArrays.newIntArray(0, false); + enableGroupIdTracking(new SeenGroupIds.Empty()); + } + + @Override + public void toIntermediate(Block[] blocks, int offset, IntVector selected, DriverContext driverContext) { + assert blocks.length >= offset; + try ( + var minXsBuilder = driverContext.blockFactory().newIntBlockBuilder(selected.getPositionCount()); + var maxXsBuilder = driverContext.blockFactory().newIntBlockBuilder(selected.getPositionCount()); + var maxYsBuilder = driverContext.blockFactory().newIntBlockBuilder(selected.getPositionCount()); + var minYsBuilder = driverContext.blockFactory().newIntBlockBuilder(selected.getPositionCount()); + ) { + for (int i = 0; i < selected.getPositionCount(); i++) { + int group = selected.getInt(i); + assert hasValue(group); + minXsBuilder.appendInt(minXs.get(group)); + maxXsBuilder.appendInt(maxXs.get(group)); + maxYsBuilder.appendInt(maxYs.get(group)); + minYsBuilder.appendInt(minYs.get(group)); + } + blocks[offset + 0] = minXsBuilder.build(); + blocks[offset + 1] = maxXsBuilder.build(); + blocks[offset + 2] = maxYsBuilder.build(); + blocks[offset + 3] = minYsBuilder.build(); + } + } + + public void add(int groupId, Geometry geometry) { + ensureCapacity(groupId); + pointType.computeEnvelope(geometry) + .ifPresent( + r -> add( + groupId, + pointType.encodeX(r.getMinX()), + pointType.encodeX(r.getMaxX()), + pointType.encodeY(r.getMaxY()), + pointType.encodeY(r.getMinY()) + ) + ); + } + + public void add(int groupId, long encoded) { + int x = pointType.extractX(encoded); + int y = pointType.extractY(encoded); + add(groupId, x, x, y, y); + } + + public void add(int groupId, int minX, int maxX, int maxY, int minY) { + ensureCapacity(groupId); + if (hasValue(groupId)) { + minXs.set(groupId, Math.min(minXs.get(groupId), minX)); + maxXs.set(groupId, Math.max(maxXs.get(groupId), maxX)); + maxYs.set(groupId, Math.max(maxYs.get(groupId), maxY)); + minYs.set(groupId, Math.min(minYs.get(groupId), minY)); + } else { + minXs.set(groupId, minX); + maxXs.set(groupId, maxX); + maxYs.set(groupId, maxY); + minYs.set(groupId, minY); + } + trackGroupId(groupId); + } + + private void ensureCapacity(int groupId) { + long requiredSize = groupId + 1; + if (minXs.size() < requiredSize) { + assert minXs.size() == maxXs.size() && minXs.size() == maxYs.size() && minXs.size() == minYs.size(); + minXs = bigArrays.grow(minXs, requiredSize); + maxXs = bigArrays.grow(maxXs, requiredSize); + maxYs = bigArrays.grow(maxYs, requiredSize); + minYs = bigArrays.grow(minYs, requiredSize); + } + } + + public Block toBlock(IntVector selected, DriverContext driverContext) { + try (var builder = driverContext.blockFactory().newBytesRefBlockBuilder(selected.getPositionCount())) { + for (int i = 0; i < selected.getPositionCount(); i++) { + int si = selected.getInt(i); + if (hasValue(si)) { + builder.appendBytesRef( + new BytesRef( + WellKnownBinary.toWKB( + new Rectangle( + pointType.decodeX(minXs.get(si)), + pointType.decodeX(maxXs.get(si)), + pointType.decodeY(maxYs.get(si)), + pointType.decodeY(minYs.get(si)) + ), + ByteOrder.LITTLE_ENDIAN + ) + ) + ); + } else { + builder.appendNull(); + } + } + return builder.build(); + } + } + + public void add(int groupId, SpatialExtentGroupingState inState, int inPosition) { + ensureCapacity(groupId); + if (inState.hasValue(inPosition)) { + add( + groupId, + inState.minXs.get(inPosition), + inState.maxXs.get(inPosition), + inState.maxYs.get(inPosition), + inState.minYs.get(inPosition) + ); + } + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGroupingStateWrappedLongitudeState.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGroupingStateWrappedLongitudeState.java new file mode 100644 index 0000000000000..3dd7a6d4acde2 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentGroupingStateWrappedLongitudeState.java @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.common.util.IntArray; +import org.elasticsearch.compute.aggregation.AbstractArrayState; +import org.elasticsearch.compute.aggregation.GroupingAggregatorState; +import org.elasticsearch.compute.aggregation.SeenGroupIds; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.WellKnownBinary; + +import java.nio.ByteOrder; + +final class SpatialExtentGroupingStateWrappedLongitudeState extends AbstractArrayState implements GroupingAggregatorState { + // Only geo points support longitude wrapping. + private static final PointType POINT_TYPE = PointType.GEO; + private IntArray minNegXs; + private IntArray minPosXs; + private IntArray maxNegXs; + private IntArray maxPosXs; + private IntArray maxYs; + private IntArray minYs; + + private GeoPointEnvelopeVisitor geoPointVisitor = new GeoPointEnvelopeVisitor(); + + SpatialExtentGroupingStateWrappedLongitudeState() { + this(BigArrays.NON_RECYCLING_INSTANCE); + } + + SpatialExtentGroupingStateWrappedLongitudeState(BigArrays bigArrays) { + super(bigArrays); + this.minNegXs = bigArrays.newIntArray(0, false); + this.minPosXs = bigArrays.newIntArray(0, false); + this.maxNegXs = bigArrays.newIntArray(0, false); + this.maxPosXs = bigArrays.newIntArray(0, false); + this.maxYs = bigArrays.newIntArray(0, false); + this.minYs = bigArrays.newIntArray(0, false); + enableGroupIdTracking(new SeenGroupIds.Empty()); + } + + @Override + public void toIntermediate(Block[] blocks, int offset, IntVector selected, DriverContext driverContext) { + assert blocks.length >= offset; + try ( + var minNegXsBuilder = driverContext.blockFactory().newIntBlockBuilder(selected.getPositionCount()); + var minPosXsBuilder = driverContext.blockFactory().newIntBlockBuilder(selected.getPositionCount()); + var maxNegXsBuilder = driverContext.blockFactory().newIntBlockBuilder(selected.getPositionCount()); + var maxPosXsBuilder = driverContext.blockFactory().newIntBlockBuilder(selected.getPositionCount()); + var maxYsBuilder = driverContext.blockFactory().newIntBlockBuilder(selected.getPositionCount()); + var minYsBuilder = driverContext.blockFactory().newIntBlockBuilder(selected.getPositionCount()); + ) { + for (int i = 0; i < selected.getPositionCount(); i++) { + int group = selected.getInt(i); + assert hasValue(group); + assert minNegXs.get(group) <= 0 == maxNegXs.get(group) <= 0; + assert minPosXs.get(group) >= 0 == maxPosXs.get(group) >= 0; + minNegXsBuilder.appendInt(minNegXs.get(group)); + minPosXsBuilder.appendInt(minPosXs.get(group)); + maxNegXsBuilder.appendInt(maxNegXs.get(group)); + maxPosXsBuilder.appendInt(maxPosXs.get(group)); + maxYsBuilder.appendInt(maxYs.get(group)); + minYsBuilder.appendInt(minYs.get(group)); + } + blocks[offset + 0] = minNegXsBuilder.build(); + blocks[offset + 1] = minPosXsBuilder.build(); + blocks[offset + 2] = maxNegXsBuilder.build(); + blocks[offset + 3] = maxPosXsBuilder.build(); + blocks[offset + 4] = maxYsBuilder.build(); + blocks[offset + 5] = minYsBuilder.build(); + } + } + + public void add(int groupId, Geometry geo) { + ensureCapacity(groupId); + geoPointVisitor.reset(); + if (geo.visit(new SpatialEnvelopeVisitor(geoPointVisitor))) { + add( + groupId, + SpatialAggregationUtils.encodeNegativeLongitude(geoPointVisitor.getMinNegX()), + SpatialAggregationUtils.encodePositiveLongitude(geoPointVisitor.getMinPosX()), + SpatialAggregationUtils.encodeNegativeLongitude(geoPointVisitor.getMaxNegX()), + SpatialAggregationUtils.encodePositiveLongitude(geoPointVisitor.getMaxPosX()), + POINT_TYPE.encodeY(geoPointVisitor.getMaxY()), + POINT_TYPE.encodeY(geoPointVisitor.getMinY()) + ); + } + } + + public void add(int groupId, SpatialExtentGroupingStateWrappedLongitudeState inState, int inPosition) { + ensureCapacity(groupId); + if (inState.hasValue(inPosition)) { + add( + groupId, + inState.minNegXs.get(inPosition), + inState.minPosXs.get(inPosition), + inState.maxNegXs.get(inPosition), + inState.maxPosXs.get(inPosition), + inState.maxYs.get(inPosition), + inState.minYs.get(inPosition) + ); + } + } + + public void add(int groupId, long encoded) { + int x = POINT_TYPE.extractX(encoded); + int y = POINT_TYPE.extractY(encoded); + add(groupId, x, x, x, x, y, y); + } + + public void add(int groupId, int minNegX, int minPosX, int maxNegX, int maxPosX, int maxY, int minY) { + ensureCapacity(groupId); + if (hasValue(groupId)) { + minNegXs.set(groupId, Math.min(minNegXs.get(groupId), minNegX)); + minPosXs.set(groupId, SpatialAggregationUtils.minPos(minPosXs.get(groupId), minPosX)); + maxNegXs.set(groupId, SpatialAggregationUtils.maxNeg(maxNegXs.get(groupId), maxNegX)); + maxPosXs.set(groupId, Math.max(maxPosXs.get(groupId), maxPosX)); + maxYs.set(groupId, Math.max(maxYs.get(groupId), maxY)); + minYs.set(groupId, Math.min(minYs.get(groupId), minY)); + } else { + minNegXs.set(groupId, minNegX); + minPosXs.set(groupId, minPosX); + maxNegXs.set(groupId, maxNegX); + maxPosXs.set(groupId, maxPosX); + maxYs.set(groupId, maxY); + minYs.set(groupId, minY); + } + assert minNegX <= 0 == maxNegX <= 0 : "minNegX=" + minNegX + " maxNegX=" + maxNegX; + assert minPosX >= 0 == maxPosX >= 0 : "minPosX=" + minPosX + " maxPosX=" + maxPosX; + trackGroupId(groupId); + } + + private void ensureCapacity(int groupId) { + long requiredSize = groupId + 1; + if (minNegXs.size() < requiredSize) { + minNegXs = bigArrays.grow(minNegXs, requiredSize); + minPosXs = bigArrays.grow(minPosXs, requiredSize); + maxNegXs = bigArrays.grow(maxNegXs, requiredSize); + maxPosXs = bigArrays.grow(maxPosXs, requiredSize); + minYs = bigArrays.grow(minYs, requiredSize); + maxYs = bigArrays.grow(maxYs, requiredSize); + } + } + + public Block toBlock(IntVector selected, DriverContext driverContext) { + try (var builder = driverContext.blockFactory().newBytesRefBlockBuilder(selected.getPositionCount())) { + for (int i = 0; i < selected.getPositionCount(); i++) { + int si = selected.getInt(i); + if (hasValue(si)) { + builder.appendBytesRef( + new BytesRef( + WellKnownBinary.toWKB( + SpatialAggregationUtils.asRectangle( + minNegXs.get(si), + minPosXs.get(si), + maxNegXs.get(si), + maxPosXs.get(si), + maxYs.get(si), + minYs.get(si) + ), + ByteOrder.LITTLE_ENDIAN + ) + ) + ); + } else { + builder.appendNull(); + } + } + return builder.build(); + } + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentLongitudeWrappingAggregator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentLongitudeWrappingAggregator.java new file mode 100644 index 0000000000000..80ba2d5e45658 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentLongitudeWrappingAggregator.java @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.data.IntVector; +import org.elasticsearch.compute.operator.DriverContext; + +// A bit of abuse of notation here, since we're extending this class to "inherit" its static methods. +// Unfortunately, this is the way it has to be done, since the generated code invokes these methods statically. +abstract class SpatialExtentLongitudeWrappingAggregator { + public static void combineIntermediate( + SpatialExtentStateWrappedLongitudeState current, + int minNegX, + int minPosX, + int maxNegX, + int maxPosX, + int maxY, + int minY + ) { + current.add(minNegX, minPosX, maxNegX, maxPosX, maxY, minY); + } + + public static void combineIntermediate( + SpatialExtentGroupingStateWrappedLongitudeState current, + int groupId, + int minNegX, + int minPosX, + int maxNegX, + int maxPosX, + int maxY, + int minY + ) { + current.add(groupId, minNegX, minPosX, maxNegX, maxPosX, maxY, minY); + } + + public static Block evaluateFinal(SpatialExtentStateWrappedLongitudeState state, DriverContext driverContext) { + return state.toBlock(driverContext); + } + + public static Block evaluateFinal( + SpatialExtentGroupingStateWrappedLongitudeState state, + IntVector selected, + DriverContext driverContext + ) { + return state.toBlock(selected, driverContext); + } + + public static void combineStates( + SpatialExtentGroupingStateWrappedLongitudeState current, + int groupId, + SpatialExtentGroupingStateWrappedLongitudeState inState, + int inPosition + ) { + current.add(groupId, inState, inPosition); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentState.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentState.java new file mode 100644 index 0000000000000..0eea9b79f73ea --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentState.java @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.aggregation.AggregatorState; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.utils.WellKnownBinary; + +import java.nio.ByteOrder; + +final class SpatialExtentState implements AggregatorState { + private final PointType pointType; + private boolean seen = false; + private int minX = Integer.MAX_VALUE; + private int maxX = Integer.MIN_VALUE; + private int maxY = Integer.MIN_VALUE; + private int minY = Integer.MAX_VALUE; + + SpatialExtentState(PointType pointType) { + this.pointType = pointType; + } + + @Override + public void close() {} + + @Override + public void toIntermediate(Block[] blocks, int offset, DriverContext driverContext) { + assert blocks.length >= offset + 4; + var blockFactory = driverContext.blockFactory(); + blocks[offset + 0] = blockFactory.newConstantIntBlockWith(minX, 1); + blocks[offset + 1] = blockFactory.newConstantIntBlockWith(maxX, 1); + blocks[offset + 2] = blockFactory.newConstantIntBlockWith(maxY, 1); + blocks[offset + 3] = blockFactory.newConstantIntBlockWith(minY, 1); + } + + public void add(Geometry geo) { + pointType.computeEnvelope(geo) + .ifPresent( + r -> add( + pointType.encodeX(r.getMinX()), + pointType.encodeX(r.getMaxX()), + pointType.encodeY(r.getMaxY()), + pointType.encodeY(r.getMinY()) + ) + ); + } + + public void add(int minX, int maxX, int maxY, int minY) { + seen = true; + this.minX = Math.min(this.minX, minX); + this.maxX = Math.max(this.maxX, maxX); + this.maxY = Math.max(this.maxY, maxY); + this.minY = Math.min(this.minY, minY); + } + + public void add(long encoded) { + int x = pointType.extractX(encoded); + int y = pointType.extractY(encoded); + add(x, x, y, y); + } + + public Block toBlock(DriverContext driverContext) { + var factory = driverContext.blockFactory(); + return seen ? factory.newConstantBytesRefBlockWith(new BytesRef(toWKB()), 1) : factory.newConstantNullBlock(1); + } + + private byte[] toWKB() { + return WellKnownBinary.toWKB( + new Rectangle(pointType.decodeX(minX), pointType.decodeX(maxX), pointType.decodeY(maxY), pointType.decodeY(minY)), + ByteOrder.LITTLE_ENDIAN + ); + } +} diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentStateWrappedLongitudeState.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentStateWrappedLongitudeState.java new file mode 100644 index 0000000000000..99200d2ed99f5 --- /dev/null +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/aggregation/spatial/SpatialExtentStateWrappedLongitudeState.java @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.compute.aggregation.spatial; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.aggregation.AggregatorState; +import org.elasticsearch.compute.data.Block; +import org.elasticsearch.compute.operator.DriverContext; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.WellKnownBinary; + +import java.nio.ByteOrder; + +final class SpatialExtentStateWrappedLongitudeState implements AggregatorState { + // Only geo points support longitude wrapping. + private static final PointType POINT_TYPE = PointType.GEO; + private boolean seen = false; + private int minNegX = SpatialAggregationUtils.DEFAULT_NEG; + private int minPosX = SpatialAggregationUtils.DEFAULT_POS; + private int maxNegX = SpatialAggregationUtils.DEFAULT_NEG; + private int maxPosX = SpatialAggregationUtils.DEFAULT_POS; + private int maxY = Integer.MIN_VALUE; + private int minY = Integer.MAX_VALUE; + + private GeoPointEnvelopeVisitor geoPointVisitor = new GeoPointEnvelopeVisitor(); + + @Override + public void close() {} + + @Override + public void toIntermediate(Block[] blocks, int offset, DriverContext driverContext) { + assert blocks.length >= offset + 6; + var blockFactory = driverContext.blockFactory(); + blocks[offset + 0] = blockFactory.newConstantIntBlockWith(minNegX, 1); + blocks[offset + 1] = blockFactory.newConstantIntBlockWith(minPosX, 1); + blocks[offset + 2] = blockFactory.newConstantIntBlockWith(maxNegX, 1); + blocks[offset + 3] = blockFactory.newConstantIntBlockWith(maxPosX, 1); + blocks[offset + 4] = blockFactory.newConstantIntBlockWith(maxY, 1); + blocks[offset + 5] = blockFactory.newConstantIntBlockWith(minY, 1); + } + + public void add(Geometry geo) { + geoPointVisitor.reset(); + if (geo.visit(new SpatialEnvelopeVisitor(geoPointVisitor))) { + add( + SpatialAggregationUtils.encodeNegativeLongitude(geoPointVisitor.getMinNegX()), + SpatialAggregationUtils.encodePositiveLongitude(geoPointVisitor.getMinPosX()), + SpatialAggregationUtils.encodeNegativeLongitude(geoPointVisitor.getMaxNegX()), + SpatialAggregationUtils.encodePositiveLongitude(geoPointVisitor.getMaxPosX()), + POINT_TYPE.encodeY(geoPointVisitor.getMaxY()), + POINT_TYPE.encodeY(geoPointVisitor.getMinY()) + ); + } + } + + public void add(int minNegX, int minPosX, int maxNegX, int maxPosX, int maxY, int minY) { + seen = true; + this.minNegX = Math.min(this.minNegX, minNegX); + this.minPosX = SpatialAggregationUtils.minPos(this.minPosX, minPosX); + this.maxNegX = SpatialAggregationUtils.maxNeg(this.maxNegX, maxNegX); + this.maxPosX = Math.max(this.maxPosX, maxPosX); + this.maxY = Math.max(this.maxY, maxY); + this.minY = Math.min(this.minY, minY); + assert this.minNegX <= 0 == this.maxNegX <= 0 : "minNegX=" + this.minNegX + " maxNegX=" + this.maxNegX; + assert this.minPosX >= 0 == this.maxPosX >= 0 : "minPosX=" + this.minPosX + " maxPosX=" + this.maxPosX; + } + + public void add(long encoded) { + int x = POINT_TYPE.extractX(encoded); + int y = POINT_TYPE.extractY(encoded); + add(x, x, x, x, y, y); + } + + public Block toBlock(DriverContext driverContext) { + var factory = driverContext.blockFactory(); + return seen ? factory.newConstantBytesRefBlockWith(new BytesRef(toWKB()), 1) : factory.newConstantNullBlock(1); + } + + private byte[] toWKB() { + return WellKnownBinary.toWKB( + SpatialAggregationUtils.asRectangle(minNegX, minPosX, maxNegX, maxPosX, maxY, minY), + ByteOrder.LITTLE_ENDIAN + ); + } +} diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-airports_no_doc_values.json b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-airports_no_doc_values.json index d7097f89a17df..782fd40712f43 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-airports_no_doc_values.json +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-airports_no_doc_values.json @@ -24,7 +24,9 @@ "type": "keyword" }, "city_location": { - "type": "geo_point" + "type": "geo_point", + "index": true, + "doc_values": false } } } diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec index ac9948c90f5e9..8694c973448e9 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/spatial.csv-spec @@ -519,6 +519,63 @@ centroid:geo_point | count:long POINT (42.97109629958868 14.7552534006536) | 1 ; +############################################### +# Tests for ST_EXTENT_AGG on GEO_POINT type + +stExtentSingleGeoPoint +required_capability: st_extent_agg +ROW point = TO_GEOPOINT("POINT(42.97109629958868 14.7552534006536)") +| STATS extent = ST_EXTENT_AGG(point) +; + +extent:geo_shape +BBOX(42.97109629958868, 42.97109629958868, 14.7552534006536, 14.7552534006536) +; + +stExtentMultipleGeoPoints +required_capability: st_extent_agg +// tag::st_extent_agg-airports[] +FROM airports +| WHERE country == "India" +| STATS extent = ST_EXTENT_AGG(location) +// end::st_extent_agg-airports[] +; + +// tag::st_extent_agg-airports-result[] +extent:geo_shape +BBOX (70.77995480038226, 91.5882289968431, 33.9830909203738, 8.47650992218405) +// end::st_extent_agg-airports-result[] +; + +stExtentMultipleGeoPointsNoDocValues +required_capability: st_extent_agg +FROM airports_no_doc_values | WHERE country == "India" | STATS extent = ST_EXTENT_AGG(location) +; + +extent:geo_shape +BBOX (70.77995480038226, 91.5882289968431, 33.9830909203738, 8.47650992218405) +; + +stExtentMultipleGeoPointGrouping +required_capability: st_extent_agg +FROM airports | STATS extent = ST_EXTENT_AGG(location) BY country | SORT country | LIMIT 3 +; + +extent:geo_shape | country:keyword +BBOX (69.2100736219436, 69.2100736219436, 34.56339786294848, 34.56339786294848) | Afghanistan +BBOX (19.715032372623682, 19.715032372623682, 41.4208514476195, 41.4208514476195) | Albania +BBOX (-0.6067969836294651, 6.621946580708027, 36.69972063973546, 35.62027471605688) | Algeria +; + +stExtentGeoShapes +required_capability: st_extent_agg +FROM airport_city_boundaries | WHERE region == "City of New York" | STATS extent = ST_EXTENT_AGG(city_boundary) +; + +extent:geo_shape +BBOX (-74.25880000926554, -73.70020005851984, 40.91759996954352, 40.47659996431321) +; + ############################################### # Tests for ST_INTERSECTS on GEO_POINT type @@ -1698,6 +1755,48 @@ centroid:cartesian_point | count:long POINT (726480.0130685265 3359566.331716279) | 849 ; +############################################### +# Tests for ST_EXTENT_AGG on CARTESIAN_POINT type + +stExtentSingleCartesianPoint +required_capability: st_extent_agg +ROW point = TO_CARTESIANPOINT("POINT(429.7109629958868 147.552534006536)") +| STATS extent = ST_EXTENT_AGG(point) +; + +extent:cartesian_shape +BBOX (429.7109680175781, 429.7109680175781, 147.5525360107422, 147.5525360107422) +; + +stExtentMultipleCartesianPoints +required_capability: st_extent_agg +FROM airports_web | WHERE scalerank == 9 | STATS extent = ST_EXTENT_AGG(location) +; + +extent:cartesian_shape +BBOX (4783520.5, 1.6168486E7, 8704352.0, -584415.9375) +; + +stExtentMultipleCartesianPointGrouping +required_capability: st_extent_agg +FROM airports_web | STATS extent = ST_EXTENT_AGG(location) BY scalerank | SORT scalerank DESC | LIMIT 3 +; + +extent:cartesian_shape | scalerank:integer +BBOX (4783520.5, 1.6168486E7, 8704352.0, -584415.9375) | 9 +BBOX (-1.936604E7, 1.8695374E7, 1.4502138E7, -3943067.25) | 8 +BBOX (-1.891609E7, 1.9947946E7, 8455470.0, -7128878.5) | 7 +; + +stExtentCartesianShapes +required_capability: st_extent_agg +FROM cartesian_multipolygons | STATS extent = ST_EXTENT_AGG(shape) +; + +extent:cartesian_shape +BBOX (0.0, 3.0, 3.0, 0.0) +; + ############################################### # Tests for ST_INTERSECTS on CARTESIAN_POINT type diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index ddabb3e937dc2..4cf3162fcca3b 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -189,6 +189,9 @@ public enum Cap { */ ST_DISTANCE, + /** Support for function {@code ST_EXTENT}. */ + ST_EXTENT_AGG, + /** * Fix determination of CRS types in spatial functions when folding. */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index 3749b46879354..50d0d2438d8a1 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -28,6 +28,7 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.Percentile; import org.elasticsearch.xpack.esql.expression.function.aggregate.Rate; import org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialCentroid; +import org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialExtent; import org.elasticsearch.xpack.esql.expression.function.aggregate.StdDev; import org.elasticsearch.xpack.esql.expression.function.aggregate.Sum; import org.elasticsearch.xpack.esql.expression.function.aggregate.Top; @@ -353,6 +354,7 @@ private static FunctionDefinition[][] functions() { new FunctionDefinition[] { def(SpatialCentroid.class, SpatialCentroid::new, "st_centroid_agg"), def(SpatialContains.class, SpatialContains::new, "st_contains"), + def(SpatialExtent.class, SpatialExtent::new, "st_extent_agg"), def(SpatialDisjoint.class, SpatialDisjoint::new, "st_disjoint"), def(SpatialIntersects.class, SpatialIntersects::new, "st_intersects"), def(SpatialWithin.class, SpatialWithin::new, "st_within"), diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java index d74b5c8b386b8..db1d2a9e6f254 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/AggregateWritables.java @@ -25,6 +25,7 @@ public static List getNamedWriteables() { Percentile.ENTRY, Rate.ENTRY, SpatialCentroid.ENTRY, + SpatialExtent.ENTRY, StdDev.ENTRY, Sum.ENTRY, Top.ENTRY, diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialAggregateFunction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialAggregateFunction.java index 87eec540932b1..35f99e4b648df 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialAggregateFunction.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialAggregateFunction.java @@ -8,6 +8,9 @@ package org.elasticsearch.xpack.esql.expression.function.aggregate; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference; +import org.elasticsearch.license.License; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; @@ -22,26 +25,34 @@ * select the best one. */ public abstract class SpatialAggregateFunction extends AggregateFunction { - protected final boolean useDocValues; + protected final FieldExtractPreference fieldExtractPreference; - protected SpatialAggregateFunction(Source source, Expression field, Expression filter, boolean useDocValues) { + protected SpatialAggregateFunction(Source source, Expression field, Expression filter, FieldExtractPreference fieldExtractPreference) { super(source, field, filter, emptyList()); - this.useDocValues = useDocValues; + this.fieldExtractPreference = fieldExtractPreference; } - protected SpatialAggregateFunction(StreamInput in, boolean useDocValues) throws IOException { + protected SpatialAggregateFunction(StreamInput in, FieldExtractPreference fieldExtractPreference) throws IOException { super(in); - // The useDocValues field is only used on data nodes local planning, and therefor never serialized - this.useDocValues = useDocValues; + // The fieldExtractPreference field is only used on data nodes local planning, and therefore never serialized + this.fieldExtractPreference = fieldExtractPreference; } public abstract SpatialAggregateFunction withDocValues(); + @Override + public boolean checkLicense(XPackLicenseState state) { + return switch (field().dataType()) { + case GEO_SHAPE, CARTESIAN_SHAPE -> state.isAllowedByLicense(License.OperationMode.PLATINUM); + default -> true; + }; + } + @Override public int hashCode() { // NB: the hashcode is currently used for key generation so // to avoid clashes between aggs with the same arguments, add the class name as variation - return Objects.hash(getClass(), children(), useDocValues); + return Objects.hash(getClass(), children(), fieldExtractPreference); } @Override @@ -50,12 +61,12 @@ public boolean equals(Object obj) { SpatialAggregateFunction other = (SpatialAggregateFunction) obj; return Objects.equals(other.field(), field()) && Objects.equals(other.parameters(), parameters()) - && Objects.equals(other.useDocValues, useDocValues); + && Objects.equals(other.fieldExtractPreference, fieldExtractPreference); } return false; } - public boolean useDocValues() { - return useDocValues; + public FieldExtractPreference fieldExtractPreference() { + return fieldExtractPreference; } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialCentroid.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialCentroid.java index aad95c07e3492..84915d024ea82 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialCentroid.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialCentroid.java @@ -13,6 +13,7 @@ import org.elasticsearch.compute.aggregation.spatial.SpatialCentroidCartesianPointSourceValuesAggregatorFunctionSupplier; import org.elasticsearch.compute.aggregation.spatial.SpatialCentroidGeoPointDocValuesAggregatorFunctionSupplier; import org.elasticsearch.compute.aggregation.spatial.SpatialCentroidGeoPointSourceValuesAggregatorFunctionSupplier; +import org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference; import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.Literal; @@ -27,6 +28,7 @@ import java.io.IOException; import java.util.List; +import static org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference.NONE; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT; import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isSpatialPoint; @@ -47,15 +49,15 @@ public class SpatialCentroid extends SpatialAggregateFunction implements ToAggre examples = @Example(file = "spatial", tag = "st_centroid_agg-airports") ) public SpatialCentroid(Source source, @Param(name = "field", type = { "geo_point", "cartesian_point" }) Expression field) { - this(source, field, Literal.TRUE, false); + this(source, field, Literal.TRUE, NONE); } - private SpatialCentroid(Source source, Expression field, Expression filter, boolean useDocValues) { - super(source, field, filter, useDocValues); + private SpatialCentroid(Source source, Expression field, Expression filter, FieldExtractPreference preference) { + super(source, field, filter, preference); } private SpatialCentroid(StreamInput in) throws IOException { - super(in, false); + super(in, NONE); } @Override @@ -65,12 +67,12 @@ public String getWriteableName() { @Override public SpatialCentroid withFilter(Expression filter) { - return new SpatialCentroid(source(), field(), filter, useDocValues); + return new SpatialCentroid(source(), field(), filter, fieldExtractPreference); } @Override public SpatialCentroid withDocValues() { - return new SpatialCentroid(source(), field(), filter(), true); + return new SpatialCentroid(source(), field(), filter(), FieldExtractPreference.DOC_VALUES); } @Override @@ -98,23 +100,16 @@ public SpatialCentroid replaceChildren(List newChildren) { @Override public AggregatorFunctionSupplier supplier(List inputChannels) { DataType type = field().dataType(); - if (useDocValues) { - // When the points are read as doc-values (eg. from the index), feed them into the doc-values aggregator - if (type == DataType.GEO_POINT) { - return new SpatialCentroidGeoPointDocValuesAggregatorFunctionSupplier(inputChannels); - } - if (type == DataType.CARTESIAN_POINT) { - return new SpatialCentroidCartesianPointDocValuesAggregatorFunctionSupplier(inputChannels); - } - } else { - // When the points are read as WKB from source or as point literals, feed them into the source-values aggregator - if (type == DataType.GEO_POINT) { - return new SpatialCentroidGeoPointSourceValuesAggregatorFunctionSupplier(inputChannels); - } - if (type == DataType.CARTESIAN_POINT) { - return new SpatialCentroidCartesianPointSourceValuesAggregatorFunctionSupplier(inputChannels); - } - } - throw EsqlIllegalArgumentException.illegalDataType(type); + return switch (type) { + case DataType.GEO_POINT -> switch (fieldExtractPreference) { + case DOC_VALUES -> new SpatialCentroidGeoPointDocValuesAggregatorFunctionSupplier(inputChannels); + case NONE -> new SpatialCentroidGeoPointSourceValuesAggregatorFunctionSupplier(inputChannels); + }; + case DataType.CARTESIAN_POINT -> switch (fieldExtractPreference) { + case DOC_VALUES -> new SpatialCentroidCartesianPointDocValuesAggregatorFunctionSupplier(inputChannels); + case NONE -> new SpatialCentroidCartesianPointSourceValuesAggregatorFunctionSupplier(inputChannels); + }; + default -> throw EsqlIllegalArgumentException.illegalDataType(type); + }; } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialExtent.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialExtent.java new file mode 100644 index 0000000000000..5cc1701faf13a --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialExtent.java @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.esql.expression.function.aggregate; + +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.compute.aggregation.AggregatorFunctionSupplier; +import org.elasticsearch.compute.aggregation.spatial.SpatialExtentCartesianPointDocValuesAggregatorFunctionSupplier; +import org.elasticsearch.compute.aggregation.spatial.SpatialExtentCartesianPointSourceValuesAggregatorFunctionSupplier; +import org.elasticsearch.compute.aggregation.spatial.SpatialExtentCartesianShapeAggregatorFunctionSupplier; +import org.elasticsearch.compute.aggregation.spatial.SpatialExtentGeoPointDocValuesAggregatorFunctionSupplier; +import org.elasticsearch.compute.aggregation.spatial.SpatialExtentGeoPointSourceValuesAggregatorFunctionSupplier; +import org.elasticsearch.compute.aggregation.spatial.SpatialExtentGeoShapeAggregatorFunctionSupplier; +import org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference; +import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.expression.Literal; +import org.elasticsearch.xpack.esql.core.tree.NodeInfo; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.function.Example; +import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.planner.ToAggregator; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT; +import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isSpatial; + +/** + * Calculate spatial extent of all values of a field in matching documents. + */ +public final class SpatialExtent extends SpatialAggregateFunction implements ToAggregator { + public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( + Expression.class, + "SpatialExtent", + SpatialExtent::new + ); + + @FunctionInfo( + returnType = { "geo_shape", "cartesian_shape" }, + description = "Calculate the spatial extent over a field with geometry type. Returns a bounding box for all values of the field.", + isAggregation = true, + examples = @Example(file = "spatial", tag = "st_extent_agg-airports") + ) + public SpatialExtent( + Source source, + @Param(name = "field", type = { "geo_point", "cartesian_point", "geo_shape", "cartesian_shape" }) Expression field + ) { + this(source, field, Literal.TRUE, FieldExtractPreference.NONE); + } + + private SpatialExtent(Source source, Expression field, Expression filter, FieldExtractPreference preference) { + super(source, field, filter, preference); + } + + private SpatialExtent(StreamInput in) throws IOException { + super(in, FieldExtractPreference.NONE); + } + + @Override + public String getWriteableName() { + return ENTRY.name; + } + + @Override + public SpatialExtent withFilter(Expression filter) { + return new SpatialExtent(source(), field(), filter, fieldExtractPreference); + } + + @Override + public org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialExtent withDocValues() { + return new SpatialExtent(source(), field(), filter(), FieldExtractPreference.DOC_VALUES); + } + + @Override + protected TypeResolution resolveType() { + return isSpatial(field(), sourceText(), DEFAULT); + } + + @Override + public DataType dataType() { + return DataType.isSpatialGeo(field().dataType()) ? DataType.GEO_SHAPE : DataType.CARTESIAN_SHAPE; + } + + @Override + protected NodeInfo info() { + return NodeInfo.create(this, SpatialExtent::new, field()); + } + + @Override + public SpatialExtent replaceChildren(List newChildren) { + return new SpatialExtent(source(), newChildren.get(0)); + } + + @Override + public AggregatorFunctionSupplier supplier(List inputChannels) { + return switch (field().dataType()) { + case DataType.GEO_POINT -> switch (fieldExtractPreference) { + case DOC_VALUES -> new SpatialExtentGeoPointDocValuesAggregatorFunctionSupplier(inputChannels); + case NONE -> new SpatialExtentGeoPointSourceValuesAggregatorFunctionSupplier(inputChannels); + }; + case DataType.CARTESIAN_POINT -> switch (fieldExtractPreference) { + case DOC_VALUES -> new SpatialExtentCartesianPointDocValuesAggregatorFunctionSupplier(inputChannels); + case NONE -> new SpatialExtentCartesianPointSourceValuesAggregatorFunctionSupplier(inputChannels); + }; + // Shapes don't differentiate between source and doc values. + case DataType.GEO_SHAPE -> new SpatialExtentGeoShapeAggregatorFunctionSupplier(inputChannels); + case DataType.CARTESIAN_SHAPE -> new SpatialExtentCartesianShapeAggregatorFunctionSupplier(inputChannels); + default -> throw EsqlIllegalArgumentException.illegalDataType(field().dataType()); + }; + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelope.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelope.java index 934991f3a8088..ca243efcc2851 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelope.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelope.java @@ -14,6 +14,7 @@ import org.elasticsearch.compute.operator.EvalOperator; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; @@ -129,7 +130,7 @@ static BytesRef fromWellKnownBinaryGeo(BytesRef wkb) { if (geometry instanceof Point) { return wkb; } - var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, true); + var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.WRAP); if (envelope.isPresent()) { return UNSPECIFIED.asWkb(envelope.get()); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMax.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMax.java index d6d710b175113..69eede1c5fac5 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMax.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMax.java @@ -14,6 +14,7 @@ import org.elasticsearch.compute.operator.EvalOperator; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; @@ -114,7 +115,7 @@ static double fromWellKnownBinaryGeo(BytesRef wkb) { if (geometry instanceof Point point) { return point.getX(); } - var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, true); + var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.WRAP); if (envelope.isPresent()) { return envelope.get().getMaxX(); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMin.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMin.java index a5fa11bc11b0f..b29a547ab0af6 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMin.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMin.java @@ -14,6 +14,7 @@ import org.elasticsearch.compute.operator.EvalOperator; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; @@ -114,7 +115,7 @@ static double fromWellKnownBinaryGeo(BytesRef wkb) { if (geometry instanceof Point point) { return point.getX(); } - var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, true); + var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.WRAP); if (envelope.isPresent()) { return envelope.get().getMinX(); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMax.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMax.java index fbbea8e024a6b..981b500bcaef7 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMax.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMax.java @@ -14,6 +14,7 @@ import org.elasticsearch.compute.operator.EvalOperator; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; @@ -114,7 +115,7 @@ static double fromWellKnownBinaryGeo(BytesRef wkb) { if (geometry instanceof Point point) { return point.getY(); } - var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, true); + var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.WRAP); if (envelope.isPresent()) { return envelope.get().getMaxY(); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMin.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMin.java index 1707d3b4f2fb9..882aeb30afaee 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMin.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMin.java @@ -14,6 +14,7 @@ import org.elasticsearch.compute.operator.EvalOperator; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; @@ -114,7 +115,7 @@ static double fromWellKnownBinaryGeo(BytesRef wkb) { if (geometry instanceof Point point) { return point.getY(); } - var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, true); + var envelope = SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.WRAP); if (envelope.isPresent()) { return envelope.get().getMinY(); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AggregateMapper.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AggregateMapper.java index 1f55e293b8e75..1918e3036e2b0 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AggregateMapper.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/AggregateMapper.java @@ -35,6 +35,7 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.Rate; import org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialAggregateFunction; import org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialCentroid; +import org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialExtent; import org.elasticsearch.xpack.esql.expression.function.aggregate.StdDev; import org.elasticsearch.xpack.esql.expression.function.aggregate.Sum; import org.elasticsearch.xpack.esql.expression.function.aggregate.ToPartial; @@ -66,7 +67,7 @@ final class AggregateMapper { private static final List NUMERIC = List.of("Int", "Long", "Double"); - private static final List SPATIAL = List.of("GeoPoint", "CartesianPoint"); + private static final List SPATIAL_EXTRA_CONFIGS = List.of("SourceValues", "DocValues"); /** List of all mappable ESQL agg functions (excludes surrogates like AVG = SUM/COUNT). */ private static final List> AGG_FUNCTIONS = List.of( @@ -77,6 +78,7 @@ final class AggregateMapper { Min.class, Percentile.class, SpatialCentroid.class, + SpatialExtent.class, StdDev.class, Sum.class, Values.class, @@ -89,7 +91,11 @@ final class AggregateMapper { ); /** Record of agg Class, type, and grouping (or non-grouping). */ - private record AggDef(Class aggClazz, String type, String extra, boolean grouping) {} + private record AggDef(Class aggClazz, String type, String extra, boolean grouping) { + public AggDef withoutExtra() { + return new AggDef(aggClazz, type, "", grouping); + } + } /** Map of AggDef types to intermediate named expressions. */ private static final Map> MAPPER = AGG_FUNCTIONS.stream() @@ -145,7 +151,7 @@ private static List entryForAgg(String aggAlias, AggregateFunct var aggDef = new AggDef( aggregateFunction.getClass(), dataTypeToString(aggregateFunction.field().dataType(), aggregateFunction.getClass()), - aggregateFunction instanceof SpatialCentroid ? "SourceValues" : "", + aggregateFunction instanceof SpatialAggregateFunction ? "SourceValues" : "", grouping ); var is = getNonNull(aggDef); @@ -154,7 +160,7 @@ private static List entryForAgg(String aggAlias, AggregateFunct /** Gets the agg from the mapper - wrapper around map::get for more informative failure.*/ private static List getNonNull(AggDef aggDef) { - var l = MAPPER.get(aggDef); + var l = MAPPER.getOrDefault(aggDef, MAPPER.get(aggDef.withoutExtra())); if (l == null) { throw new EsqlIllegalArgumentException("Cannot find intermediate state for: " + aggDef); } @@ -170,9 +176,14 @@ private static Stream, Tuple>> typeAndNames(Class types = List.of("Boolean", "Int", "Long", "Double", "Ip", "BytesRef"); } else if (clazz == Count.class) { types = List.of(""); // no extra type distinction - } else if (SpatialAggregateFunction.class.isAssignableFrom(clazz)) { - types = SPATIAL; - extraConfigs = List.of("SourceValues", "DocValues"); + } else if (clazz == SpatialCentroid.class) { + types = List.of("GeoPoint", "CartesianPoint"); + extraConfigs = SPATIAL_EXTRA_CONFIGS; + } else if (clazz == SpatialExtent.class) { + return Stream.concat( + combine(clazz, List.of("GeoPoint", "CartesianPoint"), SPATIAL_EXTRA_CONFIGS), + combine(clazz, List.of("GeoShape", "CartesianShape"), List.of("")) + ); } else if (Values.class.isAssignableFrom(clazz)) { // TODO can't we figure this out from the function itself? types = List.of("Int", "Long", "Double", "Boolean", "BytesRef"); @@ -188,6 +199,10 @@ private static Stream, Tuple>> typeAndNames(Class assert false : "unknown aggregate type " + clazz; throw new IllegalArgumentException("unknown aggregate type " + clazz); } + return combine(clazz, types, extraConfigs); + } + + private static Stream, Tuple>> combine(Class clazz, List types, List extraConfigs) { return combinations(types, extraConfigs).map(combo -> new Tuple<>(clazz, combo)); } @@ -219,6 +234,15 @@ private static List lookupIntermediateState(AggDef aggDef /** Looks up the intermediate state method for a given class, type, and grouping. */ private static MethodHandle lookup(Class clazz, String type, String extra, boolean grouping) { + try { + return lookupRetry(clazz, type, extra, grouping); + } catch (IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) { + throw new EsqlIllegalArgumentException(e); + } + } + + private static MethodHandle lookupRetry(Class clazz, String type, String extra, boolean grouping) throws IllegalAccessException, + NoSuchMethodException, ClassNotFoundException { try { return MethodHandles.lookup() .findStatic( @@ -226,8 +250,14 @@ private static MethodHandle lookup(Class clazz, String type, String extra, bo "intermediateStateDesc", MethodType.methodType(List.class) ); - } catch (IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) { - throw new EsqlIllegalArgumentException(e); + } catch (NoSuchMethodException ignore) { + // Retry without the extra information. + return MethodHandles.lookup() + .findStatic( + Class.forName(determineAggName(clazz, type, "", grouping)), + "intermediateStateDesc", + MethodType.methodType(List.class) + ); } } @@ -301,8 +331,10 @@ private static String dataTypeToString(DataType type, Class aggClass) { case DataType.KEYWORD, DataType.IP, DataType.VERSION, DataType.TEXT, DataType.SEMANTIC_TEXT -> "BytesRef"; case GEO_POINT -> "GeoPoint"; case CARTESIAN_POINT -> "CartesianPoint"; + case GEO_SHAPE -> "GeoShape"; + case CARTESIAN_SHAPE -> "CartesianShape"; case UNSUPPORTED, NULL, UNSIGNED_LONG, SHORT, BYTE, FLOAT, HALF_FLOAT, SCALED_FLOAT, OBJECT, SOURCE, DATE_PERIOD, TIME_DURATION, - CARTESIAN_SHAPE, GEO_SHAPE, DOC_DATA_TYPE, TSID_DATA_TYPE, PARTIAL_AGG -> throw new EsqlIllegalArgumentException( + DOC_DATA_TYPE, TSID_DATA_TYPE, PARTIAL_AGG -> throw new EsqlIllegalArgumentException( "illegal agg type: " + type.typeName() ); }; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/RectangleMatcher.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/RectangleMatcher.java new file mode 100644 index 0000000000000..48fbc9c8e0378 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/RectangleMatcher.java @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression; + +import org.elasticsearch.compute.aggregation.spatial.PointType; +import org.elasticsearch.geometry.Rectangle; +import org.hamcrest.Description; +import org.hamcrest.Matchers; +import org.hamcrest.TypeSafeMatcher; + +/** + * Example usage: assertThat(actualRectangle, RectangleMatcher.closeTo(expectedRectangle, 0.0001, PointType.CARTESIAN));, or it + * can be used as a parameter to {@link WellKnownBinaryBytesRefMatcher}. + */ +public class RectangleMatcher extends TypeSafeMatcher { + private final Rectangle r; + private final PointType pointType; + private final double error; + + public static TypeSafeMatcher closeTo(Rectangle r, double error, PointType pointType) { + return new RectangleMatcher(r, error, pointType); + } + + private RectangleMatcher(Rectangle r, double error, PointType pointType) { + this.r = r; + this.pointType = pointType; + this.error = error; + } + + @Override + protected boolean matchesSafely(Rectangle other) { + // For geo bounds, longitude of (-180, 180) and (epsilon, -epsilon) are actually very close, since both encompass the entire globe. + boolean wrapAroundWorkAround = pointType == PointType.GEO && r.getMinX() >= r.getMaxX(); + boolean matchMinX = Matchers.closeTo(r.getMinX(), error).matches(other.getMinX()) + || (wrapAroundWorkAround && Matchers.closeTo(r.getMinX() - 180, error).matches(other.getMinX())) + || (wrapAroundWorkAround && Matchers.closeTo(r.getMinX(), error).matches(other.getMinX() - 180)); + boolean matchMaxX = Matchers.closeTo(r.getMaxX(), error).matches(other.getMaxX()) + || (wrapAroundWorkAround && Matchers.closeTo(r.getMaxX() + 180, error).matches(other.getMaxX())) + || (wrapAroundWorkAround && Matchers.closeTo(r.getMaxX(), error).matches(other.getMaxX() + 180)); + + return matchMinX + && matchMaxX + && Matchers.closeTo(r.getMaxY(), error).matches(other.getMaxY()) + && Matchers.closeTo(r.getMinY(), error).matches(other.getMinY()); + } + + @Override + public void describeMismatchSafely(Rectangle rectangle, Description description) { + description.appendText("was ").appendValue(rectangle); + } + + @Override + public void describeTo(Description description) { + description.appendValue(" " + r); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/WellKnownBinaryBytesRefMatcher.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/WellKnownBinaryBytesRefMatcher.java new file mode 100644 index 0000000000000..535bb820458cd --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/WellKnownBinaryBytesRefMatcher.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.utils.GeometryValidator; +import org.elasticsearch.geometry.utils.WellKnownBinary; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +/** A wrapper for matching geometries encoded as WKB in a BytesRef. */ +public class WellKnownBinaryBytesRefMatcher extends TypeSafeMatcher { + private final Matcher matcher; + + public WellKnownBinaryBytesRefMatcher(Matcher matcher) { + this.matcher = matcher; + } + + @Override + public boolean matchesSafely(BytesRef bytesRef) { + return matcher.matches(fromBytesRef(bytesRef)); + } + + @Override + public void describeMismatchSafely(BytesRef bytesRef, Description description) { + matcher.describeMismatch(fromBytesRef(bytesRef), description); + } + + @SuppressWarnings("unchecked") + private G fromBytesRef(BytesRef bytesRef) { + return (G) WellKnownBinary.fromWKB(GeometryValidator.NOOP, false /* coerce */, bytesRef.bytes, bytesRef.offset, bytesRef.length); + } + + @Override + public void describeTo(Description description) { + matcher.describeTo(description); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractAggregationTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractAggregationTestCase.java index df1675ba22568..c086245d6fd61 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractAggregationTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractAggregationTestCase.java @@ -277,9 +277,11 @@ private void evaluate(Expression evaluableExpression) { } private void resolveExpression(Expression expression, Consumer onAggregator, Consumer onEvaluableExpression) { - logger.info( - "Test Values: " + testCase.getData().stream().map(TestCaseSupplier.TypedData::toString).collect(Collectors.joining(",")) - ); + String valuesString = testCase.getData().stream().map(TestCaseSupplier.TypedData::toString).collect(Collectors.joining(",")); + if (valuesString.length() > 200) { + valuesString = valuesString.substring(0, 200) + "..."; + } + logger.info("Test Values: " + valuesString); if (testCase.getExpectedTypeError() != null) { assertTypeResolutionFailure(expression); return; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/MultiRowTestCaseSupplier.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/MultiRowTestCaseSupplier.java index 775ca45bfa124..bb0d2e57c3440 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/MultiRowTestCaseSupplier.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/MultiRowTestCaseSupplier.java @@ -9,9 +9,11 @@ import org.apache.lucene.document.InetAddressPoint; import org.apache.lucene.util.BytesRef; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.network.InetAddresses; import org.elasticsearch.geo.GeometryTestUtils; import org.elasticsearch.geo.ShapeTestUtils; +import org.elasticsearch.geometry.Geometry; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.versionfield.Version; @@ -19,11 +21,11 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; import static org.elasticsearch.test.ESTestCase.randomBoolean; import static org.elasticsearch.test.ESTestCase.randomList; -import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.CARTESIAN; import static org.elasticsearch.xpack.esql.core.util.SpatialCoordinateTypes.GEO; import static org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier.TypedDataSupplier; @@ -263,9 +265,7 @@ public static List dateCases(int minRows, int maxRows) { } /** - * * Generate cases for {@link DataType#DATE_NANOS}. - * */ public static List dateNanosCases(int minRows, int maxRows) { List cases = new ArrayList<>(); @@ -370,53 +370,58 @@ public static List versionCases(int minRows, int maxRows) { return cases; } - public static List geoPointCases(int minRows, int maxRows, boolean withAltitude) { - List cases = new ArrayList<>(); + public enum IncludingAltitude { + YES, + NO + } - addSuppliers( - cases, + public static List geoPointCases(int minRows, int maxRows, IncludingAltitude withAltitude) { + return spatialCases(minRows, maxRows, withAltitude, "geo_point", DataType.GEO_POINT, GeometryTestUtils::randomPoint); + } + + public static List geoShapeCasesWithoutCircle(int minRows, int maxRows, IncludingAltitude includingAltitude) { + return spatialCases( minRows, maxRows, - "", - DataType.GEO_POINT, - () -> GEO.asWkb(GeometryTestUtils.randomPoint(false)) + includingAltitude, + "geo_shape", + DataType.GEO_SHAPE, + b -> GeometryTestUtils.randomGeometryWithoutCircle(0, b) ); - - if (withAltitude) { - addSuppliers( - cases, - minRows, - maxRows, - "", - DataType.GEO_POINT, - () -> GEO.asWkb(GeometryTestUtils.randomPoint(true)) - ); - } - - return cases; } - public static List cartesianPointCases(int minRows, int maxRows, boolean withAltitude) { - List cases = new ArrayList<>(); - - addSuppliers( - cases, + public static List cartesianShapeCasesWithoutCircle(int minRows, int maxRows, IncludingAltitude includingAltitude) { + return spatialCases( minRows, maxRows, - "", - DataType.CARTESIAN_POINT, - () -> CARTESIAN.asWkb(ShapeTestUtils.randomPoint(false)) + includingAltitude, + "geo_shape", + DataType.CARTESIAN_SHAPE, + b -> ShapeTestUtils.randomGeometryWithoutCircle(0, b) ); + } - if (withAltitude) { - addSuppliers( - cases, - minRows, - maxRows, - "", - DataType.CARTESIAN_POINT, - () -> CARTESIAN.asWkb(ShapeTestUtils.randomPoint(true)) - ); + public static List cartesianPointCases(int minRows, int maxRows, IncludingAltitude includingAltitude) { + return spatialCases(minRows, maxRows, includingAltitude, "cartesian_point", DataType.CARTESIAN_POINT, ShapeTestUtils::randomPoint); + } + + @SuppressWarnings("fallthrough") + private static List spatialCases( + int minRows, + int maxRows, + IncludingAltitude includingAltitude, + String name, + DataType type, + Function gen + ) { + List cases = new ArrayList<>(); + + switch (includingAltitude) { + case YES: + addSuppliers(cases, minRows, maxRows, Strings.format("", name), type, () -> GEO.asWkb(gen.apply(true))); + // Explicit fallthrough: always generate a case without altitude. + case NO: + addSuppliers(cases, minRows, maxRows, Strings.format("", name), type, () -> GEO.asWkb(gen.apply(false))); } return cases; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/CountTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/CountTests.java index 131072acff870..0485714959f63 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/CountTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/CountTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.AbstractAggregationTestCase; import org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier; +import org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier.IncludingAltitude; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; import java.math.BigInteger; @@ -44,8 +45,8 @@ public static Iterable parameters() { MultiRowTestCaseSupplier.booleanCases(1, 1000), MultiRowTestCaseSupplier.ipCases(1, 1000), MultiRowTestCaseSupplier.versionCases(1, 1000), - MultiRowTestCaseSupplier.geoPointCases(1, 1000, true), - MultiRowTestCaseSupplier.cartesianPointCases(1, 1000, true), + MultiRowTestCaseSupplier.geoPointCases(1, 1000, IncludingAltitude.YES), + MultiRowTestCaseSupplier.cartesianPointCases(1, 1000, IncludingAltitude.YES), MultiRowTestCaseSupplier.stringCases(1, 1000, DataType.KEYWORD), MultiRowTestCaseSupplier.stringCases(1, 1000, DataType.TEXT), MultiRowTestCaseSupplier.stringCases(1, 1000, DataType.SEMANTIC_TEXT) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialCentroidTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialCentroidTests.java index 15ea029a05554..b92b32aa7ad09 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialCentroidTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialCentroidTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.xpack.esql.expression.function.AbstractAggregationTestCase; import org.elasticsearch.xpack.esql.expression.function.FunctionName; import org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier; +import org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier.IncludingAltitude; import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; @@ -41,8 +42,8 @@ public SpatialCentroidTests(@Name("TestCase") Supplier parameters() { var suppliers = Stream.of( - MultiRowTestCaseSupplier.geoPointCases(1, 1000, true), - MultiRowTestCaseSupplier.cartesianPointCases(1, 1000, true) + MultiRowTestCaseSupplier.geoPointCases(1, 1000, IncludingAltitude.NO), + MultiRowTestCaseSupplier.cartesianPointCases(1, 1000, IncludingAltitude.NO) ).flatMap(List::stream).map(SpatialCentroidTests::makeSupplier).toList(); // The withNoRowsExpectingNull() cases don't work here, as this aggregator doesn't return nulls. diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialExtentTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialExtentTests.java new file mode 100644 index 0000000000000..a1faa537ba052 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/aggregate/SpatialExtentTests.java @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function.aggregate; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import org.apache.lucene.util.BytesRef; +import org.elasticsearch.compute.aggregation.spatial.PointType; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.utils.GeometryValidator; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; +import org.elasticsearch.geometry.utils.WellKnownBinary; +import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.tree.Source; +import org.elasticsearch.xpack.esql.core.type.DataType; +import org.elasticsearch.xpack.esql.expression.RectangleMatcher; +import org.elasticsearch.xpack.esql.expression.WellKnownBinaryBytesRefMatcher; +import org.elasticsearch.xpack.esql.expression.function.AbstractAggregationTestCase; +import org.elasticsearch.xpack.esql.expression.function.FunctionName; +import org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier; +import org.elasticsearch.xpack.esql.expression.function.MultiRowTestCaseSupplier.IncludingAltitude; +import org.elasticsearch.xpack.esql.expression.function.TestCaseSupplier; + +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; + +@FunctionName("st_extent_agg") +public class SpatialExtentTests extends AbstractAggregationTestCase { + public SpatialExtentTests(@Name("TestCase") Supplier testCaseSupplier) { + this.testCase = testCaseSupplier.get(); + } + + @ParametersFactory + public static Iterable parameters() { + var suppliers = Stream.of( + MultiRowTestCaseSupplier.geoPointCases(1, 1000, IncludingAltitude.NO), + MultiRowTestCaseSupplier.cartesianPointCases(1, 1000, IncludingAltitude.NO), + MultiRowTestCaseSupplier.geoShapeCasesWithoutCircle(1, 1000, IncludingAltitude.NO), + MultiRowTestCaseSupplier.cartesianShapeCasesWithoutCircle(1, 1000, IncludingAltitude.NO) + ).flatMap(List::stream).map(SpatialExtentTests::makeSupplier).toList(); + + // The withNoRowsExpectingNull() cases don't work here, as this aggregator doesn't return nulls. + // return parameterSuppliersFromTypedDataWithDefaultChecks(suppliers); + return parameterSuppliersFromTypedData(randomizeBytesRefsOffset(suppliers)); + } + + @Override + protected Expression build(Source source, List args) { + return new SpatialExtent(source, args.get(0)); + } + + private static TestCaseSupplier makeSupplier(TestCaseSupplier.TypedDataSupplier fieldSupplier) { + return new TestCaseSupplier(List.of(fieldSupplier.type()), () -> { + PointType pointType = switch (fieldSupplier.type()) { + case DataType.CARTESIAN_POINT, DataType.CARTESIAN_SHAPE -> PointType.CARTESIAN; + case DataType.GEO_POINT, DataType.GEO_SHAPE -> PointType.GEO; + default -> throw new IllegalArgumentException("Unsupported type: " + fieldSupplier.type()); + }; + var pointVisitor = switch (pointType) { + case CARTESIAN -> new SpatialEnvelopeVisitor.CartesianPointVisitor(); + case GEO -> new SpatialEnvelopeVisitor.GeoPointVisitor(WrapLongitude.WRAP); + }; + + var fieldTypedData = fieldSupplier.get(); + DataType expectedType = DataType.isSpatialGeo(fieldTypedData.type()) ? DataType.GEO_SHAPE : DataType.CARTESIAN_SHAPE; + fieldTypedData.multiRowData() + .stream() + .map(value -> (BytesRef) value) + .map(value -> WellKnownBinary.fromWKB(GeometryValidator.NOOP, false, value.bytes, value.offset, value.length)) + .forEach(g -> g.visit(new SpatialEnvelopeVisitor(pointVisitor))); + assert pointVisitor.isValid(); + Rectangle result = pointVisitor.getResult(); + return new TestCaseSupplier.TestCase( + List.of(fieldTypedData), + "SpatialExtent[field=Attribute[channel=0]]", + expectedType, + new WellKnownBinaryBytesRefMatcher<>( + RectangleMatcher.closeTo( + new Rectangle( + // Since we use integers locally which are later decoded to doubles, all computation is effectively done using + // floats, not doubles. + (float) result.getMinX(), + (float) result.getMaxX(), + (float) result.getMaxY(), + (float) result.getMinY() + ), + 1e-3, + pointType + ) + ) + ); + }); + } +} diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeTests.java index ac87d45491447..9f629d9127673 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StEnvelopeTests.java @@ -13,6 +13,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; @@ -74,7 +75,9 @@ private static BytesRef valueOf(BytesRef wkb, boolean geo) { if (geometry instanceof Point) { return wkb; } - var envelope = geo ? SpatialEnvelopeVisitor.visitGeo(geometry, true) : SpatialEnvelopeVisitor.visitCartesian(geometry); + var envelope = geo + ? SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.WRAP) + : SpatialEnvelopeVisitor.visitCartesian(geometry); if (envelope.isPresent()) { return UNSPECIFIED.asWkb(envelope.get()); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxTests.java index dc6e61e44f599..9205879fa1cb9 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMaxTests.java @@ -13,6 +13,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; @@ -61,7 +62,9 @@ private static double valueOf(BytesRef wkb, boolean geo) { if (geometry instanceof Point point) { return point.getX(); } - var envelope = geo ? SpatialEnvelopeVisitor.visitGeo(geometry, true) : SpatialEnvelopeVisitor.visitCartesian(geometry); + var envelope = geo + ? SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.WRAP) + : SpatialEnvelopeVisitor.visitCartesian(geometry); if (envelope.isPresent()) { return envelope.get().getMaxX(); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinTests.java index 8c06d18b1e281..3603bff9656fe 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StXMinTests.java @@ -13,6 +13,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; @@ -61,7 +62,9 @@ private static double valueOf(BytesRef wkb, boolean geo) { if (geometry instanceof Point point) { return point.getX(); } - var envelope = geo ? SpatialEnvelopeVisitor.visitGeo(geometry, true) : SpatialEnvelopeVisitor.visitCartesian(geometry); + var envelope = geo + ? SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.WRAP) + : SpatialEnvelopeVisitor.visitCartesian(geometry); if (envelope.isPresent()) { return envelope.get().getMinX(); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxTests.java index 7222d7517f7ff..cb2a03c3a9473 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMaxTests.java @@ -13,6 +13,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; @@ -61,7 +62,9 @@ private static double valueOf(BytesRef wkb, boolean geo) { if (geometry instanceof Point point) { return point.getY(); } - var envelope = geo ? SpatialEnvelopeVisitor.visitGeo(geometry, true) : SpatialEnvelopeVisitor.visitCartesian(geometry); + var envelope = geo + ? SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.WRAP) + : SpatialEnvelopeVisitor.visitCartesian(geometry); if (envelope.isPresent()) { return envelope.get().getMaxY(); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinTests.java index 843c7bb649114..0c191f6dc4c5b 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/spatial/StYMinTests.java @@ -13,6 +13,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.geometry.Point; import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor; +import org.elasticsearch.geometry.utils.SpatialEnvelopeVisitor.WrapLongitude; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; @@ -61,7 +62,9 @@ private static double valueOf(BytesRef wkb, boolean geo) { if (geometry instanceof Point point) { return point.getY(); } - var envelope = geo ? SpatialEnvelopeVisitor.visitGeo(geometry, true) : SpatialEnvelopeVisitor.visitCartesian(geometry); + var envelope = geo + ? SpatialEnvelopeVisitor.visitGeo(geometry, WrapLongitude.WRAP) + : SpatialEnvelopeVisitor.visitCartesian(geometry); if (envelope.isPresent()) { return envelope.get().getMinY(); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java index ec1d55a0fc58f..dc3ae0a3388cb 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/PhysicalPlanOptimizerTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.geometry.Polygon; import org.elasticsearch.geometry.ShapeType; import org.elasticsearch.index.IndexMode; +import org.elasticsearch.index.mapper.MappedFieldType.FieldExtractPreference; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.ExistsQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; @@ -64,6 +65,7 @@ import org.elasticsearch.xpack.esql.expression.function.aggregate.Count; import org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialAggregateFunction; import org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialCentroid; +import org.elasticsearch.xpack.esql.expression.function.aggregate.SpatialExtent; import org.elasticsearch.xpack.esql.expression.function.aggregate.Sum; import org.elasticsearch.xpack.esql.expression.function.fulltext.Match; import org.elasticsearch.xpack.esql.expression.function.scalar.math.Round; @@ -253,7 +255,7 @@ public void init() { "mapping-airports_no_doc_values.json", functionRegistry, enrichResolution, - new TestConfigurableSearchStats().exclude(Config.DOC_VALUES, "location") + new TestConfigurableSearchStats().exclude(Config.DOC_VALUES, "location").exclude(Config.DOC_VALUES, "city_location") ); this.airportsNotIndexed = makeTestDataSource( "airports-not-indexed", @@ -2804,7 +2806,7 @@ public void testPartialAggFoldingOutputForSyntheticAgg() { * Also note that the type converting function is removed when it does not actually convert the type, * ensuring that ReferenceAttributes are not created for the same field, and the optimization can still work. */ - public void testSpatialTypesAndStatsUseDocValues() { + public void testSpatialTypesAndStatsCentroidUseDocValues() { for (String query : new String[] { "from airports | stats centroid = st_centroid_agg(location)", "from airports | stats centroid = st_centroid_agg(to_geopoint(location))", @@ -2838,6 +2840,129 @@ public void testSpatialTypesAndStatsUseDocValues() { } } + /** + * Before local optimizations: + * + * LimitExec[1000[INTEGER]] + * \_AggregateExec[[],[SPATIALSTEXTENT(location{f}#48,true[BOOLEAN]) AS extent],FINAL,[minNegX{r}#52, minPosX{r}#53, maxNegX{r}#54, + * maxPosX{r}#55, maxY{r}#56, minY{r}#57],null] + * \_ExchangeExec[[minNegX{r}#52, minPosX{r}#53, maxNegX{r}#54, maxPosX{r}#55, maxY{r}#56, minY{r}#57],true] + * \_FragmentExec[filter=null, estimatedRowSize=0, reducer=[], fragment=[ + * Aggregate[STANDARD,[],[SPATIALSTEXTENT(location{f}#48,true[BOOLEAN]) AS extent]] + * \_EsRelation[airports][abbrev{f}#44, city{f}#50, city_location{f}#51, coun..]]] + * + * After local optimizations: + * + * LimitExec[1000[INTEGER]] + * \_AggregateExec[[],[SPATIALSTEXTENT(location{f}#48,true[BOOLEAN]) AS extent],FINAL,[minNegX{r}#52, minPosX{r}#53, maxNegX{r}#54, + * maxPosX{r}#55, maxY{r}#56, minY{r}#57],21] + * \_ExchangeExec[[minNegX{r}#52, minPosX{r}#53, maxNegX{r}#54, maxPosX{r}#55, maxY{r}#56, minY{r}#57],true] + * \_AggregateExec[[],[SPATIALSTEXTENT(location{f}#48,true[BOOLEAN]) AS extent],INITIAL,[ + * minNegX{r}#73, minPosX{r}#74, maxNegX{rb#75, maxPosX{r}#76, maxY{r}#77, minY{r}#78],21] + * \_FieldExtractExec[location{f}#48][location{f}#48] + * \_EsQueryExec[airports], indexMode[standard], query[{"exists":{"field":"location","boost":1.0}}][ + * _doc{f}#79], limit[], sort[] estimatedRowSize[25] + * + * Note the FieldExtractExec has 'location' set for stats: FieldExtractExec[location{f}#9][location{f}#9] + *

    + * Also note that the type converting function is removed when it does not actually convert the type, + * ensuring that ReferenceAttributes are not created for the same field, and the optimization can still work. + */ + public void testSpatialTypesAndStatsExtentUseDocValues() { + for (String query : new String[] { + "from airports | stats extent = st_extent_agg(location)", + "from airports | stats extent = st_extent_agg(to_geopoint(location))", + "from airports | eval location = to_geopoint(location) | stats extent = st_extent_agg(location)" }) { + for (boolean withDocValues : new boolean[] { false, true }) { + var testData = withDocValues ? airports : airportsNoDocValues; + var plan = physicalPlan(query, testData); + + var limit = as(plan, LimitExec.class); + var agg = as(limit.child(), AggregateExec.class); + // Before optimization the aggregation does not use doc-values + assertAggregation(agg, "extent", SpatialExtent.class, GEO_POINT, false); + + var exchange = as(agg.child(), ExchangeExec.class); + var fragment = as(exchange.child(), FragmentExec.class); + var fAgg = as(fragment.fragment(), Aggregate.class); + as(fAgg.child(), EsRelation.class); + + // Now optimize the plan and assert the aggregation uses doc-values + var optimized = optimizedPlan(plan, testData.stats); + limit = as(optimized, LimitExec.class); + agg = as(limit.child(), AggregateExec.class); + // Above the exchange (in coordinator) the aggregation is not using doc-values + assertAggregation(agg, "extent", SpatialExtent.class, GEO_POINT, false); + exchange = as(agg.child(), ExchangeExec.class); + agg = as(exchange.child(), AggregateExec.class); + // below the exchange (in data node) the aggregation is using doc-values + assertAggregation(agg, "extent", SpatialExtent.class, GEO_POINT, withDocValues); + assertChildIsGeoPointExtract(withDocValues ? agg : as(agg.child(), FilterExec.class), withDocValues); + } + } + } + + /** + * Before local optimizations: + * + * LimitExec[1000[INTEGER]] + * \_AggregateExec[[],[SPATIALSTEXTENT(location{f}#48,true[BOOLEAN]) AS extent],FINAL,[minNegX{r}#52, minPosX{r}#53, maxNegX{r}#54, + * maxPosX{r}#55, maxY{r}#56, minY{r}#57],null] + * \_ExchangeExec[[minNegX{r}#52, minPosX{r}#53, maxNegX{r}#54, maxPosX{r}#55, maxY{r}#56, minY{r}#57],true] + * \_FragmentExec[filter=null, estimatedRowSize=0, reducer=[], fragment=[ + * Aggregate[STANDARD,[],[SPATIALSTEXTENT(location{f}#48,true[BOOLEAN]) AS extent]] + * \_EsRelation[airports][abbrev{f}#44, city{f}#50, city_location{f}#51, coun..]]] + * + * After local optimizations: + * + * LimitExec[1000[INTEGER]] + * \_AggregateExec[[],[SPATIALSTEXTENT(location{f}#48,true[BOOLEAN]) AS extent],FINAL,[minNegX{r}#52, minPosX{r}#53, maxNegX{r}#54, + * maxPosX{r}#55, maxY{r}#56, minY{r}#57],21] + * \_ExchangeExec[[minNegX{r}#52, minPosX{r}#53, maxNegX{r}#54, maxPosX{r}#55, maxY{r}#56, minY{r}#57],true] + * \_AggregateExec[[],[SPATIALSTEXTENT(location{f}#48,true[BOOLEAN]) AS extent],INITIAL,[ + * minNegX{r}#73, minPosX{r}#74, maxNegX{rb#75, maxPosX{r}#76, maxY{r}#77, minY{r}#78],21] + * \_FieldExtractExec[location{f}#48][location{f}#48] + * \_EsQueryExec[airports], indexMode[standard], query[{"exists":{"field":"location","boost":1.0}}][ + * _doc{f}#79], limit[], sort[] estimatedRowSize[25] + * + * Note the FieldExtractExec has 'location' set for stats: FieldExtractExec[location{f}#9][location{f}#9] + *

    + * Also note that the type converting function is removed when it does not actually convert the type, + * ensuring that ReferenceAttributes are not created for the same field, and the optimization can still work. + */ + public void testSpatialTypesAndStatsExtentAndCentroidUseDocValues() { + for (String query : new String[] { + "from airports | stats extent = st_extent_agg(location), centroid = st_centroid_agg(location)", + "from airports | stats extent = st_extent_agg(location), centroid = st_centroid_agg(city_location)", }) { + for (boolean withDocValues : new boolean[] { false, true }) { + var testData = withDocValues ? airports : airportsNoDocValues; + var plan = physicalPlan(query, testData); + + var limit = as(plan, LimitExec.class); + var agg = as(limit.child(), AggregateExec.class); + // Before optimization the aggregation does not use doc-values + assertAggregation(agg, "extent", SpatialExtent.class, GEO_POINT, false); + + var exchange = as(agg.child(), ExchangeExec.class); + var fragment = as(exchange.child(), FragmentExec.class); + var fAgg = as(fragment.fragment(), Aggregate.class); + as(fAgg.child(), EsRelation.class); + + // Now optimize the plan and assert the aggregation uses doc-values + var optimized = optimizedPlan(plan, testData.stats); + limit = as(optimized, LimitExec.class); + agg = as(limit.child(), AggregateExec.class); + // Above the exchange (in coordinator) the aggregation is not using doc-values + assertAggregation(agg, "extent", SpatialExtent.class, GEO_POINT, false); + exchange = as(agg.child(), ExchangeExec.class); + agg = as(exchange.child(), AggregateExec.class); + // below the exchange (in data node) the aggregation is using doc-values + assertAggregation(agg, "extent", SpatialExtent.class, GEO_POINT, withDocValues); + assertChildIsGeoPointExtract(withDocValues ? agg : as(agg.child(), FilterExec.class), withDocValues); + } + } + } + /** * This test does not have real index fields, and therefor asserts that doc-values field extraction does NOT occur. * Before local optimizations: @@ -6805,7 +6930,11 @@ private static void assertAggregation( var aggFunc = assertAggregation(plan, aliasName, aggClass); var aggField = as(aggFunc.field(), Attribute.class); var spatialAgg = as(aggFunc, SpatialAggregateFunction.class); - assertThat("Expected spatial aggregation to use doc-values", spatialAgg.useDocValues(), equalTo(useDocValues)); + assertThat( + "Expected spatial aggregation to use doc-values", + spatialAgg.fieldExtractPreference(), + equalTo(useDocValues ? FieldExtractPreference.DOC_VALUES : FieldExtractPreference.NONE) + ); assertThat("", aggField.dataType(), equalTo(fieldType)); } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml index 81f65668722fc..2a4cde9a680e9 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/60_usage.yml @@ -92,7 +92,7 @@ setup: - gt: {esql.functions.to_long: $functions_to_long} - match: {esql.functions.coalesce: $functions_coalesce} # Testing for the entire function set isn't feasbile, so we just check that we return the correct count as an approximation. - - length: {esql.functions: 128} # check the "sister" test below for a likely update to the same esql.functions length check + - length: {esql.functions: 129} # check the "sister" test below for a likely update to the same esql.functions length check --- "Basic ESQL usage output (telemetry) non-snapshot version": @@ -163,4 +163,4 @@ setup: - match: {esql.functions.cos: $functions_cos} - gt: {esql.functions.to_long: $functions_to_long} - match: {esql.functions.coalesce: $functions_coalesce} - - length: {esql.functions: 124} # check the "sister" test above for a likely update to the same esql.functions length check + - length: {esql.functions: 125} # check the "sister" test above for a likely update to the same esql.functions length check

  • cI2rsmdBQ3w;x{lH4%KLRn@9&4;g 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