From 797a70483b502ebf37b70ff2fd8c2338772f4ca1 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 30 Nov 2023 07:08:08 +0000 Subject: [PATCH 001/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-964dbe334a8 --- build-tools-internal/version.properties | 2 +- docs/Versions.asciidoc | 4 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 3 files changed, 75 insertions(+), 75 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 575d8310e9e24..6fcd9604a6d0a 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.12.0 -lucene = 9.9.0-snapshot-a6d788e1138 +lucene = 9.10.0-snapshot-964dbe334a8 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/docs/Versions.asciidoc b/docs/Versions.asciidoc index 3f44db9928434..97117e9cbc077 100644 --- a/docs/Versions.asciidoc +++ b/docs/Versions.asciidoc @@ -1,8 +1,8 @@ include::{docs-root}/shared/versions/stack/{source_branch}.asciidoc[] -:lucene_version: 9.9.0 -:lucene_version_path: 9_9_0 +:lucene_version: 9.10.0 +:lucene_version_path: 9_10_0 :jdk: 11.0.2 :jdk_major: 11 :build_type: tar diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d90d60bf701e1..9efbad76f5849 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 83eae8906a7986552654a100313c81874ee40615 Mon Sep 17 00:00:00 2001 From: ChrisHegarty Date: Thu, 30 Nov 2023 08:35:38 +0000 Subject: [PATCH 002/250] Add IndexVersion constant for Lucene 9.10 --- server/src/main/java/org/elasticsearch/index/IndexVersions.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/org/elasticsearch/index/IndexVersions.java b/server/src/main/java/org/elasticsearch/index/IndexVersions.java index 7c99764e44283..52913ddb55600 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexVersions.java +++ b/server/src/main/java/org/elasticsearch/index/IndexVersions.java @@ -92,6 +92,7 @@ private static IndexVersion def(int id, Version luceneVersion) { public static final IndexVersion ES_VERSION_8_12 = def(8_500_004, Version.LUCENE_9_8_0); public static final IndexVersion UPGRADE_TO_LUCENE_9_9 = def(8_500_010, Version.LUCENE_9_9_0); + public static final IndexVersion UPGRADE_TO_LUCENE_9_10 = def(8_500_099, Version.LUCENE_9_10_0); /* * STOP! READ THIS FIRST! No, really, From cb1827c780730072980aa9bc13a475dd1de5d8bf Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 1 Dec 2023 07:11:07 +0000 Subject: [PATCH 003/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-6d6e88f107d --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 6fcd9604a6d0a..f8e8fdfeac93b 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.12.0 -lucene = 9.10.0-snapshot-964dbe334a8 +lucene = 9.10.0-snapshot-6d6e88f107d bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9efbad76f5849..991638c800793 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 3fff61b784a3b87e7fb1071b5ac6f6a09d76e6d2 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sat, 2 Dec 2023 07:10:23 +0000 Subject: [PATCH 004/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-cc9b7394690 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 146 ++++++++++++------------ 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index f8e8fdfeac93b..dd4c45270e740 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.12.0 -lucene = 9.10.0-snapshot-6d6e88f107d +lucene = 9.10.0-snapshot-cc9b7394690 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 991638c800793..622677a05deeb 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1,5 +1,5 @@ - + false false @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From b373a9d6719de5c5db3fe7a67f440b206c1fcff0 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sun, 3 Dec 2023 07:09:24 +0000 Subject: [PATCH 005/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-b45defb3cf9 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index dd4c45270e740..e50f73178ec18 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.12.0 -lucene = 9.10.0-snapshot-cc9b7394690 +lucene = 9.10.0-snapshot-b45defb3cf9 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 622677a05deeb..2b4225c2aafce 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 5987518b5a8edee9b524925fb165b2bbe99d60c1 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 4 Dec 2023 07:09:17 +0000 Subject: [PATCH 006/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-b45defb3cf9 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 2b4225c2aafce..8733526109e61 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 5088dfebef4ed10d12c744d2d00dd066baba2f01 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 5 Dec 2023 07:10:21 +0000 Subject: [PATCH 007/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-b45defb3cf9 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index df98a444ba99c..3c81af572a43c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 8fb1d8527f1e8781703a4ea369476dddfcf4173d Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 6 Dec 2023 07:08:58 +0000 Subject: [PATCH 008/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-5852a0fb8ca --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 5d89e417c79e9..cf6e583c43f09 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.12.0 -lucene = 9.10.0-snapshot-b45defb3cf9 +lucene = 9.10.0-snapshot-5852a0fb8ca bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index b0d33cf9b5a68..d576a6063b009 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 0b9c16dd848c385d15e35fb5b276d6d6ab35bf75 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 7 Dec 2023 07:10:41 +0000 Subject: [PATCH 009/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-5852a0fb8ca --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d576a6063b009..eb394bd119554 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From cb34e41268be5f2271c53c59118886f1e35a311d Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 8 Dec 2023 07:09:20 +0000 Subject: [PATCH 010/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-65c4251718b --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index cf6e583c43f09..9119597019991 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.12.0 -lucene = 9.10.0-snapshot-5852a0fb8ca +lucene = 9.10.0-snapshot-65c4251718b bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index eb394bd119554..30fd984122428 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 1d91cd9e5b6df849754aa5be30c4bf33d98c1c07 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sat, 9 Dec 2023 07:09:48 +0000 Subject: [PATCH 011/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-06002e015dc --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 1ec42875ef793..da08a9b12b53e 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-65c4251718b +lucene = 9.10.0-snapshot-06002e015dc bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 30fd984122428..c8c177821ad5d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From daecdda22d663b2de0703a59b6b660fbac6919bd Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 11 Dec 2023 07:09:20 +0000 Subject: [PATCH 012/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-59c0f8257b9 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index da08a9b12b53e..828703d0a8274 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-06002e015dc +lucene = 9.10.0-snapshot-59c0f8257b9 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index c8c177821ad5d..3d815252b479f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From da5207ef3e597389bf0039f7f5c77ea767a0d2ce Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 12 Dec 2023 07:09:53 +0000 Subject: [PATCH 013/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-7c8d7aef42a --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 828703d0a8274..c58eb124ca5e8 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-59c0f8257b9 +lucene = 9.10.0-snapshot-7c8d7aef42a bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 3d815252b479f..7d8f9fdb5a74b 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From e52eea72fff5299eed1874e9272a514da91d8b62 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Tue, 12 Dec 2023 13:37:07 +0100 Subject: [PATCH 014/250] Remove usages of deprecated createSharedManager methods (#103261) This commit adapts Elasticsearch to not rely on createSharedManager exposed to create collector managers corresponding to TopScoreDocCollector as well as TopFieldCollector. There are some more deprecations to address around the creation of collectors, but this is a good first step. --- .../index/engine/LuceneChangesSnapshot.java | 7 +- .../elasticsearch/search/dfs/DfsPhase.java | 4 +- .../query/QueryPhaseCollectorManager.java | 13 +-- ...PassGroupingCollectorSearchAfterTests.java | 7 +- .../SinglePassGroupingCollectorTests.java | 7 +- .../query/ProfileCollectorManagerTests.java | 5 +- .../query/QueryPhaseCollectorTests.java | 97 +++++++------------ 7 files changed, 51 insertions(+), 89 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java b/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java index 1005f8f486beb..e63d5ef87973b 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java +++ b/server/src/main/java/org/elasticsearch/index/engine/LuceneChangesSnapshot.java @@ -22,7 +22,7 @@ import org.apache.lucene.search.Sort; import org.apache.lucene.search.SortField; import org.apache.lucene.search.TopDocs; -import org.apache.lucene.search.TopFieldCollector; +import org.apache.lucene.search.TopFieldCollectorManager; import org.apache.lucene.util.ArrayUtil; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.lucene.Lucene; @@ -296,14 +296,13 @@ private TopDocs searchOperations(FieldDoc after, boolean accurateTotalHits) thro final Query rangeQuery = rangeQuery(Math.max(fromSeqNo, lastSeenSeqNo), toSeqNo, indexVersionCreated); assert accurateTotalHits == false || after == null : "accurate total hits is required by the first batch only"; final SortField sortBySeqNo = new SortField(SeqNoFieldMapper.NAME, SortField.Type.LONG); - final TopFieldCollector collector = TopFieldCollector.create( + TopFieldCollectorManager topFieldCollectorManager = new TopFieldCollectorManager( new Sort(sortBySeqNo), searchBatchSize, after, accurateTotalHits ? Integer.MAX_VALUE : 0 ); - indexSearcher.search(rangeQuery, collector); - return collector.topDocs(); + return indexSearcher.search(rangeQuery, topFieldCollectorManager); } private Translog.Operation readDocAsOp(int docIndex) throws IOException { diff --git a/server/src/main/java/org/elasticsearch/search/dfs/DfsPhase.java b/server/src/main/java/org/elasticsearch/search/dfs/DfsPhase.java index 5d3288408c99b..6ec22f9c11135 100644 --- a/server/src/main/java/org/elasticsearch/search/dfs/DfsPhase.java +++ b/server/src/main/java/org/elasticsearch/search/dfs/DfsPhase.java @@ -17,7 +17,7 @@ import org.apache.lucene.search.ScoreMode; import org.apache.lucene.search.TermStatistics; import org.apache.lucene.search.TopDocs; -import org.apache.lucene.search.TopScoreDocCollector; +import org.apache.lucene.search.TopScoreDocCollectorManager; import org.elasticsearch.index.query.ParsedQuery; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.search.builder.SearchSourceBuilder; @@ -199,7 +199,7 @@ private static void executeKnnVectorQuery(SearchContext context) throws IOExcept static DfsKnnResults singleKnnSearch(Query knnQuery, int k, Profilers profilers, ContextIndexSearcher searcher, String nestedPath) throws IOException { - CollectorManager topDocsCollectorManager = TopScoreDocCollector.createSharedManager( + CollectorManager topDocsCollectorManager = new TopScoreDocCollectorManager( k, null, Integer.MAX_VALUE diff --git a/server/src/main/java/org/elasticsearch/search/query/QueryPhaseCollectorManager.java b/server/src/main/java/org/elasticsearch/search/query/QueryPhaseCollectorManager.java index 86a01756d247e..7fd09d3ddfdf1 100644 --- a/server/src/main/java/org/elasticsearch/search/query/QueryPhaseCollectorManager.java +++ b/server/src/main/java/org/elasticsearch/search/query/QueryPhaseCollectorManager.java @@ -35,9 +35,9 @@ import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.TopDocsCollector; -import org.apache.lucene.search.TopFieldCollector; +import org.apache.lucene.search.TopFieldCollectorManager; import org.apache.lucene.search.TopFieldDocs; -import org.apache.lucene.search.TopScoreDocCollector; +import org.apache.lucene.search.TopScoreDocCollectorManager; import org.apache.lucene.search.TotalHits; import org.apache.lucene.search.Weight; import org.elasticsearch.action.search.MaxScoreCollector; @@ -413,14 +413,9 @@ private static class WithHits extends QueryPhaseCollectorManager { } } if (sortAndFormats == null) { - this.topDocsManager = TopScoreDocCollector.createSharedManager(numHits, searchAfter, hitCountThreshold); + this.topDocsManager = new TopScoreDocCollectorManager(numHits, searchAfter, hitCountThreshold); } else { - this.topDocsManager = TopFieldCollector.createSharedManager( - sortAndFormats.sort, - numHits, - (FieldDoc) searchAfter, - hitCountThreshold - ); + this.topDocsManager = new TopFieldCollectorManager(sortAndFormats.sort, numHits, (FieldDoc) searchAfter, hitCountThreshold); } } diff --git a/server/src/test/java/org/elasticsearch/lucene/grouping/SinglePassGroupingCollectorSearchAfterTests.java b/server/src/test/java/org/elasticsearch/lucene/grouping/SinglePassGroupingCollectorSearchAfterTests.java index 8ad4593602a25..bec0f83f78674 100644 --- a/server/src/test/java/org/elasticsearch/lucene/grouping/SinglePassGroupingCollectorSearchAfterTests.java +++ b/server/src/test/java/org/elasticsearch/lucene/grouping/SinglePassGroupingCollectorSearchAfterTests.java @@ -18,7 +18,7 @@ import org.apache.lucene.search.Query; import org.apache.lucene.search.Sort; import org.apache.lucene.search.SortField; -import org.apache.lucene.search.TopFieldCollector; +import org.apache.lucene.search.TopFieldCollectorManager; import org.apache.lucene.search.TopFieldDocs; import org.apache.lucene.search.TotalHits; import org.apache.lucene.store.Directory; @@ -107,12 +107,11 @@ private > void assertSearchCollapse( ? SinglePassGroupingCollector.createNumeric("field", fieldType, sort, expectedNumGroups, after) : SinglePassGroupingCollector.createKeyword("field", fieldType, sort, expectedNumGroups, after); - TopFieldCollector topFieldCollector = TopFieldCollector.create(sort, totalHits, after, Integer.MAX_VALUE); + TopFieldCollectorManager topFieldCollectorManager = new TopFieldCollectorManager(sort, totalHits, after, Integer.MAX_VALUE); Query query = new MatchAllDocsQuery(); searcher.search(query, collapsingCollector); - searcher.search(query, topFieldCollector); + TopFieldDocs topDocs = searcher.search(query, topFieldCollectorManager); TopFieldGroups collapseTopFieldDocs = collapsingCollector.getTopGroups(0); - TopFieldDocs topDocs = topFieldCollector.topDocs(); assertEquals(sortField.getField(), collapseTopFieldDocs.field); assertEquals(totalHits, collapseTopFieldDocs.totalHits.value); assertEquals(expectedNumGroups, collapseTopFieldDocs.scoreDocs.length); diff --git a/server/src/test/java/org/elasticsearch/lucene/grouping/SinglePassGroupingCollectorTests.java b/server/src/test/java/org/elasticsearch/lucene/grouping/SinglePassGroupingCollectorTests.java index 8dd7ed9c21896..bb4b3f42fde85 100644 --- a/server/src/test/java/org/elasticsearch/lucene/grouping/SinglePassGroupingCollectorTests.java +++ b/server/src/test/java/org/elasticsearch/lucene/grouping/SinglePassGroupingCollectorTests.java @@ -26,7 +26,7 @@ import org.apache.lucene.search.SortField; import org.apache.lucene.search.SortedNumericSortField; import org.apache.lucene.search.SortedSetSortField; -import org.apache.lucene.search.TopFieldCollector; +import org.apache.lucene.search.TopFieldCollectorManager; import org.apache.lucene.search.TopFieldDocs; import org.apache.lucene.search.TotalHits; import org.apache.lucene.search.Weight; @@ -132,12 +132,11 @@ private > void assertSearchCollapse( ); } - TopFieldCollector topFieldCollector = TopFieldCollector.create(sort, totalHits, Integer.MAX_VALUE); + TopFieldCollectorManager topFieldCollectorManager = new TopFieldCollectorManager(sort, totalHits, Integer.MAX_VALUE); Query query = new MatchAllDocsQuery(); searcher.search(query, collapsingCollector); - searcher.search(query, topFieldCollector); + TopFieldDocs topDocs = searcher.search(query, topFieldCollectorManager); TopFieldGroups collapseTopFieldDocs = collapsingCollector.getTopGroups(0); - TopFieldDocs topDocs = topFieldCollector.topDocs(); assertEquals(collapseField.getField(), collapseTopFieldDocs.field); assertEquals(expectedNumGroups, collapseTopFieldDocs.scoreDocs.length); assertEquals(totalHits, collapseTopFieldDocs.totalHits.value); diff --git a/server/src/test/java/org/elasticsearch/search/profile/query/ProfileCollectorManagerTests.java b/server/src/test/java/org/elasticsearch/search/profile/query/ProfileCollectorManagerTests.java index 5cfe368a9a392..fc8b9706d387a 100644 --- a/server/src/test/java/org/elasticsearch/search/profile/query/ProfileCollectorManagerTests.java +++ b/server/src/test/java/org/elasticsearch/search/profile/query/ProfileCollectorManagerTests.java @@ -19,6 +19,7 @@ import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.TopScoreDocCollector; +import org.apache.lucene.search.TopScoreDocCollectorManager; import org.apache.lucene.store.Directory; import org.apache.lucene.tests.index.RandomIndexWriter; import org.apache.lucene.tests.search.DummyTotalHitCountCollector; @@ -121,12 +122,12 @@ public Integer reduce(Collection collectors) { */ public void testManagerWithSearcher() throws IOException { { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(10, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(10, null, 1000); TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), topDocsManager); assertEquals(numDocs, topDocs.totalHits.value); } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(10, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(10, null, 1000); String profileReason = "profiler_reason"; ProfileCollectorManager profileCollectorManager = new ProfileCollectorManager<>(topDocsManager, profileReason); TopDocs topDocs = searcher.search(new MatchAllDocsQuery(), profileCollectorManager); diff --git a/server/src/test/java/org/elasticsearch/search/query/QueryPhaseCollectorTests.java b/server/src/test/java/org/elasticsearch/search/query/QueryPhaseCollectorTests.java index b466101be07d8..f222e697488d2 100644 --- a/server/src/test/java/org/elasticsearch/search/query/QueryPhaseCollectorTests.java +++ b/server/src/test/java/org/elasticsearch/search/query/QueryPhaseCollectorTests.java @@ -34,6 +34,7 @@ import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.TopScoreDocCollector; +import org.apache.lucene.search.TopScoreDocCollectorManager; import org.apache.lucene.search.Weight; import org.apache.lucene.search.similarities.BM25Similarity; import org.apache.lucene.store.Directory; @@ -108,7 +109,7 @@ public void testNegativeTerminateAfter() { public void testTopDocsOnly() throws IOException { { - CollectorManager topScoreDocManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topScoreDocManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager> manager = createCollectorManager( topScoreDocManager, null, @@ -121,7 +122,7 @@ public void testTopDocsOnly() throws IOException { assertEquals(numDocs, result.topDocs.totalHits.value); } { - CollectorManager topScoreDocManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topScoreDocManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager> manager = createCollectorManager( topScoreDocManager, null, @@ -137,7 +138,7 @@ public void testTopDocsOnly() throws IOException { public void testWithAggs() throws IOException { { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager aggsManager = DummyTotalHitCountCollector.createManager(); CollectorManager> manager = createCollectorManager( topDocsManager, @@ -152,7 +153,7 @@ public void testWithAggs() throws IOException { assertEquals(numDocs, result.aggs.intValue()); } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager aggsManager = DummyTotalHitCountCollector.createManager(); CollectorManager> manager = createCollectorManager( topDocsManager, @@ -170,7 +171,7 @@ public void testWithAggs() throws IOException { public void testPostFilterTopDocsOnly() throws IOException { { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); TermQuery termQuery = new TermQuery(new Term("field2", "value")); Weight filterWeight = termQuery.createWeight(searcher, ScoreMode.TOP_DOCS, 1f); CollectorManager> manager = createCollectorManager( @@ -185,7 +186,7 @@ public void testPostFilterTopDocsOnly() throws IOException { assertEquals(numField2Docs, result.topDocs.totalHits.value); } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); TermQuery termQuery = new TermQuery(new Term("field1", "value")); Weight filterWeight = termQuery.createWeight(searcher, ScoreMode.TOP_DOCS, 1f); CollectorManager> manager = createCollectorManager( @@ -203,7 +204,7 @@ public void testPostFilterTopDocsOnly() throws IOException { public void testPostFilterWithAggs() throws IOException { { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager aggsManager = DummyTotalHitCountCollector.createManager(); TermQuery termQuery = new TermQuery(new Term("field1", "value")); Weight filterWeight = termQuery.createWeight(searcher, ScoreMode.TOP_DOCS, 1f); @@ -220,7 +221,7 @@ public void testPostFilterWithAggs() throws IOException { assertEquals(numDocs, result.aggs.intValue()); } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager aggsManager = DummyTotalHitCountCollector.createManager(); TermQuery termQuery = new TermQuery(new Term("field2", "value")); Weight filterWeight = termQuery.createWeight(searcher, ScoreMode.TOP_DOCS, 1f); @@ -247,18 +248,14 @@ public void testMinScoreTopDocsOnly() throws IOException { .add(new BoostQuery(new TermQuery(new Term("field2", "value")), 200f), BooleanClause.Occur.SHOULD) .build(); { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager( - numField2Docs + 1, - null, - 1000 - ); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(numField2Docs + 1, null, 1000); TopDocs topDocs = searcher.search(booleanQuery, topDocsManager); assertEquals(numDocs, topDocs.totalHits.value); maxScore = topDocs.scoreDocs[0].score; thresholdScore = topDocs.scoreDocs[numField2Docs].score; } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager> manager = createCollectorManager( topDocsManager, null, @@ -271,7 +268,7 @@ public void testMinScoreTopDocsOnly() throws IOException { assertEquals(numField2Docs, result.topDocs.totalHits.value); } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager> manager = createCollectorManager( topDocsManager, null, @@ -284,7 +281,7 @@ public void testMinScoreTopDocsOnly() throws IOException { assertEquals(numDocs, result.topDocs.totalHits.value); } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager> manager = createCollectorManager( topDocsManager, null, @@ -306,18 +303,14 @@ public void testMinScoreWithAggs() throws IOException { .add(new BoostQuery(new TermQuery(new Term("field2", "value")), 200f), BooleanClause.Occur.SHOULD) .build(); { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager( - numField2Docs + 1, - null, - 1000 - ); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(numField2Docs + 1, null, 1000); TopDocs topDocs = searcher.search(booleanQuery, topDocsManager); assertEquals(numDocs, topDocs.totalHits.value); maxScore = topDocs.scoreDocs[0].score; thresholdScore = topDocs.scoreDocs[numField2Docs].score; } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager aggsManager = DummyTotalHitCountCollector.createManager(); CollectorManager> manager = createCollectorManager( topDocsManager, @@ -333,7 +326,7 @@ public void testMinScoreWithAggs() throws IOException { assertEquals(numField2Docs, result.aggs.intValue()); } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager aggsManager = DummyTotalHitCountCollector.createManager(); CollectorManager> manager = createCollectorManager( topDocsManager, @@ -348,7 +341,7 @@ public void testMinScoreWithAggs() throws IOException { assertEquals(numDocs, result.aggs.intValue()); } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager aggsManager = DummyTotalHitCountCollector.createManager(); CollectorManager> manager = createCollectorManager( topDocsManager, @@ -374,18 +367,14 @@ public void testPostFilterAndMinScoreTopDocsOnly() throws IOException { TermQuery termQuery = new TermQuery(new Term("field2", "value")); Weight filterWeight = termQuery.createWeight(searcher, ScoreMode.TOP_DOCS, 1f); { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager( - numField3Docs + 1, - null, - 1000 - ); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(numField3Docs + 1, null, 1000); TopDocs topDocs = searcher.search(booleanQuery, topDocsManager); assertEquals(numDocs, topDocs.totalHits.value); maxScore = topDocs.scoreDocs[0].score; thresholdScore = topDocs.scoreDocs[numField3Docs].score; } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager> manager = createCollectorManager( topDocsManager, filterWeight, @@ -398,7 +387,7 @@ public void testPostFilterAndMinScoreTopDocsOnly() throws IOException { assertEquals(numField2AndField3Docs, result.topDocs.totalHits.value); } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager> manager = createCollectorManager( topDocsManager, filterWeight, @@ -411,7 +400,7 @@ public void testPostFilterAndMinScoreTopDocsOnly() throws IOException { assertEquals(numField2Docs, result.topDocs.totalHits.value); } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager> manager = createCollectorManager( topDocsManager, filterWeight, @@ -435,18 +424,14 @@ public void testPostFilterAndMinScoreWithAggs() throws IOException { TermQuery termQuery = new TermQuery(new Term("field2", "value")); Weight filterWeight = termQuery.createWeight(searcher, ScoreMode.TOP_DOCS, 1f); { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager( - numField3Docs + 1, - null, - 1000 - ); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(numField3Docs + 1, null, 1000); TopDocs topDocs = searcher.search(booleanQuery, topDocsManager); assertEquals(numDocs, topDocs.totalHits.value); maxScore = topDocs.scoreDocs[0].score; thresholdScore = topDocs.scoreDocs[numField3Docs].score; } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager aggsManager = DummyTotalHitCountCollector.createManager(); CollectorManager> manager = createCollectorManager( topDocsManager, @@ -461,7 +446,7 @@ public void testPostFilterAndMinScoreWithAggs() throws IOException { assertEquals(numField3Docs, result.aggs.intValue()); } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager aggsManager = DummyTotalHitCountCollector.createManager(); CollectorManager> manager = createCollectorManager( topDocsManager, @@ -476,7 +461,7 @@ public void testPostFilterAndMinScoreWithAggs() throws IOException { assertEquals(numDocs, result.aggs.intValue()); } { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager aggsManager = DummyTotalHitCountCollector.createManager(); CollectorManager> manager = createCollectorManager( topDocsManager, @@ -635,18 +620,14 @@ public void testTerminateAfterTopDocsOnlyWithMinScore() throws IOException { .add(new BoostQuery(new TermQuery(new Term("field2", "value")), 200f), BooleanClause.Occur.SHOULD) .build(); { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager( - numField2Docs + 1, - null, - 1000 - ); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(numField2Docs + 1, null, 1000); TopDocs topDocs = searcher.search(booleanQuery, topDocsManager); assertEquals(numDocs, topDocs.totalHits.value); maxScore = topDocs.scoreDocs[0].score; } { int terminateAfter = randomIntBetween(1, numField2Docs - 1); - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager> manager = createCollectorManager( topDocsManager, null, @@ -667,18 +648,14 @@ public void testTerminateAfterWithAggsAndMinScore() throws IOException { .add(new BoostQuery(new TermQuery(new Term("field2", "value")), 200f), BooleanClause.Occur.SHOULD) .build(); { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager( - numField2Docs + 1, - null, - 1000 - ); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(numField2Docs + 1, null, 1000); TopDocs topDocs = searcher.search(booleanQuery, topDocsManager); assertEquals(numDocs, topDocs.totalHits.value); maxScore = topDocs.scoreDocs[0].score; } { int terminateAfter = randomIntBetween(1, numField2Docs - 1); - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager aggsManager = DummyTotalHitCountCollector.createManager(); CollectorManager> manager = createCollectorManager( topDocsManager, @@ -703,18 +680,14 @@ public void testTerminateAfterAndPostFilterAndMinScoreTopDocsOnly() throws IOExc TermQuery termQuery = new TermQuery(new Term("field2", "value")); Weight filterWeight = termQuery.createWeight(searcher, ScoreMode.TOP_DOCS, 1f); { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager( - numField3Docs + 1, - null, - 1000 - ); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(numField3Docs + 1, null, 1000); TopDocs topDocs = searcher.search(booleanQuery, topDocsManager); assertEquals(numDocs, topDocs.totalHits.value); maxScore = topDocs.scoreDocs[0].score; } { int terminateAfter = randomIntBetween(1, numField2AndField3Docs - 1); - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager> manager = createCollectorManager( topDocsManager, filterWeight, @@ -737,18 +710,14 @@ public void testTerminateAfterAndPostFilterAndMinScoreWithAggs() throws IOExcept TermQuery termQuery = new TermQuery(new Term("field2", "value")); Weight filterWeight = termQuery.createWeight(searcher, ScoreMode.TOP_DOCS, 1f); { - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager( - numField3Docs + 1, - null, - 1000 - ); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(numField3Docs + 1, null, 1000); TopDocs topDocs = searcher.search(booleanQuery, topDocsManager); assertEquals(numDocs, topDocs.totalHits.value); maxScore = topDocs.scoreDocs[0].score; } { int terminateAfter = randomIntBetween(1, numField2AndField3Docs - 1); - CollectorManager topDocsManager = TopScoreDocCollector.createSharedManager(1, null, 1000); + CollectorManager topDocsManager = new TopScoreDocCollectorManager(1, null, 1000); CollectorManager aggsManager = DummyTotalHitCountCollector.createManager(); CollectorManager> manager = createCollectorManager( topDocsManager, From cf1b5bcc7ab781240b3c95d313385deff3d71e4e Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 13 Dec 2023 07:09:03 +0000 Subject: [PATCH 015/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-4c3b404ba6e --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index c58eb124ca5e8..0784c4dad6d7d 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-7c8d7aef42a +lucene = 9.10.0-snapshot-4c3b404ba6e bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 7d8f9fdb5a74b..f685d3512df29 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 5a3b221f1c88b4396afef4dfa2847f91a72704a4 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 14 Dec 2023 07:09:45 +0000 Subject: [PATCH 016/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-ceec058767b --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 0784c4dad6d7d..fe19f45d2b03e 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-4c3b404ba6e +lucene = 9.10.0-snapshot-ceec058767b bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index f685d3512df29..2897ac03538f7 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 0ecdb1f4240b2b7a1e55ac28ab5772a19682de3b Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 15 Dec 2023 07:10:33 +0000 Subject: [PATCH 017/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-26ba2ff087d --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index fe19f45d2b03e..862139f46907c 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-ceec058767b +lucene = 9.10.0-snapshot-26ba2ff087d bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 2897ac03538f7..7a782f02456ca 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From d0bc479a2ca9924da8afef121a1b671a27fccb35 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sat, 16 Dec 2023 07:09:57 +0000 Subject: [PATCH 018/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-7d62b23ee90 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 862139f46907c..473144d19380d 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-26ba2ff087d +lucene = 9.10.0-snapshot-7d62b23ee90 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 7a782f02456ca..cc00d7e4556fc 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 3745c8ea26421cc7f854c991bf48ecf17b8116a8 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sun, 17 Dec 2023 07:09:30 +0000 Subject: [PATCH 019/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-45ead8fec98 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 473144d19380d..7b88ef2724c6a 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-7d62b23ee90 +lucene = 9.10.0-snapshot-45ead8fec98 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index cc00d7e4556fc..ffb3f947eae3e 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 46fb75796a98ed0a426bea18be148f771d39b294 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 18 Dec 2023 07:10:13 +0000 Subject: [PATCH 020/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-7f75e20788c --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 7b88ef2724c6a..f8decd4481bdc 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-45ead8fec98 +lucene = 9.10.0-snapshot-7f75e20788c bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index ffb3f947eae3e..b5bcde4a6990b 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 04cdf4246171fa4b04faeaa140a8682958556765 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 19 Dec 2023 07:09:39 +0000 Subject: [PATCH 021/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-41794f0957e --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index f8decd4481bdc..f7573ea211664 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-7f75e20788c +lucene = 9.10.0-snapshot-41794f0957e bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index b5bcde4a6990b..010360624d44f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 78034db6228aa7508df904a5dd31213b61d7eae5 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 20 Dec 2023 07:11:01 +0000 Subject: [PATCH 022/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-a63d4599f48 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index f7573ea211664..33d532f71d983 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-41794f0957e +lucene = 9.10.0-snapshot-a63d4599f48 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 010360624d44f..6efcf6c601c1b 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From bb938ca221d0feb24e3f93aa3f6b5a5c982dd6a0 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 21 Dec 2023 07:10:08 +0000 Subject: [PATCH 023/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-db5eeb9fcc3 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 33d532f71d983..17a4f8e7c3e57 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-a63d4599f48 +lucene = 9.10.0-snapshot-db5eeb9fcc3 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6efcf6c601c1b..508066edc7f42 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 5ee60ae2a609aecefcccb60f103e754edc9c3bdd Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 22 Dec 2023 07:09:33 +0000 Subject: [PATCH 024/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-8b392a70e4c --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 17a4f8e7c3e57..5acb74d6d24b8 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-db5eeb9fcc3 +lucene = 9.10.0-snapshot-8b392a70e4c bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 508066edc7f42..55f8bf523c93d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 6a4c1676ac26d879a3160a6f2b99177e6f007d64 Mon Sep 17 00:00:00 2001 From: ChrisHegarty Date: Fri, 22 Dec 2023 12:22:16 +0000 Subject: [PATCH 025/250] Update index version for new format --- server/src/main/java/org/elasticsearch/index/IndexVersions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/index/IndexVersions.java b/server/src/main/java/org/elasticsearch/index/IndexVersions.java index 8e71da9a8a746..19c7fc89829d4 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexVersions.java +++ b/server/src/main/java/org/elasticsearch/index/IndexVersions.java @@ -97,7 +97,7 @@ private static IndexVersion def(int id, Version luceneVersion) { public static final IndexVersion ES_VERSION_8_13 = def(8_500_009, Version.LUCENE_9_9_1); public static final IndexVersion NEW_INDEXVERSION_FORMAT = def(8_501_00_0, Version.LUCENE_9_9_1); - public static final IndexVersion UPGRADE_TO_LUCENE_9_10 = def(8_500_099, Version.LUCENE_9_10_0); + public static final IndexVersion UPGRADE_TO_LUCENE_9_10 = def(8_502_00_0, Version.LUCENE_9_10_0); /* * STOP! READ THIS FIRST! No, really, From c428d7d126a79de3b70eb2f4d4447f9e44c970be Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sat, 23 Dec 2023 07:09:51 +0000 Subject: [PATCH 026/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-8b392a70e4c --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 55f8bf523c93d..2e3605ff70420 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From a5816f4a8862732bc9c805aa6ab09b569b05501e Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sun, 24 Dec 2023 07:09:58 +0000 Subject: [PATCH 027/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-86573e56d3d --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index ca4440bdd531d..814733eb593fe 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-8b392a70e4c +lucene = 9.10.0-snapshot-86573e56d3d bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 2e3605ff70420..7196a46bcb3c6 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 7d63cfa7b4cf7b7aed77f434785d9eafbab189a7 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 25 Dec 2023 07:10:40 +0000 Subject: [PATCH 028/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-86573e56d3d --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 7196a46bcb3c6..f7b1d39d2954c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 3f16b9028105cf4fdfbbc777c40fae260970083f Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 26 Dec 2023 07:08:58 +0000 Subject: [PATCH 029/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-86573e56d3d --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index f7b1d39d2954c..5bc6d4460a629 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From b295cc8b3ce96fb28dada61669649297fbaee378 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 27 Dec 2023 07:09:40 +0000 Subject: [PATCH 030/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-86573e56d3d --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 5bc6d4460a629..28380adb42c53 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 1667aa7647c3ea751e558964283faf17108dfd2a Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 28 Dec 2023 07:10:25 +0000 Subject: [PATCH 031/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-86573e56d3d --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 28380adb42c53..fef0bd4bbc201 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 48caf751663cecd245d98a14fce750ed67c2c084 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 29 Dec 2023 07:09:40 +0000 Subject: [PATCH 032/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-86573e56d3d --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index fef0bd4bbc201..9526b004b40d6 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 855eb73317aa1d69b344c94d7e7b35505b6b6014 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sat, 30 Dec 2023 07:10:06 +0000 Subject: [PATCH 033/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-5c375cad754 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 814733eb593fe..f36c3267bb91b 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-86573e56d3d +lucene = 9.10.0-snapshot-5c375cad754 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9526b004b40d6..dc97d37fa4771 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2659,124 +2659,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 0f3664870c3c727b6ef95b3a39e6e7f3b4bbcfab Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sun, 31 Dec 2023 07:09:23 +0000 Subject: [PATCH 034/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-5c375cad754 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index dc97d37fa4771..4efcb683f5108 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 337bdc0bf6ef8630b523ef4242709c402c97b5fd Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 1 Jan 2024 07:10:07 +0000 Subject: [PATCH 035/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-5c375cad754 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 4efcb683f5108..0e7a77772d760 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 6c92cbedbf0e42652d97b134fd2dcdd9c6e851dc Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 2 Jan 2024 07:09:41 +0000 Subject: [PATCH 036/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-5c375cad754 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 0e7a77772d760..0a1fdf551b967 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From e2cc9ec37aa8d5dfeb316e3d101a743d717032d1 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 3 Jan 2024 07:10:20 +0000 Subject: [PATCH 037/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-5c375cad754 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 0a1fdf551b967..11c341f3d2ad7 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 7272eb04bdcfb1347d5635f4ecd0f8d35022c454 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 4 Jan 2024 07:09:58 +0000 Subject: [PATCH 038/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-5c375cad754 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 11c341f3d2ad7..8ebff23328618 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 1a26b55063e6456aade8c056358200270fb0d230 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 5 Jan 2024 07:09:55 +0000 Subject: [PATCH 039/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-5c375cad754 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8ebff23328618..2dacbcebbdddd 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 32a7675cdde2c009b7fb801b59a95c34b7aa758b Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sat, 6 Jan 2024 07:09:28 +0000 Subject: [PATCH 040/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-5c375cad754 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 2dacbcebbdddd..0c0626c49d859 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2661,122 +2661,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 0b89315d1bb870f8483072d9d9a767bfa6df35e7 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sun, 7 Jan 2024 07:09:43 +0000 Subject: [PATCH 041/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-af7b6ef53d3 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index e890c41500267..156039b1da520 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-5c375cad754 +lucene = 9.10.0-snapshot-af7b6ef53d3 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d07dbe71e0087..fcdf564399bd3 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2664,124 +2664,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 20932c6bdb7e3924dee81bef49dc8bdce02c41c5 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 8 Jan 2024 07:10:00 +0000 Subject: [PATCH 042/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-af7b6ef53d3 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index fcdf564399bd3..24dba259515ee 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2666,122 +2666,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 9c964dd6411338db253fb8eff52e72160cfc6dab Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 9 Jan 2024 07:10:01 +0000 Subject: [PATCH 043/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-8e8fdea7d23 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 156039b1da520..6fa1da661a630 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-af7b6ef53d3 +lucene = 9.10.0-snapshot-8e8fdea7d23 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 24dba259515ee..31c778df0421d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2664,124 +2664,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From ea0bfb2f2cbe4a56089f99864f855ed783ac686b Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 10 Jan 2024 07:10:12 +0000 Subject: [PATCH 044/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-ad525056591 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 6fa1da661a630..bdecd6c85bf7d 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-8e8fdea7d23 +lucene = 9.10.0-snapshot-ad525056591 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 34413a1ef0066..809e8f670edf6 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2664,124 +2664,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 8916e8272d05b4424a1c101eb76c75b3133bb4d2 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 12 Jan 2024 07:10:07 +0000 Subject: [PATCH 045/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-7ad2507c2e5 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index bdecd6c85bf7d..cda0a322f62b4 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-ad525056591 +lucene = 9.10.0-snapshot-7ad2507c2e5 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 809e8f670edf6..e2252375a0bd4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2664,124 +2664,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From a4c0fc0c4dcd956461bb036fbc6c7450d49966c0 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sat, 13 Jan 2024 07:09:46 +0000 Subject: [PATCH 046/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-fb3bcc0f61e --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index cda0a322f62b4..f26f5dc67a30b 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-7ad2507c2e5 +lucene = 9.10.0-snapshot-fb3bcc0f61e bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index e2252375a0bd4..8c0941cfaea99 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2664,124 +2664,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 271e0e2c00688e683b660d3bcd0e004146044f1d Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sun, 14 Jan 2024 07:10:14 +0000 Subject: [PATCH 047/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-fb3bcc0f61e --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8c0941cfaea99..02dc9f21936c9 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2666,122 +2666,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 86adada6f4525af742bc62bfa31608585bc802e3 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 15 Jan 2024 07:11:37 +0000 Subject: [PATCH 048/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-a457b5fd5fc --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index f26f5dc67a30b..6703d8228eb81 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-fb3bcc0f61e +lucene = 9.10.0-snapshot-a457b5fd5fc bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 02dc9f21936c9..2e4649f20a6ad 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2664,124 +2664,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 13edac340674ed6c8241a9ab1bf6f2a7b09eb537 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 16 Jan 2024 07:10:45 +0000 Subject: [PATCH 049/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-f3d625ea06c --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 6703d8228eb81..96d967cda44af 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-a457b5fd5fc +lucene = 9.10.0-snapshot-f3d625ea06c bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 2e4649f20a6ad..4066db57d82d8 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2664,124 +2664,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From daaf34badb17eefe084aba3b08a9bffcedb2db95 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 17 Jan 2024 07:09:53 +0000 Subject: [PATCH 050/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-00e2fe6cacb --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 96d967cda44af..1702cca1f1058 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-f3d625ea06c +lucene = 9.10.0-snapshot-00e2fe6cacb bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 4066db57d82d8..e37d364d258c3 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2664,124 +2664,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From f4b644059f36296f91f3d676e34459a12f0b0969 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 18 Jan 2024 07:09:41 +0000 Subject: [PATCH 051/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-564219a65a9 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 1702cca1f1058..23574d1f94886 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-00e2fe6cacb +lucene = 9.10.0-snapshot-564219a65a9 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.1+12@415e3f918a1f4062a0074a2794853d0d diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index e37d364d258c3..d8a755b3e17da 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2664,124 +2664,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 3b6e8501cf4831b28f10c00d787b2969116f38e0 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sat, 20 Jan 2024 07:11:03 +0000 Subject: [PATCH 052/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-c8980471e12 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index d859efd68d688..5ea7cdf7b4293 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-564219a65a9 +lucene = 9.10.0-snapshot-c8980471e12 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d2d285adf84ab..3b726d67f5629 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 207a8fd40ad409599bcc5b191e5486f2613e1703 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sun, 21 Jan 2024 07:10:24 +0000 Subject: [PATCH 053/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-c8980471e12 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 3b726d67f5629..18295ac643581 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2635,122 +2635,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From b3e04a83845dd25b27313859c3ed3faa1343d7e0 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 22 Jan 2024 07:10:16 +0000 Subject: [PATCH 054/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-c8980471e12 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 18295ac643581..257e929698112 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2635,122 +2635,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 46eb1ee017b590ce32ac937b42b1d7bdb5e1b8d3 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 23 Jan 2024 07:10:45 +0000 Subject: [PATCH 055/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-b951c4c0611 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 5ea7cdf7b4293..bb782201a29a1 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-c8980471e12 +lucene = 9.10.0-snapshot-b951c4c0611 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 257e929698112..0af19eef3ebeb 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From ae600d9bda4ec0e2656d25ebc03acce2ca02db60 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 24 Jan 2024 07:11:07 +0000 Subject: [PATCH 056/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-9ccfc30ddcb --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index bb782201a29a1..6b2b03422c7c7 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-b951c4c0611 +lucene = 9.10.0-snapshot-9ccfc30ddcb bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 0af19eef3ebeb..2d1a6c69df8be 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From a662e092e798972e1196a6cc1c3d57f02082eb7f Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 25 Jan 2024 07:10:11 +0000 Subject: [PATCH 057/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-aabee01500d --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 6b2b03422c7c7..f06c994168225 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-9ccfc30ddcb +lucene = 9.10.0-snapshot-aabee01500d bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 2d1a6c69df8be..45211414d4a2c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 647005f60ef648b056559e30318cb4acac8b2bd3 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Fri, 26 Jan 2024 11:49:00 -0500 Subject: [PATCH 058/250] Fix compilation for new lucene snapshot and FieldInfo ctor (#104818) Fixes compilation due to `parentField` info in `FieldInfo` ctor --- .../elasticsearch/index/engine/TranslogDirectoryReader.java | 3 +++ .../org/elasticsearch/index/mapper/DocumentLeafReader.java | 1 + .../elasticsearch/snapshots/sourceonly/SourceOnlySnapshot.java | 3 ++- .../org/elasticsearch/xpack/lucene/bwc/codecs/BWCCodec.java | 3 ++- .../lucene/bwc/codecs/lucene50/Lucene50FieldInfosFormat.java | 1 + 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/engine/TranslogDirectoryReader.java b/server/src/main/java/org/elasticsearch/index/engine/TranslogDirectoryReader.java index a09810750c66e..ab84166701c59 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/TranslogDirectoryReader.java +++ b/server/src/main/java/org/elasticsearch/index/engine/TranslogDirectoryReader.java @@ -161,6 +161,7 @@ private static class TranslogLeafReader extends LeafReader { 0, VectorEncoding.FLOAT32, VectorSimilarityFunction.EUCLIDEAN, + false, false ); private static final FieldInfo FAKE_ROUTING_FIELD = new FieldInfo( @@ -179,6 +180,7 @@ private static class TranslogLeafReader extends LeafReader { 0, VectorEncoding.FLOAT32, VectorSimilarityFunction.EUCLIDEAN, + false, false ); private static final FieldInfo FAKE_ID_FIELD = new FieldInfo( @@ -197,6 +199,7 @@ private static class TranslogLeafReader extends LeafReader { 0, VectorEncoding.FLOAT32, VectorSimilarityFunction.EUCLIDEAN, + false, false ); private static final Set TRANSLOG_FIELD_NAMES = Set.of(SourceFieldMapper.NAME, RoutingFieldMapper.NAME, IdFieldMapper.NAME); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentLeafReader.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentLeafReader.java index 49934776bc4a3..db90c8f052a5e 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentLeafReader.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentLeafReader.java @@ -291,6 +291,7 @@ private static FieldInfo fieldInfo(String name) { 0, VectorEncoding.FLOAT32, VectorSimilarityFunction.EUCLIDEAN, + false, false ); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/sourceonly/SourceOnlySnapshot.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/sourceonly/SourceOnlySnapshot.java index c332694d93975..093ec031d0b30 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/sourceonly/SourceOnlySnapshot.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/snapshots/sourceonly/SourceOnlySnapshot.java @@ -262,7 +262,8 @@ private SegmentCommitInfo syncSegment( 0, VectorEncoding.FLOAT32, VectorSimilarityFunction.EUCLIDEAN, - fieldInfo.isSoftDeletesField() + fieldInfo.isSoftDeletesField(), + fieldInfo.isParentField() ) ); } diff --git a/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/codecs/BWCCodec.java b/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/codecs/BWCCodec.java index df6fded49e6bb..25b4b685ac50f 100644 --- a/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/codecs/BWCCodec.java +++ b/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/codecs/BWCCodec.java @@ -109,7 +109,8 @@ private static FieldInfos filterFields(FieldInfos fieldInfos) { 0, fieldInfo.getVectorEncoding(), fieldInfo.getVectorSimilarityFunction(), - fieldInfo.isSoftDeletesField() + fieldInfo.isSoftDeletesField(), + fieldInfo.isParentField() ) ); } diff --git a/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/codecs/lucene50/Lucene50FieldInfosFormat.java b/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/codecs/lucene50/Lucene50FieldInfosFormat.java index 9cef274aa753e..83fcb17449100 100644 --- a/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/codecs/lucene50/Lucene50FieldInfosFormat.java +++ b/x-pack/plugin/old-lucene-versions/src/main/java/org/elasticsearch/xpack/lucene/bwc/codecs/lucene50/Lucene50FieldInfosFormat.java @@ -111,6 +111,7 @@ public FieldInfos read(Directory directory, SegmentInfo segmentInfo, String segm 0, VectorEncoding.FLOAT32, VectorSimilarityFunction.EUCLIDEAN, + false, false ); infos[i].checkConsistency(); From b7476f5f2431a6ed4015dcb439c4ea6b5971a0a0 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sat, 27 Jan 2024 07:10:16 +0000 Subject: [PATCH 059/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-42d5806fd69 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index f06c994168225..a573d8a0ac323 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-aabee01500d +lucene = 9.10.0-snapshot-42d5806fd69 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 45211414d4a2c..94cc20797fb89 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 42cad64de845fcac0aa30d2dd6f0db6aa8f16efd Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sun, 28 Jan 2024 07:09:24 +0000 Subject: [PATCH 060/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-42d5806fd69 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 94cc20797fb89..8d730fa2d7990 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2635,122 +2635,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From b9418bd07dd92b359a9cc4a7b570e4e4189c16e8 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 29 Jan 2024 07:09:35 +0000 Subject: [PATCH 061/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-42d5806fd69 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8d730fa2d7990..68ba7a1f56bfc 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2635,122 +2635,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From f4f841400aa5564f2e833d7d5ac9480ac355642e Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 30 Jan 2024 07:10:08 +0000 Subject: [PATCH 062/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-deac9c26512 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index a573d8a0ac323..6744aba490c16 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-42d5806fd69 +lucene = 9.10.0-snapshot-deac9c26512 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 68ba7a1f56bfc..7322d7854eb47 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From e1b74aa88ae25b8a66b2f34e4a1bb90a37d2012b Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 31 Jan 2024 07:09:51 +0000 Subject: [PATCH 063/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-1e36b461474 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 6744aba490c16..9b8d8497b0219 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-deac9c26512 +lucene = 9.10.0-snapshot-1e36b461474 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 7322d7854eb47..bbbf62bb7b252 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 47ca7aeb70935b2104053d605bb5aa5d1a55b0ba Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Wed, 31 Jan 2024 11:46:03 -0500 Subject: [PATCH 064/250] Update/main (#104974) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change release version lookup to an instance method (#104902) * Upgrade to Lucene 9.9.2 (#104753) This commit upgrades to Lucene 9.9.2. * Improve `CANNOT_REBALANCE_CAN_ALLOCATE` explanation (#104904) Clarify that in this situation there is a rebalancing move that would improve the cluster balance, but there's some reason why rebalancing is not happening. Also points at the `can_rebalance_cluster_decisions` as well as the node-by-node decisions since the action needed could be described in either place. * Get from translog fails with large dense_vector (#104700) This change fixes the engine to apply the current codec when retrieving documents from the translog. We need to use the same codec than the main index in order to ensure that all the source data is indexable. The internal codec treats some fields differently than the default one, for instance dense_vectors are limited to 1024 dimensions. This PR ensures that these customizations are applied when indexing document for translog retrieval. Closes #104639 Co-authored-by: Elastic Machine * [Connector Secrets] Add delete API endpoint (#104815) * Add DELETE endpoint for /_connector/_secret/{id} * Add endpoint to write_connector_secrets cluster privilege * Merge Aggregations into InternalAggregations (#104896) This commit merges Aggregations into InternalAggregations in order to remove the unnecessary hierarchy. * [Profiling] Simplify cost calculation (#104816) * [Profiling] Add the number of cores to HostMetadata * Update AWS pricelist (remove cost_factor, add usd_per_hour) * Switch cost calculations from 'cost_factor' to 'usd_per_hour' * Remove superfluous CostEntry.toXContent() * Check for Number type in CostEntry.fromSource() * Add comment * Retry get_from_translog during relocations (#104579) During a promotable relocation, a `get_from_translog` sent by the unpromotable shard to handle a real-time get might encounter `ShardNotFoundException` or `IndexNotFoundException`. In these cases, we should retry. This is just for `GET`. I'll open a second PR for `mGET`. The relevant IT is in the Stateless PR. Relates ES-5727 * indicating fix for 8.12.1 for int8_hnsw (#104912) * Removing the assumption from some tests that the request builder's request() method always returns the same object (#104881) * [DOCS] Adds get setting and update settings asciidoc files to security API index (#104916) * [DOCS] Adds get setting and update settings asciidoc files to security API index. * [DOCS] Fixes references in docs. * Reuse APMMeterService of APMTelemetryProvider (#104906) * Mute more tests that tend to leak searchhits (#104922) * ESQL: Fix SearchStats#count(String) to count values not rows (#104891) SearchStats#count incorrectly counts the number of documents (or rows) in which a document appears instead of the actual number of values. This PR fixes this by looking at the term frequency instead of the doc count. Fix #104795 * Adding request source for cohere (#104926) * Fixing a broken javadoc comment in ReindexDocumentationIT (#104930) This fixes a javadoc comment that was broken by #104881 * Fix enabling / disabling of APM agent "recording" in APMAgentSettings (#104324) * Add `type` parameter support, for sorting, to the Query API Key API (#104625) This adds support for the `type` parameter, for sorting, to the Query API key API. The type for an API Key can currently be either `rest` or `cross_cluster`. This was overlooked in #103695 when support for the `type` parameter was first introduced only for querying. * Apply publish plugin to es-opensaml-security-api project (#104933) * Support `match` for the Query API Key API (#104594) This adds support for the `match` query type to the Query API key Information API. Note that since string values associated to API Keys are mapped as `keywords`, a `match` query with no analyzer parameter is effectively equivalent to a `term` query for such fields (e.g. `name`, `username`, `realm_name`). Relates: #101691 * [Connectors API] Relax strict response parsing for get/list operations (#104909) * Limit concurrent shards per node for ESQL (#104832) Today, we allow ESQL to execute against an unlimited number of shards concurrently on each node. This can lead to cases where we open and hold too many shards, equivalent to opening too many file descriptors or using too much memory for FieldInfos in ValuesSourceReaderOperator. This change limits the number of concurrent shards to 10 per node. This number was chosen based on the _search API, which limits it to 5. Besides the primary reason stated above, this change has other implications: We might execute fewer shards for queries with LIMIT only, leading to scenarios where we execute only some high-priority shards then stop. For now, we don't have a partial reduce at the node level, but if we introduce one in the future, it might not be as efficient as executing all shards at the same time. There are pauses between batches because batches are executed sequentially one by one. However, I believe the performance of queries executing against many shards (after can_match) is less important than resiliency. Closes #103666 * [DOCS] Support for nested functions in ES|QL STATS...BY (#104788) * Document nested expressions for stats * More docs * Apply suggestions from review - count-distinct.asciidoc - Content restructured, moving the section about approximate counts to end of doc. - count.asciidoc - Clarified that omitting the `expression` parameter in `COUNT` is equivalent to `COUNT(*)`, which counts the number of rows. - percentile.asciidoc - Moved the note about `PERCENTILE` being approximate and non-deterministic to end of doc. - stats.asciidoc - Clarified the `STATS` command - Added a note indicating that individual `null` values are skipped during aggregation * Comment out mentioning a buggy behavior * Update sum with inline function example, update test file * Fix typo * Delete line * Simplify wording * Fix conflict fix typo --------- Co-authored-by: Liam Thompson Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> * [ML] Passing input type through to cohere request (#104781) * Pushing input type through to cohere request * switching logic to allow request to always override * Fixing failure * Removing getModelId calls * Addressing feedback * Switching to enumset * [Transform] Unmute 2 remaining continuous tests: HistogramGroupByIT and TermsGroupByIT (#104898) * Adding ActionRequestLazyBuilder implementation of RequestBuilder (#104927) This introduces a second implementation of RequestBuilder (#104778). As opposed to ActionRequestBuilder, ActionRequestLazyBuilder does not create its request until the request() method is called, and does not hold onto that request (so each call to request() gets a new request instance). This PR also updates BulkRequestBuilder to inherit from ActionRequestLazyBuilder as an example of its use. * Update versions to skip after backport to 8.12 (#104953) * Update/Cleanup references to old tracing.apm.* legacy settings in favor of the telemetry.* settings (#104917) * Exclude tests that do not work in a mixed cluster scenario (#104935) * ES|QL: Improve type validation in aggs for UNSIGNED_LONG and better support for VERSION (#104911) * [Connector API] Make update configuration action non-additive (#104615) * Save allocating enum values array in two hot spots (#104952) Our readEnum code instantiates/clones enum value arrays on read. Normally, this doesn't matter much but the two spots adjusted here are visibly hot during bulk indexing, causing GBs of allocations during e.g. the http_logs indexing run. * ESQL: Correct out-of-range filter pushdowns (#99961) Fix pushed down filters for binary comparisons that compare a byte/short/int/long with an out of range value, like WHERE some_int_field < 1E300. * [DOCS] Dense vector element type should be float for OpenAI (#104966) * Fix test assertions (#104963) * Move functions that generate lucene geometries under a utility class (#104928) We have functions that generate lucene geometries scattered in different places of the code. This commit moves everything under a utility class. * fixing index versions --------- Co-authored-by: Simon Cooper Co-authored-by: Chris Hegarty <62058229+ChrisHegarty@users.noreply.github.com> Co-authored-by: David Turner Co-authored-by: Jim Ferenczi Co-authored-by: Elastic Machine Co-authored-by: Navarone Feekery <13634519+navarone-feekery@users.noreply.github.com> Co-authored-by: Ignacio Vera Co-authored-by: Tim Rühsen Co-authored-by: Pooya Salehi Co-authored-by: Keith Massey Co-authored-by: István Zoltán Szabó Co-authored-by: Moritz Mack Co-authored-by: Costin Leau Co-authored-by: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Co-authored-by: Albert Zaharovits Co-authored-by: Mark Vieira Co-authored-by: Jedr Blaszyk Co-authored-by: Nhat Nguyen Co-authored-by: Abdon Pijpelink Co-authored-by: Liam Thompson Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Co-authored-by: Przemysław Witek Co-authored-by: Joe Gallo Co-authored-by: Lorenzo Dematté Co-authored-by: Luigi Dell'Aquila Co-authored-by: Armin Braun Co-authored-by: Alexander Spies Co-authored-by: David Kyle --- TRACING.md | 12 +- .../gradle/testclusters/RunTask.java | 15 +- .../server/cli/APMJvmOptionsTests.java | 2 - docs/changelog/104594.yaml | 5 + docs/changelog/104625.yaml | 6 + docs/changelog/104753.yaml | 5 + docs/changelog/104832.yaml | 6 + docs/changelog/104891.yaml | 6 + docs/changelog/104904.yaml | 5 + docs/changelog/104909.yaml | 5 + docs/changelog/104911.yaml | 7 + docs/changelog/104927.yaml | 5 + docs/changelog/99961.yaml | 6 + docs/reference/esql/functions/avg.asciidoc | 19 +- .../esql/functions/count-distinct.asciidoc | 64 ++- docs/reference/esql/functions/count.asciidoc | 20 +- docs/reference/esql/functions/max.asciidoc | 21 +- .../median-absolute-deviation.asciidoc | 20 +- docs/reference/esql/functions/median.asciidoc | 19 +- docs/reference/esql/functions/min.asciidoc | 21 +- .../esql/functions/percentile.asciidoc | 43 +- docs/reference/esql/functions/sum.asciidoc | 21 +- .../esql/processing-commands/stats.asciidoc | 48 +- docs/reference/release-notes/8.12.0.asciidoc | 2 + docs/reference/rest-api/security.asciidoc | 2 + .../rest-api/security/get-settings.asciidoc | 11 +- .../rest-api/security/query-api-key.asciidoc | 14 +- .../security/update-settings.asciidoc | 26 +- .../semantic-search-inference.asciidoc | 64 +-- .../elasticsearch/xcontent/XContentType.java | 8 +- .../bucket/TimeSeriesAggregationsIT.java | 14 +- .../DerivativePipelineAggregator.java | 1 - modules/apm/METERING.md | 2 +- .../org/elasticsearch/telemetry/apm/APM.java | 8 +- .../apm/internal/APMAgentSettings.java | 31 +- .../apm/internal/APMMeterService.java | 2 +- .../apm/internal/APMTelemetryProvider.java | 6 +- .../apm/internal/APMAgentSettingsTests.java | 200 ++++++-- .../test/ingest/220_drop_processor.yml | 8 +- .../rest-api-spec/test/ingest/60_fail.yml | 4 +- .../index/rankeval/RatedRequestsTests.java | 4 + .../documentation/ReindexDocumentationIT.java | 3 +- .../elasticsearch/reindex/CancelTests.java | 52 +- .../reindex/ReindexSingleNodeTests.java | 2 +- qa/apm/docker-compose.yml | 10 +- qa/mixed-cluster/build.gradle | 14 +- .../api/connector_secret.delete.json | 28 ++ .../action/search/TransportSearchIT.java | 6 +- .../action/termvectors/GetTermVectorsIT.java | 2 +- .../AggregationsIntegrationIT.java | 2 +- .../search/aggregations/CombiIT.java | 2 +- .../search/aggregations/MetadataIT.java | 4 +- .../SignificantTermsSignificanceScoreIT.java | 10 +- .../metrics/ScriptedMetricIT.java | 4 +- ...ketMetricsPipeLineAggregationTestCase.java | 4 +- .../pipeline/ExtendedStatsBucketIT.java | 6 +- .../pipeline/PercentilesBucketIT.java | 6 +- .../aggregations/pipeline/StatsBucketIT.java | 6 +- .../search/geo/GeoDistanceIT.java | 4 +- .../org/elasticsearch/TransportVersion.java | 8 + .../org/elasticsearch/TransportVersions.java | 7 +- .../action/ActionRequestLazyBuilder.java | 61 +++ .../action/bulk/BulkRequestBuilder.java | 149 +++++- .../action/get/TransportGetAction.java | 129 +++-- .../get/TransportGetFromTranslogAction.java | 1 - .../action/index/IndexRequest.java | 3 +- .../action/search/SearchResponse.java | 17 +- .../action/search/SearchResponseMerger.java | 2 +- .../action/search/SearchResponseSections.java | 8 +- .../action/support/WriteRequest.java | 4 +- .../routing/allocation/Explanations.java | 8 +- .../common/geo/LuceneGeometriesUtils.java | 449 +++++++++++++++++ .../org/elasticsearch/index/IndexVersion.java | 8 + .../elasticsearch/index/IndexVersions.java | 11 +- .../index/engine/InternalEngine.java | 2 +- .../index/engine/TranslogDirectoryReader.java | 16 +- .../index/mapper/GeoShapeIndexer.java | 17 +- .../index/mapper/GeoShapeQueryable.java | 185 +------ .../inference/InferenceService.java | 8 +- .../elasticsearch/inference/InputType.java | 5 +- .../elasticsearch/node/NodeConstruction.java | 2 +- .../elasticsearch/search/SearchService.java | 2 +- .../search/aggregations/Aggregations.java | 145 ------ .../aggregations/InternalAggregations.java | 138 ++++- .../InternalMultiBucketAggregation.java | 2 +- .../sampler/random/InternalRandomSampler.java | 4 +- .../BucketMetricsPipelineAggregator.java | 4 +- .../BucketScriptPipelineAggregator.java | 1 - .../CumulativeSumPipelineAggregator.java | 1 - .../SerialDiffPipelineAggregator.java | 1 - .../pipeline/SiblingPipelineAggregator.java | 3 +- .../action/bulk/BulkRequestBuilderTests.java | 37 ++ .../index/IndexRequestBuilderTests.java | 5 + .../geo/LuceneGeometriesUtilsTests.java | 476 ++++++++++++++++++ .../index/shard/ShardGetServiceTests.java | 66 ++- .../HierarchyCircuitBreakerServiceTests.java | 7 +- .../search/SearchServiceTests.java | 2 +- .../InternalAggregationsTests.java | 6 +- .../terms/RareTermsAggregatorTests.java | 10 +- .../pipeline/AvgBucketAggregatorTests.java | 11 +- .../search/query/QuerySearchResultTests.java | 6 +- .../SharedSignificantTermsTestMethods.java | 4 +- .../test/apmintegration/MetricsApmIT.java | 4 +- .../test/apmintegration/TracesApmIT.java | 6 +- .../search/MockSearchService.java | 15 + .../es-opensaml-security-api/build.gradle | 1 + ...mulativeCardinalityPipelineAggregator.java | 1 - .../MovingPercentilesPipelineAggregator.java | 12 +- .../NormalizePipelineAggregator.java | 1 - .../inference/action/InferenceAction.java | 26 +- .../evaluation/EvaluationMetric.java | 4 +- .../evaluation/classification/Accuracy.java | 4 +- .../evaluation/classification/AucRoc.java | 4 +- .../MulticlassConfusionMatrix.java | 4 +- .../evaluation/classification/Precision.java | 4 +- .../evaluation/classification/Recall.java | 4 +- .../AbstractConfusionMatrixMetric.java | 6 +- .../evaluation/outlierdetection/AucRoc.java | 4 +- .../outlierdetection/ConfusionMatrix.java | 4 +- .../outlierdetection/Precision.java | 4 +- .../evaluation/outlierdetection/Recall.java | 4 +- .../evaluation/regression/Huber.java | 4 +- .../regression/MeanSquaredError.java | 4 +- .../MeanSquaredLogarithmicError.java | 4 +- .../evaluation/regression/RSquared.java | 4 +- .../privilege/ClusterPrivilegeResolver.java | 2 +- .../evaluation/MockAggregations.java | 17 +- .../classification/AccuracyTests.java | 3 +- .../classification/ClassificationTests.java | 6 +- .../MulticlassConfusionMatrixTests.java | 9 +- .../classification/PrecisionTests.java | 14 +- .../classification/RecallTests.java | 14 +- .../ConfusionMatrixTests.java | 4 +- .../outlierdetection/PrecisionTests.java | 8 +- .../outlierdetection/RecallTests.java | 8 +- .../evaluation/regression/HuberTests.java | 8 +- .../regression/MeanSquaredErrorTests.java | 8 +- .../MeanSquaredLogarithmicErrorTests.java | 8 +- .../evaluation/regression/RSquaredTests.java | 16 +- .../DownsampleActionSingleNodeTests.java | 24 +- .../335_connector_update_configuration.yml | 30 +- .../entsearch/510_connector_secret_get.yml | 2 +- .../entsearch/520_connector_secret_delete.yml | 71 +++ .../xpack/application/EnterpriseSearch.java | 8 +- .../application/connector/Connector.java | 52 +- .../connector/ConnectorIndexService.java | 70 ++- .../connector/ConnectorSearchResult.java | 51 ++ .../connector/ConnectorsAPISearchResult.java | 89 ++++ .../connector/action/GetConnectorAction.java | 12 +- .../connector/action/ListConnectorAction.java | 10 +- .../secrets/ConnectorSecretsIndexService.java | 17 + .../action/DeleteConnectorSecretAction.java | 19 + .../action/DeleteConnectorSecretRequest.java | 67 +++ .../action/DeleteConnectorSecretResponse.java | 60 +++ .../RestDeleteConnectorSecretAction.java | 42 ++ .../TransportDeleteConnectorSecretAction.java | 41 ++ .../connector/syncjob/ConnectorSyncJob.java | 128 ++--- .../syncjob/ConnectorSyncJobIndexService.java | 30 +- .../syncjob/ConnectorSyncJobSearchResult.java | 52 ++ .../action/GetConnectorSyncJobAction.java | 8 +- .../action/ListConnectorSyncJobsAction.java | 7 +- .../connector/ConnectorIndexServiceTests.java | 79 ++- .../connector/ConnectorTestUtils.java | 28 ++ ...ctorActionResponseBWCSerializingTests.java | 16 +- ...ctorActionResponseBWCSerializingTests.java | 5 +- .../ConnectorSecretsIndexServiceTests.java | 41 ++ .../secrets/ConnectorSecretsTestUtils.java | 11 + .../DeleteConnectorSecretActionTests.java | 34 ++ ...ectorSecretRequestBWCSerializingTests.java | 37 ++ ...ctorSecretResponseBWCSerializingTests.java | 46 ++ ...sportDeleteConnectorSecretActionTests.java | 72 +++ .../ConnectorSyncJobIndexServiceTests.java | 50 +- .../syncjob/ConnectorSyncJobTestUtils.java | 31 +- .../syncjob/ConnectorSyncJobTests.java | 9 +- ...JobsActionResponseBWCSerializingTests.java | 2 +- .../eql/execution/search/RuntimeUtils.java | 4 +- .../execution/sample/CircuitBreakerTests.java | 4 +- .../exchange/ExchangeSinkHandler.java | 5 +- x-pack/plugin/esql/qa/server/build.gradle | 2 + .../xpack/esql/qa/rest/RestEsqlTestCase.java | 192 ++++++- .../src/main/resources/mapping-all-types.json | 61 +++ .../src/main/resources/show.csv-spec | 32 +- .../src/main/resources/stats.csv-spec | 129 +++++ .../resources/stats_count_distinct.csv-spec | 12 + .../main/resources/stats_percentile.csv-spec | 39 ++ .../action/AbstractEsqlIntegTestCase.java | 3 + .../xpack/esql/action/ManyShardsIT.java | 75 ++- .../xpack/esql/action/WarningsIT.java | 13 +- .../expression/function/aggregate/Avg.java | 12 +- .../function/aggregate/CountDistinct.java | 30 +- .../expression/function/aggregate/Max.java | 8 +- .../expression/function/aggregate/Median.java | 14 +- .../aggregate/MedianAbsoluteDeviation.java | 7 +- .../expression/function/aggregate/Min.java | 8 +- .../function/aggregate/NumericAggregate.java | 16 +- .../function/aggregate/Percentile.java | 14 +- .../expression/function/aggregate/Sum.java | 2 +- .../xpack/esql/planner/AggregateMapper.java | 21 +- .../esql/planner/EsqlTranslatorHandler.java | 121 +++++ .../xpack/esql/plugin/ComputeService.java | 125 +++-- .../xpack/esql/plugin/QueryPragmas.java | 10 + .../xpack/esql/stats/SearchStats.java | 4 +- .../xpack/esql/analysis/AnalyzerTests.java | 42 ++ .../xpack/esql/analysis/VerifierTests.java | 4 +- .../LocalPhysicalPlanOptimizerTests.java | 153 +++++- .../mock/TestInferenceServiceExtension.java | 5 +- .../action/TransportInferenceAction.java | 1 + .../action/cohere/CohereActionCreator.java | 5 +- .../action/cohere/CohereActionVisitor.java | 3 +- .../action/openai/OpenAiActionCreator.java | 2 +- .../cohere/CohereEmbeddingsRequest.java | 1 + .../cohere/CohereEmbeddingsRequestEntity.java | 30 +- .../external/request/cohere/CohereUtils.java | 9 + .../inference/services/SenderService.java | 12 +- .../inference/services/ServiceUtils.java | 38 +- .../services/cohere/CohereModel.java | 3 +- .../services/cohere/CohereService.java | 6 +- .../embeddings/CohereEmbeddingsModel.java | 19 +- .../CohereEmbeddingsServiceSettings.java | 3 +- .../CohereEmbeddingsTaskSettings.java | 104 +++- .../services/elser/ElserMlNodeService.java | 9 +- .../huggingface/HuggingFaceBaseService.java | 2 + .../services/openai/OpenAiService.java | 2 + .../embeddings/OpenAiEmbeddingsModel.java | 18 +- .../OpenAiEmbeddingsTaskSettings.java | 24 +- .../xpack/inference/InputTypeTests.java | 21 + .../action/InferenceActionRequestTests.java | 82 ++- .../cohere/CohereActionCreatorTests.java | 2 +- .../cohere/CohereEmbeddingsActionTests.java | 9 + .../CohereEmbeddingsRequestEntityTests.java | 5 + .../cohere/CohereEmbeddingsRequestTests.java | 16 + .../services/SenderServiceTests.java | 2 + .../inference/services/ServiceUtilsTests.java | 49 +- .../services/cohere/CohereServiceTests.java | 194 ++++++- .../CohereEmbeddingsModelTests.java | 141 +++++- .../CohereEmbeddingsTaskSettingsTests.java | 60 ++- .../HuggingFaceBaseServiceTests.java | 3 +- .../huggingface/HuggingFaceServiceTests.java | 5 +- .../services/openai/OpenAiServiceTests.java | 7 +- .../OpenAiEmbeddingsModelTests.java | 6 +- .../OpenAiEmbeddingsTaskSettingsTests.java | 6 +- .../TransportGetOverallBucketsAction.java | 4 +- .../xpack/ml/aggs/MlAggsHelper.java | 8 +- .../changepoint/ChangePointAggregator.java | 4 +- .../BucketCorrelationAggregator.java | 4 +- .../InferencePipelineAggregator.java | 8 +- .../kstest/BucketCountKSTestAggregator.java | 4 +- .../AbstractAggregationDataExtractor.java | 13 +- .../AggregationToJsonProcessor.java | 15 +- .../CompositeAggregationDataExtractor.java | 10 +- .../chunked/ChunkedDataExtractor.java | 6 +- .../ExtractedFieldsDetectorFactory.java | 4 +- .../TrainTestSplitterFactory.java | 4 +- .../job/persistence/JobResultsProvider.java | 12 +- .../OverallBucketsProvider.java | 4 +- .../xpack/profiling/CostCalculator.java | 4 +- .../xpack/profiling/CostEntry.java | 29 +- .../xpack/profiling/HostMetadata.java | 12 +- .../main/resources/profiling-costs.json.gz | Bin 59986 -> 72998 bytes .../xpack/profiling/CO2CalculatorTests.java | 12 +- .../xpack/profiling/CostCalculatorTests.java | 6 +- .../rollup/RollupResponseTranslator.java | 10 +- .../xpack/rollup/job/IndexerUtils.java | 6 +- .../RollupResponseTranslationTests.java | 19 +- .../rollup/action/SearchActionTests.java | 11 +- .../xpack/security/operator/Constants.java | 1 + .../xpack/security/QueryApiKeyIT.java | 11 + .../xpack/security/apikey/ApiKeyRestIT.java | 52 ++ .../apikey/TransportQueryApiKeyAction.java | 25 +- .../support/ApiKeyBoolQueryBuilder.java | 49 +- .../TransportQueryApiKeyActionTests.java | 30 +- .../service/ElasticServiceAccountsTests.java | 1 + .../support/ApiKeyBoolQueryBuilderTests.java | 213 +++++++- .../xpack/spatial/common/ShapeUtils.java | 68 --- .../index/mapper/CartesianShapeIndexer.java | 15 +- .../index/query/ShapeQueryPointProcessor.java | 153 +----- .../index/query/ShapeQueryProcessor.java | 148 +----- .../CartesianShapeDocValuesQueryTests.java | 10 +- .../xpack/sql/execution/search/Querier.java | 9 +- .../continuous/HistogramGroupByIT.java | 2 - .../continuous/TermsGroupByIT.java | 2 - .../TransformUsageTransportAction.java | 4 +- .../common/AbstractCompositeAggFunction.java | 6 +- .../CompositeBucketsChangeCollector.java | 16 +- .../vectortile/rest/RestVectorTileAction.java | 6 +- .../HistoryTemplateEmailMappingsTests.java | 4 +- .../HistoryTemplateHttpMappingsTests.java | 4 +- ...storyTemplateIndexActionMappingsTests.java | 4 +- ...storyTemplateSearchInputMappingsTests.java | 4 +- 289 files changed, 5805 insertions(+), 1895 deletions(-) create mode 100644 docs/changelog/104594.yaml create mode 100644 docs/changelog/104625.yaml create mode 100644 docs/changelog/104753.yaml create mode 100644 docs/changelog/104832.yaml create mode 100644 docs/changelog/104891.yaml create mode 100644 docs/changelog/104904.yaml create mode 100644 docs/changelog/104909.yaml create mode 100644 docs/changelog/104911.yaml create mode 100644 docs/changelog/104927.yaml create mode 100644 docs/changelog/99961.yaml create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/api/connector_secret.delete.json create mode 100644 server/src/main/java/org/elasticsearch/action/ActionRequestLazyBuilder.java create mode 100644 server/src/main/java/org/elasticsearch/common/geo/LuceneGeometriesUtils.java delete mode 100644 server/src/main/java/org/elasticsearch/search/aggregations/Aggregations.java create mode 100644 server/src/test/java/org/elasticsearch/action/bulk/BulkRequestBuilderTests.java create mode 100644 server/src/test/java/org/elasticsearch/common/geo/LuceneGeometriesUtilsTests.java create mode 100644 x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/520_connector_secret_delete.yml create mode 100644 x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorSearchResult.java create mode 100644 x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorsAPISearchResult.java create mode 100644 x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretAction.java create mode 100644 x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretRequest.java create mode 100644 x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretResponse.java create mode 100644 x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/RestDeleteConnectorSecretAction.java create mode 100644 x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/TransportDeleteConnectorSecretAction.java create mode 100644 x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobSearchResult.java create mode 100644 x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretActionTests.java create mode 100644 x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretRequestBWCSerializingTests.java create mode 100644 x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretResponseBWCSerializingTests.java create mode 100644 x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/TransportDeleteConnectorSecretActionTests.java create mode 100644 x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-all-types.json create mode 100644 x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InputTypeTests.java delete mode 100644 x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/ShapeUtils.java diff --git a/TRACING.md b/TRACING.md index 82f9b0f52fd8b..5c754e383c8e5 100644 --- a/TRACING.md +++ b/TRACING.md @@ -23,18 +23,18 @@ You must supply configuration and credentials for the APM server (see below). In your `elasticsearch.yml` add the following configuration: ``` -tracing.apm.enabled: true +telemetry.tracing.enabled: true telemetry.agent.server_url: https://:443 ``` -When using a secret token to authenticate with the APM server, you must add it to the Elasticsearch keystore under `tracing.apm.secret_token`. For example, execute: +When using a secret token to authenticate with the APM server, you must add it to the Elasticsearch keystore under `telemetry.secret_token`. For example, execute: - bin/elasticsearch-keystore add tracing.apm.secret_token + bin/elasticsearch-keystore add telemetry.secret_token -then enter the token when prompted. If you are using API keys, change the keystore key name to `tracing.apm.api_key`. +then enter the token when prompted. If you are using API keys, change the keystore key name to `telemetry.api_key`. -All APM settings live under `tracing.apm`. All settings related to the Java agent -go under `telemetry.agent`. Anything you set under there will be propagated to +All APM settings live under `telemetry`. Tracing related settings go under `telemetry.tracing` and settings +related to the Java agent go under `telemetry.agent`. Anything you set under there will be propagated to the agent. For agent settings that can be changed dynamically, you can use the cluster diff --git a/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java b/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java index 746a09d242761..9216b538bd313 100644 --- a/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java +++ b/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/RunTask.java @@ -201,10 +201,10 @@ public void beforeStart() { try { mockServer.start(); node.setting("telemetry.metrics.enabled", "true"); - node.setting("tracing.apm.enabled", "true"); - node.setting("tracing.apm.agent.transaction_sample_rate", "0.10"); - node.setting("tracing.apm.agent.metrics_interval", "10s"); - node.setting("tracing.apm.agent.server_url", "http://127.0.0.1:" + mockServer.getPort()); + node.setting("telemetry.tracing.enabled", "true"); + node.setting("telemetry.agent.transaction_sample_rate", "0.10"); + node.setting("telemetry.agent.metrics_interval", "10s"); + node.setting("telemetry.agent.server_url", "http://127.0.0.1:" + mockServer.getPort()); } catch (IOException e) { logger.warn("Unable to start APM server", e); } @@ -213,9 +213,10 @@ public void beforeStart() { // if metrics were not enabled explicitly for gradlew run we should disable them else if (node.getSettingKeys().contains("telemetry.metrics.enabled") == false) { // metrics node.setting("telemetry.metrics.enabled", "false"); - } else if (node.getSettingKeys().contains("tracing.apm.enabled") == false) { // tracing - node.setting("tracing.apm.enable", "false"); - } + } else if (node.getSettingKeys().contains("telemetry.tracing.enabled") == false + && node.getSettingKeys().contains("tracing.apm.enabled") == false) { // tracing + node.setting("telemetry.tracing.enable", "false"); + } } } diff --git a/distribution/tools/server-cli/src/test/java/org/elasticsearch/server/cli/APMJvmOptionsTests.java b/distribution/tools/server-cli/src/test/java/org/elasticsearch/server/cli/APMJvmOptionsTests.java index 6e337b0b61845..e8a8d3ee8df77 100644 --- a/distribution/tools/server-cli/src/test/java/org/elasticsearch/server/cli/APMJvmOptionsTests.java +++ b/distribution/tools/server-cli/src/test/java/org/elasticsearch/server/cli/APMJvmOptionsTests.java @@ -108,7 +108,6 @@ public void testExtractSecureSettings() { public void testExtractSettings() throws UserException { Function buildSettings = (prefix) -> Settings.builder() - .put("tracing.apm.enabled", true) .put(prefix + "server_url", "https://myurl:443") .put(prefix + "service_node_name", "instance-0000000001"); @@ -158,7 +157,6 @@ public void testExtractSettings() throws UserException { IllegalStateException.class, () -> APMJvmOptions.extractApmSettings( Settings.builder() - .put("tracing.apm.enabled", true) .put("tracing.apm.agent.server_url", "https://myurl:443") .put("telemetry.agent.server_url", "https://myurl-2:443") .build() diff --git a/docs/changelog/104594.yaml b/docs/changelog/104594.yaml new file mode 100644 index 0000000000000..7729eb028f68e --- /dev/null +++ b/docs/changelog/104594.yaml @@ -0,0 +1,5 @@ +pr: 104594 +summary: Support of `match` for the Query API Key API +area: Authentication +type: enhancement +issues: [] diff --git a/docs/changelog/104625.yaml b/docs/changelog/104625.yaml new file mode 100644 index 0000000000000..28951936107fb --- /dev/null +++ b/docs/changelog/104625.yaml @@ -0,0 +1,6 @@ +pr: 104625 +summary: "Add support for the `type` parameter, for sorting, to the Query API Key\ + \ API" +area: Security +type: enhancement +issues: [] diff --git a/docs/changelog/104753.yaml b/docs/changelog/104753.yaml new file mode 100644 index 0000000000000..f95fd3da44084 --- /dev/null +++ b/docs/changelog/104753.yaml @@ -0,0 +1,5 @@ +pr: 104753 +summary: Upgrade to Lucene 9.9.2 +area: Search +type: upgrade +issues: [] diff --git a/docs/changelog/104832.yaml b/docs/changelog/104832.yaml new file mode 100644 index 0000000000000..89f837b1c3475 --- /dev/null +++ b/docs/changelog/104832.yaml @@ -0,0 +1,6 @@ +pr: 104832 +summary: Limit concurrent shards per node for ESQL +area: ES|QL +type: bug +issues: + - 103666 diff --git a/docs/changelog/104891.yaml b/docs/changelog/104891.yaml new file mode 100644 index 0000000000000..690f2c4b11f88 --- /dev/null +++ b/docs/changelog/104891.yaml @@ -0,0 +1,6 @@ +pr: 104891 +summary: "ESQL: Fix `SearchStats#count(String)` to count values not rows" +area: ES|QL +type: bug +issues: + - 104795 diff --git a/docs/changelog/104904.yaml b/docs/changelog/104904.yaml new file mode 100644 index 0000000000000..07e22feb144ed --- /dev/null +++ b/docs/changelog/104904.yaml @@ -0,0 +1,5 @@ +pr: 104904 +summary: Improve `CANNOT_REBALANCE_CAN_ALLOCATE` explanation +area: Allocation +type: bug +issues: [] diff --git a/docs/changelog/104909.yaml b/docs/changelog/104909.yaml new file mode 100644 index 0000000000000..6d250c22a745a --- /dev/null +++ b/docs/changelog/104909.yaml @@ -0,0 +1,5 @@ +pr: 104909 +summary: "[Connectors API] Relax strict response parsing for get/list operations" +area: Application +type: enhancement +issues: [] diff --git a/docs/changelog/104911.yaml b/docs/changelog/104911.yaml new file mode 100644 index 0000000000000..17a335337e345 --- /dev/null +++ b/docs/changelog/104911.yaml @@ -0,0 +1,7 @@ +pr: 104911 +summary: "ES|QL: Improve type validation in aggs for UNSIGNED_LONG better support\ + \ for VERSION" +area: ES|QL +type: bug +issues: + - 102961 diff --git a/docs/changelog/104927.yaml b/docs/changelog/104927.yaml new file mode 100644 index 0000000000000..e0e098ba10b7b --- /dev/null +++ b/docs/changelog/104927.yaml @@ -0,0 +1,5 @@ +pr: 104927 +summary: Adding `ActionRequestLazyBuilder` implementation of `RequestBuilder` +area: Ingest Node +type: enhancement +issues: [] diff --git a/docs/changelog/99961.yaml b/docs/changelog/99961.yaml new file mode 100644 index 0000000000000..457f7801ce218 --- /dev/null +++ b/docs/changelog/99961.yaml @@ -0,0 +1,6 @@ +pr: 99961 +summary: "ESQL: Correct out-of-range filter pushdowns" +area: ES|QL +type: bug +issues: + - 99960 diff --git a/docs/reference/esql/functions/avg.asciidoc b/docs/reference/esql/functions/avg.asciidoc index 9a6f5a82d1959..7eadff29f1bfc 100644 --- a/docs/reference/esql/functions/avg.asciidoc +++ b/docs/reference/esql/functions/avg.asciidoc @@ -10,7 +10,9 @@ AVG(expression) ---- `expression`:: -Numeric expression. If `null`, the function returns `null`. +Numeric expression. +//If `null`, the function returns `null`. +// TODO: Remove comment when https://github.com/elastic/elasticsearch/issues/104900 is fixed. *Description* @@ -20,7 +22,7 @@ The average of a numeric expression. The result is always a `double` no matter the input type. -*Example* +*Examples* [source.merge.styled,esql] ---- @@ -30,3 +32,16 @@ include::{esql-specs}/stats.csv-spec[tag=avg] |=== include::{esql-specs}/stats.csv-spec[tag=avg-result] |=== + +The expression can use inline functions. For example, to calculate the average +over a multivalued column, first use `MV_AVG` to average the multiple values per +row, and use the result with the `AVG` function: + +[source.merge.styled,esql] +---- +include::{esql-specs}/stats.csv-spec[tag=docsStatsAvgNestedExpression] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/stats.csv-spec[tag=docsStatsAvgNestedExpression-result] +|=== diff --git a/docs/reference/esql/functions/count-distinct.asciidoc b/docs/reference/esql/functions/count-distinct.asciidoc index 04a200935cd48..a9f30d24e0e83 100644 --- a/docs/reference/esql/functions/count-distinct.asciidoc +++ b/docs/reference/esql/functions/count-distinct.asciidoc @@ -6,13 +6,13 @@ [source,esql] ---- -COUNT_DISTINCT(column[, precision_threshold]) +COUNT_DISTINCT(expression[, precision_threshold]) ---- *Parameters* -`column`:: -Column for which to count the number of distinct values. +`expression`:: +Expression that outputs the values on which to perform a distinct count. `precision_threshold`:: Precision threshold. Refer to <>. The @@ -23,29 +23,6 @@ same effect as a threshold of 40000. The default value is 3000. Returns the approximate number of distinct values. -[discrete] -[[esql-agg-count-distinct-approximate]] -==== Counts are approximate - -Computing exact counts requires loading values into a set and returning its -size. This doesn't scale when working on high-cardinality sets and/or large -values as the required memory usage and the need to communicate those -per-shard sets between nodes would utilize too many resources of the cluster. - -This `COUNT_DISTINCT` function is based on the -https://static.googleusercontent.com/media/research.google.com/fr//pubs/archive/40671.pdf[HyperLogLog++] -algorithm, which counts based on the hashes of the values with some interesting -properties: - -include::../../aggregations/metrics/cardinality-aggregation.asciidoc[tag=explanation] - -The `COUNT_DISTINCT` function takes an optional second parameter to configure -the precision threshold. The precision_threshold options allows to trade memory -for accuracy, and defines a unique count below which counts are expected to be -close to accurate. Above this value, counts might become a bit more fuzzy. The -maximum supported value is 40000, thresholds above this number will have the -same effect as a threshold of 40000. The default value is `3000`. - *Supported types* Can take any field type as input. @@ -71,3 +48,38 @@ include::{esql-specs}/stats_count_distinct.csv-spec[tag=count-distinct-precision |=== include::{esql-specs}/stats_count_distinct.csv-spec[tag=count-distinct-precision-result] |=== + +The expression can use inline functions. This example splits a string into +multiple values using the `SPLIT` function and counts the unique values: + +[source.merge.styled,esql] +---- +include::{esql-specs}/stats_count_distinct.csv-spec[tag=docsCountDistinctWithExpression] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/stats_count_distinct.csv-spec[tag=docsCountDistinctWithExpression-result] +|=== + +[discrete] +[[esql-agg-count-distinct-approximate]] +==== Counts are approximate + +Computing exact counts requires loading values into a set and returning its +size. This doesn't scale when working on high-cardinality sets and/or large +values as the required memory usage and the need to communicate those +per-shard sets between nodes would utilize too many resources of the cluster. + +This `COUNT_DISTINCT` function is based on the +https://static.googleusercontent.com/media/research.google.com/fr//pubs/archive/40671.pdf[HyperLogLog++] +algorithm, which counts based on the hashes of the values with some interesting +properties: + +include::../../aggregations/metrics/cardinality-aggregation.asciidoc[tag=explanation] + +The `COUNT_DISTINCT` function takes an optional second parameter to configure +the precision threshold. The precision_threshold options allows to trade memory +for accuracy, and defines a unique count below which counts are expected to be +close to accurate. Above this value, counts might become a bit more fuzzy. The +maximum supported value is 40000, thresholds above this number will have the +same effect as a threshold of 40000. The default value is `3000`. \ No newline at end of file diff --git a/docs/reference/esql/functions/count.asciidoc b/docs/reference/esql/functions/count.asciidoc index 70b13d7fc16b3..38732336413ad 100644 --- a/docs/reference/esql/functions/count.asciidoc +++ b/docs/reference/esql/functions/count.asciidoc @@ -6,14 +6,14 @@ [source,esql] ---- -COUNT([input]) +COUNT([expression]) ---- *Parameters* -`input`:: -Column or literal for which to count the number of values. If omitted, returns a -count all (the number of rows). +`expression`:: +Expression that outputs values to be counted. +If omitted, equivalent to `COUNT(*)` (the number of rows). *Description* @@ -44,3 +44,15 @@ include::{esql-specs}/docs.csv-spec[tag=countAll] |=== include::{esql-specs}/docs.csv-spec[tag=countAll-result] |=== + +The expression can use inline functions. This example splits a string into +multiple values using the `SPLIT` function and counts the values: + +[source.merge.styled,esql] +---- +include::{esql-specs}/stats.csv-spec[tag=docsCountWithExpression] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/stats.csv-spec[tag=docsCountWithExpression-result] +|=== diff --git a/docs/reference/esql/functions/max.asciidoc b/docs/reference/esql/functions/max.asciidoc index 4bc62de341d9d..f2e0d0a0205b3 100644 --- a/docs/reference/esql/functions/max.asciidoc +++ b/docs/reference/esql/functions/max.asciidoc @@ -6,17 +6,17 @@ [source,esql] ---- -MAX(column) +MAX(expression) ---- *Parameters* -`column`:: -Column from which to return the maximum value. +`expression`:: +Expression from which to return the maximum value. *Description* -Returns the maximum value of a numeric column. +Returns the maximum value of a numeric expression. *Example* @@ -28,3 +28,16 @@ include::{esql-specs}/stats.csv-spec[tag=max] |=== include::{esql-specs}/stats.csv-spec[tag=max-result] |=== + +The expression can use inline functions. For example, to calculate the maximum +over an average of a multivalued column, use `MV_AVG` to first average the +multiple values per row, and use the result with the `MAX` function: + +[source.merge.styled,esql] +---- +include::{esql-specs}/stats.csv-spec[tag=docsStatsMaxNestedExpression] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/stats.csv-spec[tag=docsStatsMaxNestedExpression-result] +|=== \ No newline at end of file diff --git a/docs/reference/esql/functions/median-absolute-deviation.asciidoc b/docs/reference/esql/functions/median-absolute-deviation.asciidoc index 301d344489643..796e0797157de 100644 --- a/docs/reference/esql/functions/median-absolute-deviation.asciidoc +++ b/docs/reference/esql/functions/median-absolute-deviation.asciidoc @@ -6,13 +6,13 @@ [source,esql] ---- -MEDIAN_ABSOLUTE_DEVIATION(column) +MEDIAN_ABSOLUTE_DEVIATION(expression) ---- *Parameters* -`column`:: -Column from which to return the median absolute deviation. +`expression`:: +Expression from which to return the median absolute deviation. *Description* @@ -44,3 +44,17 @@ include::{esql-specs}/stats_percentile.csv-spec[tag=median-absolute-deviation] |=== include::{esql-specs}/stats_percentile.csv-spec[tag=median-absolute-deviation-result] |=== + +The expression can use inline functions. For example, to calculate the the +median absolute deviation of the maximum values of a multivalued column, first +use `MV_MAX` to get the maximum value per row, and use the result with the +`MEDIAN_ABSOLUTE_DEVIATION` function: + +[source.merge.styled,esql] +---- +include::{esql-specs}/stats_percentile.csv-spec[tag=docsStatsMADNestedExpression] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/stats_percentile.csv-spec[tag=docsStatsMADNestedExpression-result] +|=== diff --git a/docs/reference/esql/functions/median.asciidoc b/docs/reference/esql/functions/median.asciidoc index 17b51d9c50b26..ef845aafd3915 100644 --- a/docs/reference/esql/functions/median.asciidoc +++ b/docs/reference/esql/functions/median.asciidoc @@ -6,13 +6,13 @@ [source,esql] ---- -MEDIAN(column) +MEDIAN(expression) ---- *Parameters* -`column`:: -Column from which to return the median value. +`expression`:: +Expression from which to return the median value. *Description* @@ -37,3 +37,16 @@ include::{esql-specs}/stats_percentile.csv-spec[tag=median] |=== include::{esql-specs}/stats_percentile.csv-spec[tag=median-result] |=== + +The expression can use inline functions. For example, to calculate the median of +the maximum values of a multivalued column, first use `MV_MAX` to get the +maximum value per row, and use the result with the `MEDIAN` function: + +[source.merge.styled,esql] +---- +include::{esql-specs}/stats_percentile.csv-spec[tag=docsStatsMedianNestedExpression] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/stats_percentile.csv-spec[tag=docsStatsMedianNestedExpression-result] +|=== diff --git a/docs/reference/esql/functions/min.asciidoc b/docs/reference/esql/functions/min.asciidoc index b95efbbc6b3a5..313822818128c 100644 --- a/docs/reference/esql/functions/min.asciidoc +++ b/docs/reference/esql/functions/min.asciidoc @@ -6,17 +6,17 @@ [source,esql] ---- -MIN(column) +MIN(expression) ---- *Parameters* -`column`:: -Column from which to return the minimum value. +`expression`:: +Expression from which to return the minimum value. *Description* -Returns the minimum value of a numeric column. +Returns the minimum value of a numeric expression. *Example* @@ -28,3 +28,16 @@ include::{esql-specs}/stats.csv-spec[tag=min] |=== include::{esql-specs}/stats.csv-spec[tag=min-result] |=== + +The expression can use inline functions. For example, to calculate the minimum +over an average of a multivalued column, use `MV_AVG` to first average the +multiple values per row, and use the result with the `MIN` function: + +[source.merge.styled,esql] +---- +include::{esql-specs}/stats.csv-spec[tag=docsStatsMinNestedExpression] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/stats.csv-spec[tag=docsStatsMinNestedExpression-result] +|=== diff --git a/docs/reference/esql/functions/percentile.asciidoc b/docs/reference/esql/functions/percentile.asciidoc index ab3f14af70486..e00ee436c31cf 100644 --- a/docs/reference/esql/functions/percentile.asciidoc +++ b/docs/reference/esql/functions/percentile.asciidoc @@ -6,13 +6,13 @@ [source,esql] ---- -PERCENTILE(column, percentile) +PERCENTILE(expression, percentile) ---- *Parameters* -`column`:: -Column to convert from multiple values to single value. +`expression`:: +Expression from which to return a percentile. `percentile`:: A constant numeric expression. @@ -23,18 +23,6 @@ Returns the value at which a certain percentage of observed values occur. For example, the 95th percentile is the value which is greater than 95% of the observed values and the 50th percentile is the <>. -[discrete] -[[esql-agg-percentile-approximate]] -==== `PERCENTILE` is (usually) approximate - -include::../../aggregations/metrics/percentile-aggregation.asciidoc[tag=approximate] - -[WARNING] -==== -`PERCENTILE` is also {wikipedia}/Nondeterministic_algorithm[non-deterministic]. -This means you can get slightly different results using the same data. -==== - *Example* [source.merge.styled,esql] @@ -45,3 +33,28 @@ include::{esql-specs}/stats_percentile.csv-spec[tag=percentile] |=== include::{esql-specs}/stats_percentile.csv-spec[tag=percentile-result] |=== + +The expression can use inline functions. For example, to calculate a percentile +of the maximum values of a multivalued column, first use `MV_MAX` to get the +maximum value per row, and use the result with the `PERCENTILE` function: + +[source.merge.styled,esql] +---- +include::{esql-specs}/stats_percentile.csv-spec[tag=docsStatsPercentileNestedExpression] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/stats_percentile.csv-spec[tag=docsStatsPercentileNestedExpression-result] +|=== + +[discrete] +[[esql-agg-percentile-approximate]] +==== `PERCENTILE` is (usually) approximate + +include::../../aggregations/metrics/percentile-aggregation.asciidoc[tag=approximate] + +[WARNING] +==== +`PERCENTILE` is also {wikipedia}/Nondeterministic_algorithm[non-deterministic]. +This means you can get slightly different results using the same data. +==== \ No newline at end of file diff --git a/docs/reference/esql/functions/sum.asciidoc b/docs/reference/esql/functions/sum.asciidoc index e88ebbeb3c771..efe65d5503ec6 100644 --- a/docs/reference/esql/functions/sum.asciidoc +++ b/docs/reference/esql/functions/sum.asciidoc @@ -6,15 +6,15 @@ [source,esql] ---- -SUM(column) +SUM(expression) ---- -`column`:: -Numeric column. +`expression`:: +Numeric expression. *Description* -Returns the sum of a numeric column. +Returns the sum of a numeric expression. *Example* @@ -26,3 +26,16 @@ include::{esql-specs}/stats.csv-spec[tag=sum] |=== include::{esql-specs}/stats.csv-spec[tag=sum-result] |=== + +The expression can use inline functions. For example, to calculate +the sum of each employee's maximum salary changes, apply the +`MV_MAX` function to each row and then sum the results: + +[source.merge.styled,esql] +---- +include::{esql-specs}/stats.csv-spec[tag=docsStatsSumNestedExpression] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/stats.csv-spec[tag=docsStatsSumNestedExpression-result] +|=== diff --git a/docs/reference/esql/processing-commands/stats.asciidoc b/docs/reference/esql/processing-commands/stats.asciidoc index a34bc444578d6..fe84c56bbfc19 100644 --- a/docs/reference/esql/processing-commands/stats.asciidoc +++ b/docs/reference/esql/processing-commands/stats.asciidoc @@ -6,7 +6,8 @@ [source,esql] ---- -STATS [column1 =] expression1[, ..., [columnN =] expressionN] [BY grouping_column1[, ..., grouping_columnN]] +STATS [column1 =] expression1[, ..., [columnN =] expressionN] +[BY grouping_expression1[, ..., grouping_expressionN]] ---- *Parameters* @@ -18,8 +19,10 @@ equal to the corresponding expression (`expressionX`). `expressionX`:: An expression that computes an aggregated value. -`grouping_columnX`:: -The column containing the values to group by. +`grouping_expressionX`:: +An expression that outputs the values to group by. + +NOTE: Individual `null` values are skipped when computing aggregations. *Description* @@ -28,14 +31,14 @@ and calculate one or more aggregated values over the grouped rows. If `BY` is omitted, the output table contains exactly one row with the aggregations applied over the entire dataset. -The following aggregation functions are supported: +The following <> are supported: include::../functions/aggregation-functions.asciidoc[tag=agg_list] NOTE: `STATS` without any groups is much much faster than adding a group. -NOTE: Grouping on a single column is currently much more optimized than grouping - on many columns. In some tests we have seen grouping on a single `keyword` +NOTE: Grouping on a single expression is currently much more optimized than grouping + on many expressions. In some tests we have seen grouping on a single `keyword` column to be five times faster than grouping on two `keyword` columns. Do not try to work around this by combining the two columns together with something like <> and then grouping - that is not going to be @@ -68,10 +71,14 @@ include::{esql-specs}/stats.csv-spec[tag=statsWithoutBy-result] It's possible to calculate multiple values: -[source,esql] +[source.merge.styled,esql] ---- include::{esql-specs}/stats.csv-spec[tag=statsCalcMultipleValues] ---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/stats.csv-spec[tag=statsCalcMultipleValues-result] +|=== It's also possible to group by multiple values (only supported for long and keyword family fields): @@ -81,6 +88,33 @@ keyword family fields): include::{esql-specs}/stats.csv-spec[tag=statsGroupByMultipleValues] ---- +Both the aggregating functions and the grouping expressions accept other +functions. This is useful for using `STATS...BY` on multivalue columns. +For example, to calculate the average salary change, you can use `MV_AVG` to +first average the multiple values per employee, and use the result with the +`AVG` function: + +[source.merge.styled,esql] +---- +include::{esql-specs}/stats.csv-spec[tag=docsStatsAvgNestedExpression] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/stats.csv-spec[tag=docsStatsAvgNestedExpression-result] +|=== + +An example of grouping by an expression is grouping employees on the first +letter of their last name: + +[source.merge.styled,esql] +---- +include::{esql-specs}/stats.csv-spec[tag=docsStatsByExpression] +---- +[%header.monospaced.styled,format=dsv,separator=|] +|=== +include::{esql-specs}/stats.csv-spec[tag=docsStatsByExpression-result] +|=== + Specifying the output column name is optional. If not specified, the new column name is equal to the expression. The following query returns a column named `AVG(salary)`: diff --git a/docs/reference/release-notes/8.12.0.asciidoc b/docs/reference/release-notes/8.12.0.asciidoc index 267f00192ecdc..4c0fc50584b9f 100644 --- a/docs/reference/release-notes/8.12.0.asciidoc +++ b/docs/reference/release-notes/8.12.0.asciidoc @@ -12,6 +12,8 @@ Also see <>. When using `int8_hnsw` and the default `confidence_interval` (or any `confidence_interval` less than `1.0`) and when there are deleted documents in the segments, quantiles may fail to build and prevent merging. +This issue is fixed in 8.12.1. + [[breaking-8.12.0]] [float] === Breaking changes diff --git a/docs/reference/rest-api/security.asciidoc b/docs/reference/rest-api/security.asciidoc index aedd65de76e5d..94b632490ad86 100644 --- a/docs/reference/rest-api/security.asciidoc +++ b/docs/reference/rest-api/security.asciidoc @@ -188,6 +188,7 @@ include::security/get-role-mappings.asciidoc[] include::security/get-roles.asciidoc[] include::security/get-service-accounts.asciidoc[] include::security/get-service-credentials.asciidoc[] +include::security/get-settings.asciidoc[] include::security/get-tokens.asciidoc[] include::security/get-user-privileges.asciidoc[] @@ -202,6 +203,7 @@ include::security/oidc-logout-api.asciidoc[] include::security/query-api-key.asciidoc[] include::security/query-user.asciidoc[] include::security/update-api-key.asciidoc[] +include::security/update-settings.asciidoc[] include::security/bulk-update-api-keys.asciidoc[] include::security/saml-prepare-authentication-api.asciidoc[] include::security/saml-authenticate-api.asciidoc[] diff --git a/docs/reference/rest-api/security/get-settings.asciidoc b/docs/reference/rest-api/security/get-settings.asciidoc index d402c74b5c46b..5c38b96903cbd 100644 --- a/docs/reference/rest-api/security/get-settings.asciidoc +++ b/docs/reference/rest-api/security/get-settings.asciidoc @@ -5,17 +5,21 @@ Get Security settings ++++ +[[security-api-get-settings-prereqs]] ==== {api-prereq-title} * To use this API, you must have at least the `read_security` cluster privilege. +[[security-api-get-settings-desc]] ==== {api-description-title} -This API allows a user to retrieve the user-configurable settings for the Security internal index (`.security` and associated indices). Only a subset of the index settings — those that are user-configurable—will be shown. This includes: +This API allows a user to retrieve the user-configurable settings for the +Security internal index (`.security` and associated indices). Only a subset of +the index settings — those that are user-configurable—will be shown. This includes: - `index.auto_expand_replicas` - `index.number_of_replicas` -An example of retrieving the Security settings: +An example of retrieving the security settings: [source,console] ----------------------------------------------------------- @@ -24,4 +28,5 @@ GET /_security/settings // TEST[setup:user_profiles] // TEST[setup:service_token42] -The configurable settings can be modified using the <> API. +The configurable settings can be modified using the +<> API. diff --git a/docs/reference/rest-api/security/query-api-key.asciidoc b/docs/reference/rest-api/security/query-api-key.asciidoc index a08a8fd1858b6..394464dc21456 100644 --- a/docs/reference/rest-api/security/query-api-key.asciidoc +++ b/docs/reference/rest-api/security/query-api-key.asciidoc @@ -52,12 +52,20 @@ You can specify the following parameters in the request body: (Optional, string) A <> to filter which API keys to return. The query supports a subset of query types, including <>, <>, -<>, <>, <>, -<>, <>, <>, -<>, and <> +<>, <>, +<>, <>, +<>, <>, +<>, <>, +and <> + You can query the following public values associated with an API key. + +NOTE: The queryable string values associated with API keys are internally mapped as <>. +Consequently, if no <> parameter is specified for a +<> query, then the provided match query string is interpreted as +a single keyword value. Such a <> query is hence equivalent to a +<> query. ++ .Valid values for `query` [%collapsible%open] ==== diff --git a/docs/reference/rest-api/security/update-settings.asciidoc b/docs/reference/rest-api/security/update-settings.asciidoc index 525b297123c31..0ea41d86e85ed 100644 --- a/docs/reference/rest-api/security/update-settings.asciidoc +++ b/docs/reference/rest-api/security/update-settings.asciidoc @@ -5,12 +5,16 @@ Update Security settings ++++ +[[security-api-update-settings-prereqs]] ==== {api-prereq-title} * To use this API, you must have at least the `manage_security` cluster privilege. +[[security-api-update-settings-desc]] ==== {api-description-title} -This API allows a user to modify the settings for the Security internal indices (`.security` and associated indices). Only a subset of settings are allowed to be modified. This includes: +This API allows a user to modify the settings for the Security internal indices +(`.security` and associated indices). Only a subset of settings are allowed to +be modified. This includes: - `index.auto_expand_replicas` - `index.number_of_replicas` @@ -34,17 +38,23 @@ PUT /_security/settings ----------------------------------------------------------- // TEST[skip:making sure all the indices have been created reliably is difficult] -The configured settings can be retrieved using the <> API. If a -given index is not in use on the system, but settings are provided for it, the request will be rejected - this API does -not yet support configuring the settings for these indices before they are in use. +The configured settings can be retrieved using the +<> API. If a given index +is not in use on the system, but settings are provided for it, the request will +be rejected - this API does not yet support configuring the settings for these +indices before they are in use. + ==== {api-request-body-title} + `security`:: -(Optional, object) Settings to be used for the index used for most security configuration, including Native realm users -and roles configured via the API. +(Optional, object) Settings to be used for the index used for most security +configuration, including Native realm users and roles configured via the API. `security-tokens`:: -(Optional, object) Settings to be used for the index used to store <>. +(Optional, object) Settings to be used for the index used to store +<>. `security`:: -(Optional, object) Settings to be used for the index used to store <> information. +(Optional, object) Settings to be used for the index used to store +<> information. 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 7fbdecc0aebce..249fddce9c416 100644 --- a/docs/reference/search/search-your-data/semantic-search-inference.asciidoc +++ b/docs/reference/search/search-your-data/semantic-search-inference.asciidoc @@ -4,9 +4,9 @@ Semantic search with the {infer} API ++++ -The instructions in this tutorial shows you how to use the {infer} API with the -Open AI service to perform semantic search on your data. The following example -uses OpenAI's `text-embedding-ada-002` second generation embedding model. You +The instructions in this tutorial shows you how to use the {infer} API with the +Open AI service to perform semantic search on your data. The following example +uses OpenAI's `text-embedding-ada-002` second generation embedding model. You can use any OpenAI models, they are all supported by the {infer} API. @@ -14,8 +14,8 @@ can use any OpenAI models, they are all supported by the {infer} API. [[infer-openai-requirements]] ==== Requirements -An https://openai.com/[OpenAI account] is required to use the {infer} API with -the OpenAI service. +An https://openai.com/[OpenAI account] is required to use the {infer} API with +the OpenAI service. [discrete] @@ -39,13 +39,13 @@ PUT _inference/text_embedding/openai_embeddings <1> ------------------------------------------------------------ // TEST[skip:TBD] <1> The task type is `text_embedding` in the path. -<2> The API key of your OpenAI account. You can find your OpenAI API keys in -your OpenAI account under the -https://platform.openai.com/api-keys[API keys section]. You need to provide -your API key only once. The <> does not return your API +<2> The API key of your OpenAI account. You can find your OpenAI API keys in +your OpenAI account under the +https://platform.openai.com/api-keys[API keys section]. You need to provide +your API key only once. The <> does not return your API key. -<3> The name of the embedding model to use. You can find the list of OpenAI -embedding models +<3> The name of the embedding model to use. You can find the list of OpenAI +embedding models https://platform.openai.com/docs/guides/embeddings/embedding-models[here]. @@ -53,9 +53,9 @@ https://platform.openai.com/docs/guides/embeddings/embedding-models[here]. [[infer-openai-mappings]] ==== 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 <> +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 to index the output of the OpenAI model. [source,console] @@ -67,7 +67,7 @@ PUT openai-embeddings "content_embedding": { <1> "type": "dense_vector", <2> "dims": 1536, <3> - "element_type": "byte", + "element_type": "float", "similarity": "dot_product" <4> }, "content": { <5> @@ -80,15 +80,15 @@ PUT openai-embeddings <1> The name of the field to contain the generated tokens. It must be refrenced in the {infer} pipeline configuration in the next step. <2> The field to contain the tokens is a `dense_vector` field. -<3> The output dimensions of the model. Find this value in the -https://platform.openai.com/docs/guides/embeddings/embedding-models[OpenAI documentation] +<3> The output dimensions of the model. Find this value in the +https://platform.openai.com/docs/guides/embeddings/embedding-models[OpenAI documentation] of the model you use. -<4> The faster` dot_product` function can be used to calculate similarity -because OpenAI embeddings are normalised to unit length. You can check the +<4> The faster` dot_product` function can be used to calculate similarity +because OpenAI embeddings are normalised to unit length. You can check the https://platform.openai.com/docs/guides/embeddings/which-distance-function-should-i-use[OpenAI docs] -about which similarity function to use. +about which similarity function to use. <5> The name of the field from which to create the sparse vector representation. -In this example, the name of the field is `content`. It must be referenced in +In this example, the name of the field is `content`. It must be referenced in the {infer} pipeline configuration in the next step. <6> The field type which is text in this example. @@ -98,8 +98,8 @@ the {infer} pipeline configuration in the next step. ==== Create an ingest pipeline with an inference processor Create an <> with an -<> and use the OpenAI model you created -above to infer against the data that is being ingested in the +<> and use the OpenAI model you created +above to infer against the data that is being ingested in the pipeline. [source,console] @@ -119,8 +119,8 @@ PUT _ingest/pipeline/openai_embeddings ] } -------------------------------------------------- -<1> The name of the inference model you created by using the -<>. +<1> The name of the inference model you created by using the +<>. <2> Configuration object that defines the `input_field` for the {infer} process and the `output_field` that will contain the {infer} results. @@ -179,9 +179,9 @@ POST _reindex?wait_for_completion=false number makes the update of the reindexing process quicker which enables you to follow the progress closely and detect errors early. -NOTE: The -https://platform.openai.com/account/limits[rate limit of your OpenAI account] -may affect the throughput of the reindexing process. If this happens, change +NOTE: The +https://platform.openai.com/account/limits[rate limit of your OpenAI account] +may affect the throughput of the reindexing process. If this happens, change `size` to `3` or a similar value in magnitude. The call returns a task ID to monitor the progress: @@ -192,7 +192,7 @@ GET _tasks/ ---- // TEST[skip:TBD] -You can also cancel the reindexing process if you don't want to wait until the +You can also cancel the reindexing process if you don't want to wait until the reindexing process is fully complete which might take hours: [source,console] @@ -206,12 +206,12 @@ POST _tasks//_cancel [[infer-semantic-search]] ==== Semantic search -After the dataset has been enriched with the embeddings, you can query the data +After the dataset has been enriched with the embeddings, you can query the data using {ref}/knn-search.html#knn-semantic-search[semantic search]. Pass a `query_vector_builder` to the k-nearest neighbor (kNN) vector search API, and provide the query text and the model you have used to create the embeddings. -NOTE: If you cancelled the reindexing process, you run the query only a part of +NOTE: If you cancelled the reindexing process, you run the query only a part of the data which affects the quality of your results. [source,console] @@ -237,7 +237,7 @@ GET openai-embeddings/_search -------------------------------------------------- // TEST[skip:TBD] -As a result, you receive the top 10 documents that are closest in meaning to the +As a result, you receive the top 10 documents that are closest in meaning to the query from the `openai-embeddings` index sorted by their proximity to the query: [source,consol-result] diff --git a/libs/x-content/src/main/java/org/elasticsearch/xcontent/XContentType.java b/libs/x-content/src/main/java/org/elasticsearch/xcontent/XContentType.java index 56fff226114f8..242da6fd705dd 100644 --- a/libs/x-content/src/main/java/org/elasticsearch/xcontent/XContentType.java +++ b/libs/x-content/src/main/java/org/elasticsearch/xcontent/XContentType.java @@ -273,7 +273,7 @@ public static XContentType fromMediaType(String mediaTypeHeaderValue) throws Ill return null; } - private int index; + private final int index; XContentType(int index) { this.index = index; @@ -315,4 +315,10 @@ public ParsedMediaType toParsedMediaType() { public XContentType canonical() { return this; } + + private static final XContentType[] values = values(); + + public static XContentType ofOrdinal(int ordinal) { + return values[ordinal]; + } } diff --git a/modules/aggregations/src/internalClusterTest/java/org/elasticsearch/aggregations/bucket/TimeSeriesAggregationsIT.java b/modules/aggregations/src/internalClusterTest/java/org/elasticsearch/aggregations/bucket/TimeSeriesAggregationsIT.java index 2050ce20b1aee..917d8f0b80f2c 100644 --- a/modules/aggregations/src/internalClusterTest/java/org/elasticsearch/aggregations/bucket/TimeSeriesAggregationsIT.java +++ b/modules/aggregations/src/internalClusterTest/java/org/elasticsearch/aggregations/bucket/TimeSeriesAggregationsIT.java @@ -22,8 +22,8 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.RangeQueryBuilder; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.Aggregator; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregatorBuilders; import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation; import org.elasticsearch.search.aggregations.bucket.global.Global; @@ -177,7 +177,7 @@ public void setupSuiteScopeCluster() throws Exception { public void testStandAloneTimeSeriesAgg() { assertNoFailuresAndResponse(prepareSearch("index").setSize(0).addAggregation(timeSeries("by_ts")), response -> { - Aggregations aggregations = response.getAggregations(); + InternalAggregations aggregations = response.getAggregations(); assertNotNull(aggregations); InternalTimeSeries timeSeries = aggregations.get("by_ts"); assertThat( @@ -203,7 +203,7 @@ public void testTimeSeriesGroupedByADimension() { .subAggregation(timeSeries("by_ts")) ), response -> { - Aggregations aggregations = response.getAggregations(); + InternalAggregations aggregations = response.getAggregations(); assertNotNull(aggregations); Terms terms = aggregations.get("by_dim"); Set> keys = new HashSet<>(); @@ -236,7 +236,7 @@ public void testTimeSeriesGroupedByDateHistogram() { .subAggregation(timeSeries("by_ts").subAggregation(stats("timestamp").field("@timestamp"))) ), response -> { - Aggregations aggregations = response.getAggregations(); + InternalAggregations aggregations = response.getAggregations(); assertNotNull(aggregations); Histogram histogram = aggregations.get("by_time"); Map, Long> keys = new HashMap<>(); @@ -275,7 +275,7 @@ public void testStandAloneTimeSeriesAggWithDimFilter() { assertNoFailuresAndResponse( prepareSearch("index").setQuery(queryBuilder).setSize(0).addAggregation(timeSeries("by_ts")), response -> { - Aggregations aggregations = response.getAggregations(); + InternalAggregations aggregations = response.getAggregations(); assertNotNull(aggregations); InternalTimeSeries timeSeries = aggregations.get("by_ts"); Map, Map>> filteredData = dataFilteredByDimension("dim_" + dim, val, include); @@ -308,7 +308,7 @@ public void testStandAloneTimeSeriesAggWithGlobalAggregation() { .addAggregation(global("everything").subAggregation(sum("all_sum").field("metric_" + metric))) .addAggregation(PipelineAggregatorBuilders.sumBucket("total_filter_sum", "by_ts>filter_sum")), response -> { - Aggregations aggregations = response.getAggregations(); + InternalAggregations aggregations = response.getAggregations(); assertNotNull(aggregations); InternalTimeSeries timeSeries = aggregations.get("by_ts"); Map, Map>> filteredData = dataFilteredByDimension("dim_" + dim, val, include); @@ -353,7 +353,7 @@ public void testStandAloneTimeSeriesAggWithMetricFilter() { assertNoFailuresAndResponse( prepareSearch("index").setQuery(queryBuilder).setSize(0).addAggregation(timeSeries("by_ts")), response -> { - Aggregations aggregations = response.getAggregations(); + InternalAggregations aggregations = response.getAggregations(); assertNotNull(aggregations); InternalTimeSeries timeSeries = aggregations.get("by_ts"); Map, Map>> filteredData = dataFilteredByMetric( diff --git a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/pipeline/DerivativePipelineAggregator.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/pipeline/DerivativePipelineAggregator.java index 89d445903f8cc..91aba020b8856 100644 --- a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/pipeline/DerivativePipelineAggregator.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/pipeline/DerivativePipelineAggregator.java @@ -69,7 +69,6 @@ public InternalAggregation reduce(InternalAggregation aggregation, AggregationRe xDiff = (thisBucketKey.doubleValue() - lastBucketKey.doubleValue()) / xAxisUnits; } final List aggs = StreamSupport.stream(bucket.getAggregations().spliterator(), false) - .map((p) -> (InternalAggregation) p) .collect(Collectors.toCollection(ArrayList::new)); aggs.add(new Derivative(name(), gradient, xDiff, formatter, metadata())); Bucket newBucket = factory.createBucket(factory.getKey(bucket), bucket.getDocCount(), InternalAggregations.from(aggs)); diff --git a/modules/apm/METERING.md b/modules/apm/METERING.md index 49b365e135e2b..5347d2647a9ae 100644 --- a/modules/apm/METERING.md +++ b/modules/apm/METERING.md @@ -106,7 +106,7 @@ rootProject { afterEvaluate { testClusters.matching { it.name == "runTask" }.configureEach { setting 'xpack.security.audit.enabled', 'true' - keystore 'tracing.apm.secret_token', 'TODO-REPLACE' + keystore 'telemetry.secret_token', 'TODO-REPLACE' setting 'telemetry.metrics.enabled', 'true' setting 'telemetry.agent.server_url', 'https://TODO-REPLACE-URL.apm.eastus2.staging.azure.foundit.no:443' } diff --git a/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/APM.java b/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/APM.java index d3ec2e2984013..bf3f01bd2052f 100644 --- a/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/APM.java +++ b/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/APM.java @@ -33,7 +33,7 @@ * programmatically attach the agent, the Security Manager permissions required for this * make this approach difficult to the point of impossibility. *

- * All settings are found under the tracing.apm. prefix. Any setting under + * All settings are found under the telemetry. prefix. Any setting under * the telemetry.agent. prefix will be forwarded on to the APM Java agent * by setting appropriate system properties. Some settings can only be set once, and must be * set when the agent starts. We therefore also create and configure a config file in @@ -64,14 +64,14 @@ public TelemetryProvider getTelemetryProvider(Settings settings) { @Override public Collection createComponents(PluginServices services) { final APMTracer apmTracer = telemetryProvider.get().getTracer(); + final APMMeterService apmMeter = telemetryProvider.get().getMeterService(); apmTracer.setClusterName(services.clusterService().getClusterName().value()); apmTracer.setNodeName(services.clusterService().getNodeName()); final APMAgentSettings apmAgentSettings = new APMAgentSettings(); - apmAgentSettings.syncAgentSystemProperties(settings); - final APMMeterService apmMeter = new APMMeterService(settings); - apmAgentSettings.addClusterSettingsListeners(services.clusterService(), telemetryProvider.get(), apmMeter); + apmAgentSettings.initAgentSystemProperties(settings); + apmAgentSettings.addClusterSettingsListeners(services.clusterService(), telemetryProvider.get()); logger.info("Sending apm metrics is {}", APMAgentSettings.TELEMETRY_METRICS_ENABLED_SETTING.get(settings) ? "enabled" : "disabled"); logger.info("Sending apm tracing is {}", APMAgentSettings.TELEMETRY_TRACING_ENABLED_SETTING.get(settings) ? "enabled" : "disabled"); diff --git a/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/APMAgentSettings.java b/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/APMAgentSettings.java index 3eba5bc98aaf5..88359d32a628c 100644 --- a/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/APMAgentSettings.java +++ b/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/APMAgentSettings.java @@ -37,23 +37,25 @@ public class APMAgentSettings { private static final Logger LOGGER = LogManager.getLogger(APMAgentSettings.class); - public void addClusterSettingsListeners( - ClusterService clusterService, - APMTelemetryProvider apmTelemetryProvider, - APMMeterService apmMeterService - ) { + public void addClusterSettingsListeners(ClusterService clusterService, APMTelemetryProvider apmTelemetryProvider) { final ClusterSettings clusterSettings = clusterService.getClusterSettings(); final APMTracer apmTracer = apmTelemetryProvider.getTracer(); + final APMMeterService apmMeterService = apmTelemetryProvider.getMeterService(); clusterSettings.addSettingsUpdateConsumer(TELEMETRY_TRACING_ENABLED_SETTING, enabled -> { apmTracer.setEnabled(enabled); this.setAgentSetting("instrument", Boolean.toString(enabled)); + // The agent records data other than spans, e.g. JVM metrics, so we toggle this setting in order to + // minimise its impact to a running Elasticsearch. + boolean recording = enabled || clusterSettings.get(TELEMETRY_METRICS_ENABLED_SETTING); + this.setAgentSetting("recording", Boolean.toString(recording)); }); clusterSettings.addSettingsUpdateConsumer(TELEMETRY_METRICS_ENABLED_SETTING, enabled -> { apmMeterService.setEnabled(enabled); // The agent records data other than spans, e.g. JVM metrics, so we toggle this setting in order to // minimise its impact to a running Elasticsearch. - this.setAgentSetting("recording", Boolean.toString(enabled)); + boolean recording = enabled || clusterSettings.get(TELEMETRY_TRACING_ENABLED_SETTING); + this.setAgentSetting("recording", Boolean.toString(recording)); }); clusterSettings.addSettingsUpdateConsumer(TELEMETRY_TRACING_NAMES_INCLUDE_SETTING, apmTracer::setIncludeNames); clusterSettings.addSettingsUpdateConsumer(TELEMETRY_TRACING_NAMES_EXCLUDE_SETTING, apmTracer::setExcludeNames); @@ -62,11 +64,16 @@ public void addClusterSettingsListeners( } /** - * Copies APM settings from the provided settings object into the corresponding system properties. + * Initialize APM settings from the provided settings object into the corresponding system properties. + * Later updates to these settings are synchronized using update consumers. * @param settings the settings to apply */ - public void syncAgentSystemProperties(Settings settings) { - this.setAgentSetting("recording", Boolean.toString(TELEMETRY_TRACING_ENABLED_SETTING.get(settings))); + public void initAgentSystemProperties(Settings settings) { + boolean tracing = TELEMETRY_TRACING_ENABLED_SETTING.get(settings); + boolean metrics = TELEMETRY_METRICS_ENABLED_SETTING.get(settings); + + this.setAgentSetting("recording", Boolean.toString(tracing || metrics)); + this.setAgentSetting("instrument", Boolean.toString(tracing)); // Apply values from the settings in the cluster state APM_AGENT_SETTINGS.getAsMap(settings).forEach(this::setAgentSetting); } @@ -113,7 +120,7 @@ public void setAgentSetting(String key, String value) { // Core: // forbid 'enabled', must remain enabled to dynamically enable tracing / metrics - // forbid 'recording' / 'instrument', controlled by 'telemetry.metrics.enabled' / 'tracing.apm.enabled' + // forbid 'recording' / 'instrument', controlled by 'telemetry.metrics.enabled' / 'telemetry.tracing.enabled' "service_name", "service_node_name", // forbid 'service_version', forced by APMJvmOptions @@ -200,8 +207,8 @@ public void setAgentSetting(String key, String value) { "profiling_inferred_spans_lib_directory", // Reporter: - // forbid secret_token: use tracing.apm.secret_token instead - // forbid api_key: use tracing.apm.api_key instead + // forbid secret_token: use telemetry.secret_token instead + // forbid api_key: use telemetry.api_key instead "server_url", "server_urls", "disable_send", diff --git a/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/APMMeterService.java b/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/APMMeterService.java index 21f0b8491f644..ae1204e75af1a 100644 --- a/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/APMMeterService.java +++ b/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/APMMeterService.java @@ -49,7 +49,7 @@ public APMMeterRegistry getMeterRegistry() { } /** - * @see APMAgentSettings#addClusterSettingsListeners(ClusterService, APMTelemetryProvider, APMMeterService) + * @see APMAgentSettings#addClusterSettingsListeners(ClusterService, APMTelemetryProvider) */ void setEnabled(boolean enabled) { this.enabled = enabled; diff --git a/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/APMTelemetryProvider.java b/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/APMTelemetryProvider.java index 5b78c2f5f6a3c..d7b061b4b0d19 100644 --- a/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/APMTelemetryProvider.java +++ b/modules/apm/src/main/java/org/elasticsearch/telemetry/apm/internal/APMTelemetryProvider.java @@ -14,12 +14,10 @@ import org.elasticsearch.telemetry.apm.internal.tracing.APMTracer; public class APMTelemetryProvider implements TelemetryProvider { - private final Settings settings; private final APMTracer apmTracer; private final APMMeterService apmMeterService; public APMTelemetryProvider(Settings settings) { - this.settings = settings; apmTracer = new APMTracer(settings); apmMeterService = new APMMeterService(settings); } @@ -29,6 +27,10 @@ public APMTracer getTracer() { return apmTracer; } + public APMMeterService getMeterService() { + return apmMeterService; + } + @Override public APMMeterRegistry getMeterRegistry() { return apmMeterService.getMeterRegistry(); diff --git a/modules/apm/src/test/java/org/elasticsearch/telemetry/apm/internal/APMAgentSettingsTests.java b/modules/apm/src/test/java/org/elasticsearch/telemetry/apm/internal/APMAgentSettingsTests.java index 52607a79fe69d..d7ae93aded3de 100644 --- a/modules/apm/src/test/java/org/elasticsearch/telemetry/apm/internal/APMAgentSettingsTests.java +++ b/modules/apm/src/test/java/org/elasticsearch/telemetry/apm/internal/APMAgentSettingsTests.java @@ -8,81 +8,190 @@ package org.elasticsearch.telemetry.apm.internal; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; +import org.mockito.Mockito; import java.util.List; - +import java.util.Set; + +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.APM_AGENT_SETTINGS; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TELEMETRY_API_KEY_SETTING; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TELEMETRY_METRICS_ENABLED_SETTING; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TELEMETRY_SECRET_TOKEN_SETTING; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TELEMETRY_TRACING_ENABLED_SETTING; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TELEMETRY_TRACING_NAMES_EXCLUDE_SETTING; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TELEMETRY_TRACING_NAMES_INCLUDE_SETTING; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TELEMETRY_TRACING_SANITIZE_FIELD_NAMES; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TRACING_APM_API_KEY_SETTING; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TRACING_APM_ENABLED_SETTING; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TRACING_APM_NAMES_EXCLUDE_SETTING; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TRACING_APM_NAMES_INCLUDE_SETTING; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TRACING_APM_SANITIZE_FIELD_NAMES; +import static org.elasticsearch.telemetry.apm.internal.APMAgentSettings.TRACING_APM_SECRET_TOKEN_SETTING; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class APMAgentSettingsTests extends ESTestCase { + APMAgentSettings apmAgentSettings = spy(new APMAgentSettings()); + APMTelemetryProvider apmTelemetryProvider = mock(Mockito.RETURNS_DEEP_STUBS); /** * Check that when the tracer is enabled, it also sets the APM agent's recording system property to true. */ public void testEnableTracing() { - APMAgentSettings apmAgentSettings = spy(new APMAgentSettings()); - Settings settings = Settings.builder().put(APMAgentSettings.TELEMETRY_TRACING_ENABLED_SETTING.getKey(), true).build(); - apmAgentSettings.syncAgentSystemProperties(settings); - - verify(apmAgentSettings).setAgentSetting("recording", "true"); + for (boolean metricsEnabled : List.of(true, false)) { + clearInvocations(apmAgentSettings, apmTelemetryProvider.getTracer()); + + Settings update = Settings.builder() + .put(TELEMETRY_TRACING_ENABLED_SETTING.getKey(), true) + .put(TELEMETRY_METRICS_ENABLED_SETTING.getKey(), metricsEnabled) + .build(); + apmAgentSettings.initAgentSystemProperties(update); + + verify(apmAgentSettings).setAgentSetting("recording", "true"); + verify(apmAgentSettings).setAgentSetting("instrument", "true"); + clearInvocations(apmAgentSettings); + + Settings initial = Settings.builder().put(update).put(TELEMETRY_TRACING_ENABLED_SETTING.getKey(), false).build(); + triggerUpdateConsumer(initial, update); + verify(apmAgentSettings).setAgentSetting("recording", "true"); + verify(apmAgentSettings).setAgentSetting("instrument", "true"); + verify(apmTelemetryProvider.getTracer()).setEnabled(true); + } } public void testEnableTracingUsingLegacySetting() { - APMAgentSettings apmAgentSettings = spy(new APMAgentSettings()); - Settings settings = Settings.builder().put(APMAgentSettings.TRACING_APM_ENABLED_SETTING.getKey(), true).build(); - apmAgentSettings.syncAgentSystemProperties(settings); + Settings settings = Settings.builder().put(TRACING_APM_ENABLED_SETTING.getKey(), true).build(); + apmAgentSettings.initAgentSystemProperties(settings); verify(apmAgentSettings).setAgentSetting("recording", "true"); + verify(apmAgentSettings).setAgentSetting("instrument", "true"); + } + + public void testEnableMetrics() { + for (boolean tracingEnabled : List.of(true, false)) { + clearInvocations(apmAgentSettings, apmTelemetryProvider.getMeterService()); + + Settings update = Settings.builder() + .put(TELEMETRY_METRICS_ENABLED_SETTING.getKey(), true) + .put(TELEMETRY_TRACING_ENABLED_SETTING.getKey(), tracingEnabled) + .build(); + apmAgentSettings.initAgentSystemProperties(update); + + verify(apmAgentSettings).setAgentSetting("recording", "true"); + verify(apmAgentSettings).setAgentSetting("instrument", Boolean.toString(tracingEnabled)); + clearInvocations(apmAgentSettings); + + Settings initial = Settings.builder().put(update).put(TELEMETRY_METRICS_ENABLED_SETTING.getKey(), false).build(); + triggerUpdateConsumer(initial, update); + verify(apmAgentSettings).setAgentSetting("recording", "true"); + verify(apmTelemetryProvider.getMeterService()).setEnabled(true); + } } /** - * Check that when the tracer is disabled, it also sets the APM agent's recording system property to false. + * Check that when the tracer is disabled, it also sets the APM agent's recording system property to false unless metrics are enabled. */ public void testDisableTracing() { - APMAgentSettings apmAgentSettings = spy(new APMAgentSettings()); - Settings settings = Settings.builder().put(APMAgentSettings.TELEMETRY_TRACING_ENABLED_SETTING.getKey(), false).build(); - apmAgentSettings.syncAgentSystemProperties(settings); - - verify(apmAgentSettings).setAgentSetting("recording", "false"); + for (boolean metricsEnabled : List.of(true, false)) { + clearInvocations(apmAgentSettings, apmTelemetryProvider.getTracer()); + + Settings update = Settings.builder() + .put(TELEMETRY_TRACING_ENABLED_SETTING.getKey(), false) + .put(TELEMETRY_METRICS_ENABLED_SETTING.getKey(), metricsEnabled) + .build(); + apmAgentSettings.initAgentSystemProperties(update); + + verify(apmAgentSettings).setAgentSetting("recording", Boolean.toString(metricsEnabled)); + verify(apmAgentSettings).setAgentSetting("instrument", "false"); + clearInvocations(apmAgentSettings); + + Settings initial = Settings.builder().put(update).put(TELEMETRY_TRACING_ENABLED_SETTING.getKey(), true).build(); + triggerUpdateConsumer(initial, update); + verify(apmAgentSettings).setAgentSetting("recording", Boolean.toString(metricsEnabled)); + verify(apmAgentSettings).setAgentSetting("instrument", "false"); + verify(apmTelemetryProvider.getTracer()).setEnabled(false); + } } public void testDisableTracingUsingLegacySetting() { - APMAgentSettings apmAgentSettings = spy(new APMAgentSettings()); - Settings settings = Settings.builder().put(APMAgentSettings.TRACING_APM_ENABLED_SETTING.getKey(), false).build(); - apmAgentSettings.syncAgentSystemProperties(settings); + Settings settings = Settings.builder().put(TRACING_APM_ENABLED_SETTING.getKey(), false).build(); + apmAgentSettings.initAgentSystemProperties(settings); verify(apmAgentSettings).setAgentSetting("recording", "false"); + verify(apmAgentSettings).setAgentSetting("instrument", "false"); + } + + public void testDisableMetrics() { + for (boolean tracingEnabled : List.of(true, false)) { + clearInvocations(apmAgentSettings, apmTelemetryProvider.getMeterService()); + + Settings update = Settings.builder() + .put(TELEMETRY_TRACING_ENABLED_SETTING.getKey(), tracingEnabled) + .put(TELEMETRY_METRICS_ENABLED_SETTING.getKey(), false) + .build(); + apmAgentSettings.initAgentSystemProperties(update); + + verify(apmAgentSettings).setAgentSetting("recording", Boolean.toString(tracingEnabled)); + verify(apmAgentSettings).setAgentSetting("instrument", Boolean.toString(tracingEnabled)); + clearInvocations(apmAgentSettings); + + Settings initial = Settings.builder().put(update).put(TELEMETRY_METRICS_ENABLED_SETTING.getKey(), true).build(); + triggerUpdateConsumer(initial, update); + verify(apmAgentSettings).setAgentSetting("recording", Boolean.toString(tracingEnabled)); + verify(apmTelemetryProvider.getMeterService()).setEnabled(false); + } + } + + private void triggerUpdateConsumer(Settings initial, Settings update) { + ClusterService clusterService = mock(); + ClusterSettings clusterSettings = new ClusterSettings( + initial, + Set.of( + TELEMETRY_TRACING_ENABLED_SETTING, + TELEMETRY_METRICS_ENABLED_SETTING, + TELEMETRY_TRACING_NAMES_INCLUDE_SETTING, + TELEMETRY_TRACING_NAMES_EXCLUDE_SETTING, + TELEMETRY_TRACING_SANITIZE_FIELD_NAMES, + APM_AGENT_SETTINGS + ) + ); + when(clusterService.getClusterSettings()).thenReturn(clusterSettings); + apmAgentSettings.addClusterSettingsListeners(clusterService, apmTelemetryProvider); + clusterSettings.applySettings(update); } /** * Check that when cluster settings are synchronised with the system properties, agent settings are set. */ public void testSetAgentSettings() { - APMAgentSettings apmAgentSettings = spy(new APMAgentSettings()); Settings settings = Settings.builder() - .put(APMAgentSettings.TELEMETRY_TRACING_ENABLED_SETTING.getKey(), true) - .put(APMAgentSettings.APM_AGENT_SETTINGS.getKey() + "span_compression_enabled", "true") + .put(TELEMETRY_TRACING_ENABLED_SETTING.getKey(), true) + .put(APM_AGENT_SETTINGS.getKey() + "span_compression_enabled", "true") .build(); - apmAgentSettings.syncAgentSystemProperties(settings); + apmAgentSettings.initAgentSystemProperties(settings); verify(apmAgentSettings).setAgentSetting("recording", "true"); verify(apmAgentSettings).setAgentSetting("span_compression_enabled", "true"); } public void testSetAgentsSettingsWithLegacyPrefix() { - APMAgentSettings apmAgentSettings = spy(new APMAgentSettings()); Settings settings = Settings.builder() - .put(APMAgentSettings.TELEMETRY_TRACING_ENABLED_SETTING.getKey(), true) + .put(TELEMETRY_TRACING_ENABLED_SETTING.getKey(), true) .put("tracing.apm.agent.span_compression_enabled", "true") .build(); - apmAgentSettings.syncAgentSystemProperties(settings); + apmAgentSettings.initAgentSystemProperties(settings); verify(apmAgentSettings).setAgentSetting("recording", "true"); verify(apmAgentSettings).setAgentSetting("span_compression_enabled", "true"); @@ -92,57 +201,54 @@ public void testSetAgentsSettingsWithLegacyPrefix() { * Check that invalid or forbidden APM agent settings are rejected. */ public void testRejectForbiddenOrUnknownAgentSettings() { - List prefixes = List.of(APMAgentSettings.APM_AGENT_SETTINGS.getKey(), "tracing.apm.agent."); + List prefixes = List.of(APM_AGENT_SETTINGS.getKey(), "tracing.apm.agent."); for (String prefix : prefixes) { Settings settings = Settings.builder().put(prefix + "unknown", "true").build(); - Exception exception = expectThrows( - IllegalArgumentException.class, - () -> APMAgentSettings.APM_AGENT_SETTINGS.getAsMap(settings) - ); + Exception exception = expectThrows(IllegalArgumentException.class, () -> APM_AGENT_SETTINGS.getAsMap(settings)); assertThat(exception.getMessage(), containsString("[" + prefix + "unknown]")); } // though, accept / ignore nested global_labels for (String prefix : prefixes) { Settings settings = Settings.builder().put(prefix + "global_labels." + randomAlphaOfLength(5), "123").build(); - APMAgentSettings.APM_AGENT_SETTINGS.getAsMap(settings); + APM_AGENT_SETTINGS.getAsMap(settings); } } public void testTelemetryTracingNamesIncludeFallback() { - Settings settings = Settings.builder().put(APMAgentSettings.TRACING_APM_NAMES_INCLUDE_SETTING.getKey(), "abc,xyz").build(); + Settings settings = Settings.builder().put(TRACING_APM_NAMES_INCLUDE_SETTING.getKey(), "abc,xyz").build(); - List included = APMAgentSettings.TELEMETRY_TRACING_NAMES_INCLUDE_SETTING.get(settings); + List included = TELEMETRY_TRACING_NAMES_INCLUDE_SETTING.get(settings); assertThat(included, containsInAnyOrder("abc", "xyz")); } public void testTelemetryTracingNamesExcludeFallback() { - Settings settings = Settings.builder().put(APMAgentSettings.TRACING_APM_NAMES_EXCLUDE_SETTING.getKey(), "abc,xyz").build(); + Settings settings = Settings.builder().put(TRACING_APM_NAMES_EXCLUDE_SETTING.getKey(), "abc,xyz").build(); - List included = APMAgentSettings.TELEMETRY_TRACING_NAMES_EXCLUDE_SETTING.get(settings); + List included = TELEMETRY_TRACING_NAMES_EXCLUDE_SETTING.get(settings); assertThat(included, containsInAnyOrder("abc", "xyz")); } public void testTelemetryTracingSanitizeFieldNamesFallback() { - Settings settings = Settings.builder().put(APMAgentSettings.TRACING_APM_SANITIZE_FIELD_NAMES.getKey(), "abc,xyz").build(); + Settings settings = Settings.builder().put(TRACING_APM_SANITIZE_FIELD_NAMES.getKey(), "abc,xyz").build(); - List included = APMAgentSettings.TELEMETRY_TRACING_SANITIZE_FIELD_NAMES.get(settings); + List included = TELEMETRY_TRACING_SANITIZE_FIELD_NAMES.get(settings); assertThat(included, containsInAnyOrder("abc", "xyz")); } public void testTelemetryTracingSanitizeFieldNamesFallbackDefault() { - List included = APMAgentSettings.TELEMETRY_TRACING_SANITIZE_FIELD_NAMES.get(Settings.EMPTY); + List included = TELEMETRY_TRACING_SANITIZE_FIELD_NAMES.get(Settings.EMPTY); assertThat(included, hasItem("password")); // and more defaults } public void testTelemetrySecretTokenFallback() { MockSecureSettings secureSettings = new MockSecureSettings(); - secureSettings.setString(APMAgentSettings.TRACING_APM_SECRET_TOKEN_SETTING.getKey(), "verysecret"); + secureSettings.setString(TRACING_APM_SECRET_TOKEN_SETTING.getKey(), "verysecret"); Settings settings = Settings.builder().setSecureSettings(secureSettings).build(); - try (SecureString secureString = APMAgentSettings.TELEMETRY_SECRET_TOKEN_SETTING.get(settings)) { + try (SecureString secureString = TELEMETRY_SECRET_TOKEN_SETTING.get(settings)) { assertEquals("verysecret", secureString.toString()); } @@ -150,12 +256,22 @@ public void testTelemetrySecretTokenFallback() { public void testTelemetryApiKeyFallback() { MockSecureSettings secureSettings = new MockSecureSettings(); - secureSettings.setString(APMAgentSettings.TRACING_APM_API_KEY_SETTING.getKey(), "abc"); + secureSettings.setString(TRACING_APM_API_KEY_SETTING.getKey(), "abc"); Settings settings = Settings.builder().setSecureSettings(secureSettings).build(); - try (SecureString secureString = APMAgentSettings.TELEMETRY_API_KEY_SETTING.get(settings)) { + try (SecureString secureString = TELEMETRY_API_KEY_SETTING.get(settings)) { assertEquals("abc", secureString.toString()); } } + + /** + * Check that invalid or forbidden APM agent settings are rejected if their last part resembles an allowed setting. + */ + public void testRejectUnknownSettingResemblingAnAllowedOne() { + Settings settings = Settings.builder().put(APM_AGENT_SETTINGS.getKey() + "unknown.service_name", "true").build(); + + Exception exception = expectThrows(IllegalArgumentException.class, () -> APM_AGENT_SETTINGS.getAsMap(settings)); + assertThat(exception.getMessage(), containsString("[telemetry.agent.unknown.service_name]")); + } } diff --git a/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/220_drop_processor.yml b/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/220_drop_processor.yml index 6f12087de7d5e..c47dacacde3d8 100644 --- a/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/220_drop_processor.yml +++ b/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/220_drop_processor.yml @@ -99,8 +99,8 @@ teardown: --- "Test Drop Processor with Upsert (_bulk)": - skip: - version: ' - 8.12.99' - reason: 'https://github.com/elastic/elasticsearch/issues/36746 fixed in 8.13.0' + version: ' - 8.12.0' + reason: 'https://github.com/elastic/elasticsearch/issues/36746 fixed in 8.12.1' - do: ingest.put_pipeline: id: "my_pipeline" @@ -140,8 +140,8 @@ teardown: --- "Test Drop Processor with Upsert (_update)": - skip: - version: ' - 8.12.99' - reason: 'https://github.com/elastic/elasticsearch/issues/36746 fixed in 8.13.0' + version: ' - 8.12.0' + reason: 'https://github.com/elastic/elasticsearch/issues/36746 fixed in 8.12.1' - do: ingest.put_pipeline: id: "my_pipeline" diff --git a/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/60_fail.yml b/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/60_fail.yml index 341adaa781ef0..0bf623e8ff263 100644 --- a/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/60_fail.yml +++ b/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/60_fail.yml @@ -77,8 +77,8 @@ teardown: --- "Test Fail Processor with Upsert (bulk)": - skip: - version: ' - 8.12.99' - reason: 'https://github.com/elastic/elasticsearch/issues/36746 fixed in 8.13.0' + version: ' - 8.12.0' + reason: 'https://github.com/elastic/elasticsearch/issues/36746 fixed in 8.12.1' - do: ingest.put_pipeline: id: "my_pipeline" diff --git a/modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RatedRequestsTests.java b/modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RatedRequestsTests.java index c7ad2f2ea4bb5..ac38b87e93ad0 100644 --- a/modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RatedRequestsTests.java +++ b/modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RatedRequestsTests.java @@ -110,6 +110,8 @@ public static RatedRequest createTestItem(boolean forceRequest) { } public void testXContentRoundtrip() throws IOException { + assumeFalse("https://github.com/elastic/elasticsearch/issues/104570", Constants.WINDOWS); + RatedRequest testItem = createTestItem(randomBoolean()); XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values())); XContentBuilder shuffled = shuffleXContent(testItem.toXContent(builder, ToXContent.EMPTY_PARAMS)); @@ -302,6 +304,8 @@ public void testProfileNotAllowed() { * matter for parsing xContent */ public void testParseFromXContent() throws IOException { + assumeFalse("https://github.com/elastic/elasticsearch/issues/104570", Constants.WINDOWS); + String querySpecString = """ { "id": "my_qa_query", diff --git a/modules/reindex/src/internalClusterTest/java/org/elasticsearch/client/documentation/ReindexDocumentationIT.java b/modules/reindex/src/internalClusterTest/java/org/elasticsearch/client/documentation/ReindexDocumentationIT.java index 46271f8c61e9c..071031d2ffd19 100644 --- a/modules/reindex/src/internalClusterTest/java/org/elasticsearch/client/documentation/ReindexDocumentationIT.java +++ b/modules/reindex/src/internalClusterTest/java/org/elasticsearch/client/documentation/ReindexDocumentationIT.java @@ -9,6 +9,7 @@ package org.elasticsearch.client.documentation; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionType; import org.elasticsearch.action.admin.cluster.node.tasks.get.GetTaskResponse; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse; import org.elasticsearch.client.internal.Client; @@ -259,7 +260,7 @@ public void onFailure(Exception e) { /** * Similar to what CancelTests does: blocks some operations to be able to catch some tasks in running state - * @see CancelTests#testCancel(String, AbstractBulkByScrollRequestBuilder, CancelTests.CancelAssertion, Matcher) + * @see CancelTests#testCancel(ActionType, AbstractBulkByScrollRequestBuilder, CancelTests.CancelAssertion, Matcher) */ private ReindexRequestBuilder reindexAndPartiallyBlock() throws Exception { final Client client = client(); diff --git a/modules/reindex/src/test/java/org/elasticsearch/reindex/CancelTests.java b/modules/reindex/src/test/java/org/elasticsearch/reindex/CancelTests.java index b211f7d92f51f..a2911090ab931 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/reindex/CancelTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/reindex/CancelTests.java @@ -12,6 +12,7 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionFuture; +import org.elasticsearch.action.ActionType; import org.elasticsearch.action.admin.cluster.node.tasks.cancel.CancelTasksRequest; import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksResponse; import org.elasticsearch.action.ingest.DeletePipelineRequest; @@ -21,6 +22,7 @@ import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.engine.Engine.Operation.Origin; import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.index.reindex.AbstractBulkByScrollRequest; import org.elasticsearch.index.reindex.AbstractBulkByScrollRequestBuilder; import org.elasticsearch.index.reindex.BulkByScrollResponse; import org.elasticsearch.index.reindex.BulkByScrollTask; @@ -80,15 +82,17 @@ public void clearAllowedOperations() { * Executes the cancellation test */ private void testCancel( - String action, + ActionType action, AbstractBulkByScrollRequestBuilder builder, CancelAssertion assertion, Matcher taskDescriptionMatcher ) throws Exception { createIndex(INDEX); - + // Scroll by 1 so that cancellation is easier to control + builder.source().setSize(1); + AbstractBulkByScrollRequest request = builder.request(); // Total number of documents created for this test (~10 per primary shard per slice) - int numDocs = getNumShards(INDEX).numPrimaries * 10 * builder.request().getSlices(); + int numDocs = getNumShards(INDEX).numPrimaries * 10 * request.getSlices(); ALLOWED_OPERATIONS.release(numDocs); logger.debug("setting up [{}] docs", numDocs); @@ -105,18 +109,15 @@ private void testCancel( assertHitCount(prepareSearch(INDEX).setSize(0), numDocs); assertThat(ALLOWED_OPERATIONS.drainPermits(), equalTo(0)); - // Scroll by 1 so that cancellation is easier to control - builder.source().setSize(1); - /* Allow a random number of the documents less the number of workers * to be modified by the reindex action. That way at least one worker * is blocked. */ - int numModifiedDocs = randomIntBetween(builder.request().getSlices() * 2, numDocs); + int numModifiedDocs = randomIntBetween(request.getSlices() * 2, numDocs); logger.debug("chose to modify [{}] out of [{}] docs", numModifiedDocs, numDocs); - ALLOWED_OPERATIONS.release(numModifiedDocs - builder.request().getSlices()); + ALLOWED_OPERATIONS.release(numModifiedDocs - request.getSlices()); // Now execute the reindex action... - ActionFuture future = builder.execute(); + ActionFuture future = client().execute(action, request); /* ... and wait for the indexing operation listeners to block. It * is important to realize that some of the workers might have @@ -130,7 +131,7 @@ private void testCancel( ); // 10 seconds is usually fine but on heavily loaded machines this can take a while // Status should show the task running - TaskInfo mainTask = findTaskToCancel(action, builder.request().getSlices()); + TaskInfo mainTask = findTaskToCancel(action.name(), request.getSlices()); BulkByScrollTask.Status status = (BulkByScrollTask.Status) mainTask.status(); assertNull(status.getReasonCancelled()); @@ -150,7 +151,7 @@ private void testCancel( logger.debug("asserting that parent is marked canceled {}", status); assertEquals(CancelTasksRequest.DEFAULT_REASON, status.getReasonCancelled()); - if (builder.request().getSlices() > 1) { + if (request.getSlices() > 1) { boolean foundCancelled = false; ListTasksResponse sliceList = clusterAdmin().prepareListTasks() .setTargetParentTaskId(mainTask.taskId()) @@ -168,11 +169,11 @@ private void testCancel( } logger.debug("unblocking the blocked update"); - ALLOWED_OPERATIONS.release(builder.request().getSlices()); + ALLOWED_OPERATIONS.release(request.getSlices()); // Checks that no more operations are executed assertBusy(() -> { - if (builder.request().getSlices() == 1) { + if (request.getSlices() == 1) { /* We can only be sure that we've drained all the permits if we only use a single worker. Otherwise some worker may have * exhausted all of its documents before we blocked. */ assertEquals(0, ALLOWED_OPERATIONS.availablePermits()); @@ -191,7 +192,7 @@ private void testCancel( String tasks = clusterAdmin().prepareListTasks().setTargetParentTaskId(mainTask.taskId()).setDetailed(true).get().toString(); throw new RuntimeException("Exception while waiting for the response. Running tasks: " + tasks, e); } finally { - if (builder.request().getSlices() >= 1) { + if (request.getSlices() >= 1) { // If we have more than one worker we might not have made all the modifications numModifiedDocs -= ALLOWED_OPERATIONS.availablePermits(); } @@ -221,7 +222,7 @@ public static TaskInfo findTaskToCancel(String actionName, int workerCount) { } public void testReindexCancel() throws Exception { - testCancel(ReindexAction.NAME, reindex().source(INDEX).destination("dest"), (response, total, modified) -> { + testCancel(ReindexAction.INSTANCE, reindex().source(INDEX).destination("dest"), (response, total, modified) -> { assertThat(response, matcher().created(modified).reasonCancelled(equalTo("by user request"))); refresh("dest"); @@ -239,17 +240,22 @@ public void testUpdateByQueryCancel() throws Exception { }"""); assertAcked(clusterAdmin().preparePutPipeline("set-processed", pipeline, XContentType.JSON).get()); - testCancel(UpdateByQueryAction.NAME, updateByQuery().setPipeline("set-processed").source(INDEX), (response, total, modified) -> { - assertThat(response, matcher().updated(modified).reasonCancelled(equalTo("by user request"))); - assertHitCount(prepareSearch(INDEX).setSize(0).setQuery(termQuery("processed", true)), modified); - }, equalTo("update-by-query [" + INDEX + "]")); + testCancel( + UpdateByQueryAction.INSTANCE, + updateByQuery().setPipeline("set-processed").source(INDEX), + (response, total, modified) -> { + assertThat(response, matcher().updated(modified).reasonCancelled(equalTo("by user request"))); + assertHitCount(prepareSearch(INDEX).setSize(0).setQuery(termQuery("processed", true)), modified); + }, + equalTo("update-by-query [" + INDEX + "]") + ); assertAcked(clusterAdmin().deletePipeline(new DeletePipelineRequest("set-processed")).get()); } public void testDeleteByQueryCancel() throws Exception { testCancel( - DeleteByQueryAction.NAME, + DeleteByQueryAction.INSTANCE, deleteByQuery().source(INDEX).filter(QueryBuilders.matchAllQuery()), (response, total, modified) -> { assertThat(response, matcher().deleted(modified).reasonCancelled(equalTo("by user request"))); @@ -261,7 +267,7 @@ public void testDeleteByQueryCancel() throws Exception { public void testReindexCancelWithWorkers() throws Exception { testCancel( - ReindexAction.NAME, + ReindexAction.INSTANCE, reindex().source(INDEX).filter(QueryBuilders.matchAllQuery()).destination("dest").setSlices(5), (response, total, modified) -> { assertThat(response, matcher().created(modified).reasonCancelled(equalTo("by user request")).slices(hasSize(5))); @@ -283,7 +289,7 @@ public void testUpdateByQueryCancelWithWorkers() throws Exception { assertAcked(clusterAdmin().preparePutPipeline("set-processed", pipeline, XContentType.JSON).get()); testCancel( - UpdateByQueryAction.NAME, + UpdateByQueryAction.INSTANCE, updateByQuery().setPipeline("set-processed").source(INDEX).setSlices(5), (response, total, modified) -> { assertThat(response, matcher().updated(modified).reasonCancelled(equalTo("by user request")).slices(hasSize(5))); @@ -297,7 +303,7 @@ public void testUpdateByQueryCancelWithWorkers() throws Exception { public void testDeleteByQueryCancelWithWorkers() throws Exception { testCancel( - DeleteByQueryAction.NAME, + DeleteByQueryAction.INSTANCE, deleteByQuery().source(INDEX).filter(QueryBuilders.matchAllQuery()).setSlices(5), (response, total, modified) -> { assertThat(response, matcher().deleted(modified).reasonCancelled(equalTo("by user request")).slices(hasSize(5))); diff --git a/modules/reindex/src/test/java/org/elasticsearch/reindex/ReindexSingleNodeTests.java b/modules/reindex/src/test/java/org/elasticsearch/reindex/ReindexSingleNodeTests.java index 855cb1863f399..24753c2b9ae6a 100644 --- a/modules/reindex/src/test/java/org/elasticsearch/reindex/ReindexSingleNodeTests.java +++ b/modules/reindex/src/test/java/org/elasticsearch/reindex/ReindexSingleNodeTests.java @@ -39,7 +39,7 @@ public void testDeprecatedSort() { int subsetSize = randomIntBetween(1, max - 1); ReindexRequestBuilder copy = new ReindexRequestBuilder(client()).source("source").destination("dest").refresh(true); copy.maxDocs(subsetSize); - copy.request().addSortField("foo", SortOrder.DESC); + copy.source().addSort("foo", SortOrder.DESC); assertThat(copy.get(), matcher().created(subsetSize)); assertHitCount(client().prepareSearch("dest").setSize(0), subsetSize); diff --git a/qa/apm/docker-compose.yml b/qa/apm/docker-compose.yml index b107788b2fb36..a3969479d0914 100644 --- a/qa/apm/docker-compose.yml +++ b/qa/apm/docker-compose.yml @@ -56,13 +56,13 @@ services: - xpack.security.authc.token.enabled=true - xpack.security.enabled=true # APM specific settings. We don't configure `secret_key` because Kibana is configured with a blank key - - tracing.apm.enabled=true - - tracing.apm.agent.server_url=http://apmserver:8200 + - telemetry.tracing.enabled=true + - telemetry.agent.server_url=http://apmserver:8200 # Send traces to APM server aggressively - - tracing.apm.agent.metrics_interval=1s + - telemetry.agent.metrics_interval=1s # Record everything - - tracing.apm.agent.transaction_sample_rate=1 - - tracing.apm.agent.log_level=debug + - telemetry.agent.transaction_sample_rate=1 + - telemetry.agent.log_level=debug healthcheck: interval: 20s retries: 10 diff --git a/qa/mixed-cluster/build.gradle b/qa/mixed-cluster/build.gradle index e3796683d1d32..28d372671ee99 100644 --- a/qa/mixed-cluster/build.gradle +++ b/qa/mixed-cluster/build.gradle @@ -45,10 +45,16 @@ excludeList.add('aggregations/filters_bucket/cache hits') // Validation (and associated tests) are supposed to be skipped/have // different behaviour for versions before and after 8.10 but mixed // cluster tests may not respect that - see the comment above. -excludeList.add('cluster.desired_nodes/10_basic/Test settings are validated') -excludeList.add('cluster.desired_nodes/10_basic/Test unknown settings are forbidden in known versions') -excludeList.add('cluster.desired_nodes/10_basic/Test unknown settings are allowed in future versions') -excludeList.add('cluster.desired_nodes/10_basic/Test some settings can be overridden') +// Same for node version, which has been deprecated (and made optional) +// starting from 8.13 +excludeList.add('cluster.desired_nodes/11_old_format/Test settings are validated') +excludeList.add('cluster.desired_nodes/11_old_format/Test unknown settings are forbidden in known versions') +excludeList.add('cluster.desired_nodes/11_old_format/Test unknown settings are allowed in future versions') +excludeList.add('cluster.desired_nodes/11_old_format/Test some settings can be overridden') +excludeList.add('cluster.desired_nodes/11_old_format/Test node version must be at least the current master version') +excludeList.add('cluster.desired_nodes/11_old_format/Test node version is required') +excludeList.add('cluster.desired_nodes/11_old_format/Test node version must have content') +excludeList.add('cluster.desired_nodes/11_old_format/Test node version can not be null') excludeList.add('cluster.desired_nodes/20_dry_run/Test validation works for dry run updates') BuildParams.bwcVersions.withWireCompatible { bwcVersion, baseName -> diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/connector_secret.delete.json b/rest-api-spec/src/main/resources/rest-api-spec/api/connector_secret.delete.json new file mode 100644 index 0000000000000..511e925a12e1d --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/connector_secret.delete.json @@ -0,0 +1,28 @@ +{ + "connector_secret.delete": { + "documentation": { + "url": null, + "description": "Deletes a connector secret." + }, + "stability": "experimental", + "visibility":"private", + "headers":{ + "accept": [ "application/json"] + }, + "url":{ + "paths":[ + { + "path":"/_connector/_secret/{id}", + "methods":[ "DELETE" ], + "parts":{ + "id":{ + "type":"string", + "description":"The ID of the secret" + } + } + } + ] + }, + "params":{} + } +} diff --git a/server/src/internalClusterTest/java/org/elasticsearch/action/search/TransportSearchIT.java b/server/src/internalClusterTest/java/org/elasticsearch/action/search/TransportSearchIT.java index dd71b82c106a8..5435389452a51 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/action/search/TransportSearchIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/action/search/TransportSearchIT.java @@ -41,13 +41,13 @@ import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationExecutionContext; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorBase; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.AggregatorFactory; import org.elasticsearch.search.aggregations.CardinalityUpperBound; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.search.aggregations.bucket.terms.LongTerms; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; @@ -280,7 +280,7 @@ public void testFinalReduce() throws ExecutionException, InterruptedException { : SearchRequest.subSearchRequest(taskId, originalRequest, Strings.EMPTY_ARRAY, "remote", nowInMillis, true); assertResponse(client().search(searchRequest), searchResponse -> { assertEquals(2, searchResponse.getHits().getTotalHits().value); - Aggregations aggregations = searchResponse.getAggregations(); + InternalAggregations aggregations = searchResponse.getAggregations(); LongTerms longTerms = aggregations.get("terms"); assertEquals(1, longTerms.getBuckets().size()); }); @@ -296,7 +296,7 @@ public void testFinalReduce() throws ExecutionException, InterruptedException { ); assertResponse(client().search(searchRequest), searchResponse -> { assertEquals(2, searchResponse.getHits().getTotalHits().value); - Aggregations aggregations = searchResponse.getAggregations(); + InternalAggregations aggregations = searchResponse.getAggregations(); LongTerms longTerms = aggregations.get("terms"); assertEquals(2, longTerms.getBuckets().size()); }); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/action/termvectors/GetTermVectorsIT.java b/server/src/internalClusterTest/java/org/elasticsearch/action/termvectors/GetTermVectorsIT.java index 9661f4ebb966d..cf8decc5655ec 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/action/termvectors/GetTermVectorsIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/action/termvectors/GetTermVectorsIT.java @@ -984,7 +984,7 @@ public void testFilterDocFreq() throws ExecutionException, InterruptedException, List tags = new ArrayList<>(); for (int i = 0; i < numDocs; i++) { tags.add("tag_" + i); - builders.add(prepareIndex("test").setId(i + "").setSource("tags", tags)); + builders.add(prepareIndex("test").setId(i + "").setSource("tags", List.copyOf(tags))); } indexRandom(true, builders); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/AggregationsIntegrationIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/AggregationsIntegrationIT.java index a856ee36aadc2..5144aee654b31 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/AggregationsIntegrationIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/AggregationsIntegrationIT.java @@ -47,7 +47,7 @@ public void testScroll() { assertNoFailures(response); if (respNum == 1) { // initial response. - Aggregations aggregations = response.getAggregations(); + InternalAggregations aggregations = response.getAggregations(); assertNotNull(aggregations); Terms terms = aggregations.get("f"); assertEquals(Math.min(numDocs, 3L), terms.getBucketByKey("0").getDocCount()); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/CombiIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/CombiIT.java index fc0a93ad3d290..5b8c238d7b7db 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/CombiIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/CombiIT.java @@ -66,7 +66,7 @@ public void testMultipleAggsOnSameField_WithDifferentRequiredValueSourceType() t prepareSearch("idx").addAggregation(missing("missing_values").field("value")) .addAggregation(terms("values").field("value").collectMode(aggCollectionMode)), response -> { - Aggregations aggs = response.getAggregations(); + InternalAggregations aggs = response.getAggregations(); Missing missing = aggs.get("missing_values"); assertNotNull(missing); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/MetadataIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/MetadataIT.java index f22e0a2931634..3634005d37ba4 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/MetadataIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/MetadataIT.java @@ -43,7 +43,7 @@ public void testMetadataSetOnAggregationResult() throws Exception { terms("the_terms").setMetadata(metadata).field("name").subAggregation(sum("the_sum").setMetadata(metadata).field("value")) ).addAggregation(maxBucket("the_max_bucket", "the_terms>the_sum").setMetadata(metadata)), response -> { - Aggregations aggs = response.getAggregations(); + InternalAggregations aggs = response.getAggregations(); assertNotNull(aggs); Terms terms = aggs.get("the_terms"); @@ -52,7 +52,7 @@ public void testMetadataSetOnAggregationResult() throws Exception { List buckets = terms.getBuckets(); for (Terms.Bucket bucket : buckets) { - Aggregations subAggs = bucket.getAggregations(); + InternalAggregations subAggs = bucket.getAggregations(); assertNotNull(subAggs); Sum sum = subAggs.get("the_sum"); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java index da1376a300728..21a607f113f14 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/SignificantTermsSignificanceScoreIT.java @@ -19,9 +19,9 @@ import org.elasticsearch.script.MockScriptPlugin; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; -import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationBuilder; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter; import org.elasticsearch.search.aggregations.bucket.terms.SignificantTerms; import org.elasticsearch.search.aggregations.bucket.terms.SignificantTermsAggregatorFactory; @@ -136,7 +136,7 @@ public void testXContentResponse() throws Exception { StringTerms classes = response.getAggregations().get("class"); assertThat(classes.getBuckets().size(), equalTo(2)); for (Terms.Bucket classBucket : classes.getBuckets()) { - Map aggs = classBucket.getAggregations().asMap(); + Map aggs = classBucket.getAggregations().asMap(); assertTrue(aggs.containsKey("sig_terms")); SignificantTerms agg = (SignificantTerms) aggs.get("sig_terms"); assertThat(agg.getBuckets().size(), equalTo(1)); @@ -331,7 +331,7 @@ public void testBackgroundVsSeparateSet( double score10Background = sigTerms1.getBucketByKey("0").getSignificanceScore(); double score11Background = sigTerms1.getBucketByKey("1").getSignificanceScore(); - Aggregations aggs = response2.getAggregations(); + InternalAggregations aggs = response2.getAggregations(); sigTerms0 = (SignificantTerms) ((InternalFilter) aggs.get("0")).getAggregations().getAsMap().get("sig_terms"); double score00SeparateSets = sigTerms0.getBucketByKey("0").getSignificanceScore(); @@ -386,7 +386,7 @@ public void testScoresEqualForPositiveAndNegative(SignificanceHeuristic heuristi assertThat(classes.getBuckets().size(), equalTo(2)); Iterator classBuckets = classes.getBuckets().iterator(); - Aggregations aggregations = classBuckets.next().getAggregations(); + InternalAggregations aggregations = classBuckets.next().getAggregations(); SignificantTerms sigTerms = aggregations.get("mySignificantTerms"); List classA = sigTerms.getBuckets(); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java index d40264d9facf0..02c45c4aade1b 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/metrics/ScriptedMetricIT.java @@ -19,8 +19,8 @@ import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.aggregations.Aggregation; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.global.Global; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram.Bucket; @@ -1037,7 +1037,7 @@ public void testInitMapCombineReduceWithParamsAsSubAgg() { for (Bucket b : buckets) { assertThat(b, notNullValue()); assertThat(b.getDocCount(), equalTo(1L)); - Aggregations subAggs = b.getAggregations(); + InternalAggregations subAggs = b.getAggregations(); assertThat(subAggs, notNullValue()); assertThat(subAggs.asList().size(), equalTo(1)); Aggregation subAgg = subAggs.get("scripted"); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/BucketMetricsPipeLineAggregationTestCase.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/BucketMetricsPipeLineAggregationTestCase.java index 7509cf3815085..3c9fbca476c0d 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/BucketMetricsPipeLineAggregationTestCase.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/BucketMetricsPipeLineAggregationTestCase.java @@ -19,7 +19,7 @@ import org.elasticsearch.search.aggregations.bucket.terms.IncludeExclude; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; -import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; +import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; import org.elasticsearch.search.aggregations.metrics.Sum; import org.elasticsearch.search.aggregations.metrics.SumAggregationBuilder; import org.elasticsearch.test.ESIntegTestCase; @@ -44,7 +44,7 @@ import static org.hamcrest.core.IsNull.notNullValue; @ESIntegTestCase.SuiteScopeTestCase -abstract class BucketMetricsPipeLineAggregationTestCase extends ESIntegTestCase { +abstract class BucketMetricsPipeLineAggregationTestCase extends ESIntegTestCase { static final String SINGLE_VALUED_FIELD_NAME = "l_value"; diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/ExtendedStatsBucketIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/ExtendedStatsBucketIT.java index 6562c485b9204..421a5d2d36254 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/ExtendedStatsBucketIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/ExtendedStatsBucketIT.java @@ -31,7 +31,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.core.IsNull.notNullValue; -public class ExtendedStatsBucketIT extends BucketMetricsPipeLineAggregationTestCase { +public class ExtendedStatsBucketIT extends BucketMetricsPipeLineAggregationTestCase { @Override protected ExtendedStatsBucketPipelineAggregationBuilder BucketMetricsPipelineAgg(String name, String bucketsPath) { @@ -43,7 +43,7 @@ protected void assertResult( IntToDoubleFunction buckets, Function bucketKeys, int numBuckets, - ExtendedStatsBucket pipelineBucket + InternalExtendedStatsBucket pipelineBucket ) { double sum = 0; int count = 0; @@ -71,7 +71,7 @@ protected String nestedMetric() { } @Override - protected double getNestedMetric(ExtendedStatsBucket bucket) { + protected double getNestedMetric(InternalExtendedStatsBucket bucket) { return bucket.getAvg(); } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketIT.java index c05390bac40ae..b4193b8f90e1f 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/PercentilesBucketIT.java @@ -32,7 +32,7 @@ import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.core.IsNull.notNullValue; -public class PercentilesBucketIT extends BucketMetricsPipeLineAggregationTestCase { +public class PercentilesBucketIT extends BucketMetricsPipeLineAggregationTestCase { private static final double[] PERCENTS = { 0.0, 1.0, 25.0, 50.0, 75.0, 99.0, 100.0 }; @@ -46,7 +46,7 @@ protected void assertResult( IntToDoubleFunction bucketValues, Function bucketKeys, int numBuckets, - PercentilesBucket pipelineBucket + InternalPercentilesBucket pipelineBucket ) { double[] values = new double[numBuckets]; for (int i = 0; i < numBuckets; ++i) { @@ -62,7 +62,7 @@ protected String nestedMetric() { } @Override - protected double getNestedMetric(PercentilesBucket bucket) { + protected double getNestedMetric(InternalPercentilesBucket bucket) { return bucket.percentile(50); } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/StatsBucketIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/StatsBucketIT.java index 7040f3bf115f3..cd87bd98a0926 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/StatsBucketIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/pipeline/StatsBucketIT.java @@ -14,7 +14,7 @@ import static org.elasticsearch.search.aggregations.PipelineAggregatorBuilders.statsBucket; import static org.hamcrest.Matchers.equalTo; -public class StatsBucketIT extends BucketMetricsPipeLineAggregationTestCase { +public class StatsBucketIT extends BucketMetricsPipeLineAggregationTestCase { @Override protected StatsBucketPipelineAggregationBuilder BucketMetricsPipelineAgg(String name, String bucketsPath) { @@ -26,7 +26,7 @@ protected void assertResult( IntToDoubleFunction bucketValues, Function bucketKeys, int numBuckets, - StatsBucket pipelineBucket + InternalStatsBucket pipelineBucket ) { double sum = 0; int count = 0; @@ -52,7 +52,7 @@ protected String nestedMetric() { } @Override - protected double getNestedMetric(StatsBucket bucket) { + protected double getNestedMetric(InternalStatsBucket bucket) { return bucket.getAvg(); } } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoDistanceIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoDistanceIT.java index 37c78ec568332..31524765d4e14 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoDistanceIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/geo/GeoDistanceIT.java @@ -22,7 +22,7 @@ import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.range.InternalGeoDistance; import org.elasticsearch.search.aggregations.bucket.range.Range; import org.elasticsearch.test.ESIntegTestCase; @@ -216,7 +216,7 @@ public void testGeoDistanceAggregation() throws IOException { .addRange(0, 25000) ), response -> { - Aggregations aggregations = response.getAggregations(); + InternalAggregations aggregations = response.getAggregations(); assertNotNull(aggregations); InternalGeoDistance geoDistance = aggregations.get(name); assertNotNull(geoDistance); diff --git a/server/src/main/java/org/elasticsearch/TransportVersion.java b/server/src/main/java/org/elasticsearch/TransportVersion.java index d3224bb048393..22e02652e9f68 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersion.java +++ b/server/src/main/java/org/elasticsearch/TransportVersion.java @@ -101,6 +101,14 @@ public static TransportVersion fromString(String str) { return TransportVersion.fromId(Integer.parseInt(str)); } + /** + * Returns a string representing the Elasticsearch release version of this transport version, + * if applicable for this deployment, otherwise the raw version number. + */ + public String toReleaseVersion() { + return TransportVersions.VERSION_LOOKUP.apply(id); + } + @Override public String toString() { return Integer.toString(id); diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index d0efb612493fc..5d98451e49100 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -165,6 +165,7 @@ static TransportVersion def(int id) { public static final TransportVersion REQUIRE_DATA_STREAM_ADDED = def(8_578_00_0); public static final TransportVersion ML_INFERENCE_COHERE_EMBEDDINGS_ADDED = def(8_579_00_0); public static final TransportVersion DESIRED_NODE_VERSION_OPTIONAL_STRING = def(8_580_00_0); + public static final TransportVersion ML_INFERENCE_REQUEST_INPUT_TYPE_UNSPECIFIED_ADDED = def(8_581_00_0); /* * STOP! READ THIS FIRST! No, really, @@ -289,11 +290,7 @@ static Collection getAllVersions() { return VERSION_IDS.values(); } - private static final IntFunction VERSION_LOOKUP = ReleaseVersions.generateVersionsLookup(TransportVersions.class); - - public static String toReleaseVersion(TransportVersion version) { - return VERSION_LOOKUP.apply(version.id()); - } + static final IntFunction VERSION_LOOKUP = ReleaseVersions.generateVersionsLookup(TransportVersions.class); // no instance private TransportVersions() {} diff --git a/server/src/main/java/org/elasticsearch/action/ActionRequestLazyBuilder.java b/server/src/main/java/org/elasticsearch/action/ActionRequestLazyBuilder.java new file mode 100644 index 0000000000000..7779b71c46717 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/action/ActionRequestLazyBuilder.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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action; + +import org.elasticsearch.client.internal.ElasticsearchClient; +import org.elasticsearch.core.TimeValue; + +import java.util.Objects; + +/** + * This class is similar to ActionRequestBuilder, except that it does not build the request until the request() method is called. + * @param + * @param + */ +public abstract class ActionRequestLazyBuilder + implements + RequestBuilder { + + protected final ActionType action; + protected final ElasticsearchClient client; + + protected ActionRequestLazyBuilder(ElasticsearchClient client, ActionType action) { + Objects.requireNonNull(action, "action must not be null"); + this.action = action; + this.client = client; + } + + /** + * This method creates the request. The caller of this method is responsible for calling Request#decRef. + * @return A newly-built Request, fully initialized by this builder. + */ + public abstract Request request(); + + public ActionFuture execute() { + return client.execute(action, request()); + } + + /** + * Short version of execute().actionGet(). + */ + public Response get() { + return execute().actionGet(); + } + + /** + * Short version of execute().actionGet(). + */ + public Response get(TimeValue timeout) { + return execute().actionGet(timeout); + } + + public void execute(ActionListener listener) { + client.execute(action, request(), listener); + } +} diff --git a/server/src/main/java/org/elasticsearch/action/bulk/BulkRequestBuilder.java b/server/src/main/java/org/elasticsearch/action/bulk/BulkRequestBuilder.java index 2b961b6bc7351..16e5430063650 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/BulkRequestBuilder.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/BulkRequestBuilder.java @@ -8,12 +8,17 @@ package org.elasticsearch.action.bulk; -import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestLazyBuilder; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.RequestBuilder; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.delete.DeleteRequestBuilder; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.action.support.ActiveShardCount; +import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.support.WriteRequestBuilder; import org.elasticsearch.action.support.replication.ReplicationRequest; import org.elasticsearch.action.update.UpdateRequest; @@ -23,26 +28,50 @@ import org.elasticsearch.core.TimeValue; import org.elasticsearch.xcontent.XContentType; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + /** * A bulk request holds an ordered {@link IndexRequest}s and {@link DeleteRequest}s and allows to executes * it in a single batch. */ -public class BulkRequestBuilder extends ActionRequestBuilder implements WriteRequestBuilder { +public class BulkRequestBuilder extends ActionRequestLazyBuilder + implements + WriteRequestBuilder { + private final String globalIndex; + /* + * The following 3 variables hold the list of requests that make up this bulk. Only one can be non-empty. That is, users can't add + * some IndexRequests and some IndexRequestBuilders. They need to pick one (preferably builders) and stick with it. + */ + private final List> requests = new ArrayList<>(); + private final List framedData = new ArrayList<>(); + private final List> requestBuilders = new ArrayList<>(); + private ActiveShardCount waitForActiveShards; + private TimeValue timeout; + private String timeoutString; + private String globalPipeline; + private String globalRouting; + private WriteRequest.RefreshPolicy refreshPolicy; + private String refreshPolicyString; public BulkRequestBuilder(ElasticsearchClient client, @Nullable String globalIndex) { - super(client, BulkAction.INSTANCE, new BulkRequest(globalIndex)); + super(client, BulkAction.INSTANCE); + this.globalIndex = globalIndex; } public BulkRequestBuilder(ElasticsearchClient client) { - super(client, BulkAction.INSTANCE, new BulkRequest()); + this(client, null); } /** * Adds an {@link IndexRequest} to the list of actions to execute. Follows the same behavior of {@link IndexRequest} * (for example, if no id is provided, one will be generated, or usage of the create flag). + * @deprecated use {@link #add(IndexRequestBuilder)} instead */ + @Deprecated public BulkRequestBuilder add(IndexRequest request) { - super.request.add(request); + requests.add(request); return this; } @@ -51,15 +80,17 @@ public BulkRequestBuilder add(IndexRequest request) { * (for example, if no id is provided, one will be generated, or usage of the create flag). */ public BulkRequestBuilder add(IndexRequestBuilder request) { - super.request.add(request.request()); + requestBuilders.add(request); return this; } /** * Adds an {@link DeleteRequest} to the list of actions to execute. + * @deprecated use {@link #add(DeleteRequestBuilder)} instead */ + @Deprecated public BulkRequestBuilder add(DeleteRequest request) { - super.request.add(request); + requests.add(request); return this; } @@ -67,15 +98,17 @@ public BulkRequestBuilder add(DeleteRequest request) { * Adds an {@link DeleteRequest} to the list of actions to execute. */ public BulkRequestBuilder add(DeleteRequestBuilder request) { - super.request.add(request.request()); + requestBuilders.add(request); return this; } /** * Adds an {@link UpdateRequest} to the list of actions to execute. + * @deprecated use {@link #add(UpdateRequestBuilder)} instead */ + @Deprecated public BulkRequestBuilder add(UpdateRequest request) { - super.request.add(request); + requests.add(request); return this; } @@ -83,7 +116,7 @@ public BulkRequestBuilder add(UpdateRequest request) { * Adds an {@link UpdateRequest} to the list of actions to execute. */ public BulkRequestBuilder add(UpdateRequestBuilder request) { - super.request.add(request.request()); + requestBuilders.add(request); return this; } @@ -91,7 +124,7 @@ public BulkRequestBuilder add(UpdateRequestBuilder request) { * Adds a framed data in binary format */ public BulkRequestBuilder add(byte[] data, int from, int length, XContentType xContentType) throws Exception { - request.add(data, from, length, null, xContentType); + framedData.add(new FramedData(data, from, length, null, xContentType)); return this; } @@ -100,7 +133,7 @@ public BulkRequestBuilder add(byte[] data, int from, int length, XContentType xC */ public BulkRequestBuilder add(byte[] data, int from, int length, @Nullable String defaultIndex, XContentType xContentType) throws Exception { - request.add(data, from, length, defaultIndex, xContentType); + framedData.add(new FramedData(data, from, length, defaultIndex, xContentType)); return this; } @@ -109,7 +142,7 @@ public BulkRequestBuilder add(byte[] data, int from, int length, @Nullable Strin * See {@link ReplicationRequest#waitForActiveShards(ActiveShardCount)} for details. */ public BulkRequestBuilder setWaitForActiveShards(ActiveShardCount waitForActiveShards) { - request.waitForActiveShards(waitForActiveShards); + this.waitForActiveShards = waitForActiveShards; return this; } @@ -126,7 +159,7 @@ public BulkRequestBuilder setWaitForActiveShards(final int waitForActiveShards) * A timeout to wait if the index operation can't be performed immediately. Defaults to {@code 1m}. */ public final BulkRequestBuilder setTimeout(TimeValue timeout) { - request.timeout(timeout); + this.timeout = timeout; return this; } @@ -134,7 +167,7 @@ public final BulkRequestBuilder setTimeout(TimeValue timeout) { * A timeout to wait if the index operation can't be performed immediately. Defaults to {@code 1m}. */ public final BulkRequestBuilder setTimeout(String timeout) { - request.timeout(timeout); + this.timeoutString = timeout; return this; } @@ -142,16 +175,96 @@ public final BulkRequestBuilder setTimeout(String timeout) { * The number of actions currently in the bulk. */ public int numberOfActions() { - return request.numberOfActions(); + return requests.size() + requestBuilders.size() + framedData.size(); } public BulkRequestBuilder pipeline(String globalPipeline) { - request.pipeline(globalPipeline); + this.globalPipeline = globalPipeline; return this; } public BulkRequestBuilder routing(String globalRouting) { - request.routing(globalRouting); + this.globalRouting = globalRouting; + return this; + } + + @Override + public BulkRequestBuilder setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { + this.refreshPolicy = refreshPolicy; + return this; + } + + @Override + public BulkRequestBuilder setRefreshPolicy(String refreshPolicy) { + this.refreshPolicyString = refreshPolicy; return this; } + + @Override + public BulkRequest request() { + validate(); + BulkRequest request = new BulkRequest(globalIndex); + for (RequestBuilder requestBuilder : requestBuilders) { + ActionRequest childRequest = requestBuilder.request(); + request.add((DocWriteRequest) childRequest); + } + for (DocWriteRequest childRequest : requests) { + request.add(childRequest); + } + for (FramedData framedData : framedData) { + try { + request.add(framedData.data, framedData.from, framedData.length, framedData.defaultIndex, framedData.xContentType); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + if (waitForActiveShards != null) { + request.waitForActiveShards(waitForActiveShards); + } + if (timeout != null) { + request.timeout(timeout); + } + if (timeoutString != null) { + request.timeout(timeoutString); + } + if (globalPipeline != null) { + request.pipeline(globalPipeline); + } + if (globalRouting != null) { + request.routing(globalRouting); + } + if (refreshPolicy != null) { + request.setRefreshPolicy(refreshPolicy); + } + if (refreshPolicyString != null) { + request.setRefreshPolicy(refreshPolicyString); + } + return request; + } + + private void validate() { + if (countNonEmptyLists(requestBuilders, requests, framedData) > 1) { + throw new IllegalStateException( + "Must use only request builders, requests, or byte arrays within a single bulk request. Cannot mix and match" + ); + } + if (timeout != null && timeoutString != null) { + throw new IllegalStateException("Must use only one setTimeout method"); + } + if (refreshPolicy != null && refreshPolicyString != null) { + throw new IllegalStateException("Must use only one setRefreshPolicy method"); + } + } + + private int countNonEmptyLists(List... lists) { + int sum = 0; + for (List list : lists) { + if (list.isEmpty() == false) { + sum++; + } + } + return sum; + } + + private record FramedData(byte[] data, int from, int length, @Nullable String defaultIndex, XContentType xContentType) {} } diff --git a/server/src/main/java/org/elasticsearch/action/get/TransportGetAction.java b/server/src/main/java/org/elasticsearch/action/get/TransportGetAction.java index 0cc1d51c4d97e..5eab04663e959 100644 --- a/server/src/main/java/org/elasticsearch/action/get/TransportGetAction.java +++ b/server/src/main/java/org/elasticsearch/action/get/TransportGetAction.java @@ -8,17 +8,23 @@ package org.elasticsearch.action.get; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionListenerResponseHandler; import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.action.ActionType; import org.elasticsearch.action.NoShardAvailableActionException; +import org.elasticsearch.action.UnavailableShardsException; import org.elasticsearch.action.admin.indices.refresh.TransportShardRefreshAction; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.replication.BasicReplicationRequest; import org.elasticsearch.action.support.single.shard.TransportSingleShardAction; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateObserver; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.routing.OperationRouting; @@ -27,15 +33,17 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.IndexService; import org.elasticsearch.index.engine.Engine; import org.elasticsearch.index.get.GetResult; import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.index.shard.ShardNotFoundException; import org.elasticsearch.indices.ExecutorSelector; import org.elasticsearch.indices.IndicesService; -import org.elasticsearch.logging.LogManager; -import org.elasticsearch.logging.Logger; +import org.elasticsearch.node.NodeClosedException; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; @@ -184,8 +192,8 @@ private void asyncGet(GetRequest request, ShardId shardId, ActionListener listener) throws IOException { ShardId shardId = indexShard.shardId(); - var node = getCurrentNodeOfPrimary(clusterService.state(), shardId); if (request.refresh()) { + var node = getCurrentNodeOfPrimary(clusterService.state(), shardId); logger.trace("send refresh action for shard {} to node {}", shardId, node.getId()); var refreshRequest = new BasicReplicationRequest(shardId); refreshRequest.setParentTask(request.getParentTask()); @@ -194,44 +202,97 @@ private void handleGetOnUnpromotableShard(GetRequest request, IndexShard indexSh refreshRequest, listener.delegateFailureAndWrap((l, replicationResponse) -> super.asyncShardOperation(request, shardId, l)) ); - } else if (request.realtime()) { - TransportGetFromTranslogAction.Request getFromTranslogRequest = new TransportGetFromTranslogAction.Request(request, shardId); - getFromTranslogRequest.setParentTask(request.getParentTask()); - transportService.sendRequest( - node, - TransportGetFromTranslogAction.NAME, - getFromTranslogRequest, - new ActionListenerResponseHandler<>(listener.delegateFailure((l, r) -> { - if (r.getResult() != null) { - logger.debug("received result for real-time get for id '{}' from promotable shard", request.id()); - l.onResponse(new GetResponse(r.getResult())); - } else { - logger.debug( - "no result for real-time get for id '{}' from promotable shard (segment generation to wait for: {})", - request.id(), - r.segmentGeneration() - ); - if (r.segmentGeneration() == -1) { - // Nothing to wait for (no previous unsafe generation), just handle the Get locally. - ActionRunnable.supply(l, () -> shardOperation(request, shardId)).run(); - } else { - assert r.segmentGeneration() > -1L; - assert r.primaryTerm() > Engine.UNKNOWN_PRIMARY_TERM; - indexShard.waitForPrimaryTermAndGeneration( - r.primaryTerm(), - r.segmentGeneration(), - listener.delegateFailureAndWrap((ll, aLong) -> super.asyncShardOperation(request, shardId, ll)) - ); - } - } - }), TransportGetFromTranslogAction.Response::new, getExecutor(request, shardId)) + return; + } + if (request.realtime()) { + final var state = clusterService.state(); + final var observer = new ClusterStateObserver( + state, + clusterService, + TimeValue.timeValueSeconds(60), + logger, + threadPool.getThreadContext() ); + getFromTranslog(request, indexShard, state, observer, listener); } else { // A non-real-time get with no explicit refresh requested. super.asyncShardOperation(request, shardId, listener); } } + private void getFromTranslog( + GetRequest request, + IndexShard indexShard, + ClusterState state, + ClusterStateObserver observer, + ActionListener listener + ) { + tryGetFromTranslog(request, indexShard, state, listener.delegateResponse((l, e) -> { + final var cause = ExceptionsHelper.unwrapCause(e); + logger.debug("get_from_translog failed", cause); + if (cause instanceof ShardNotFoundException + || cause instanceof IndexNotFoundException + || cause instanceof NoShardAvailableActionException + || cause instanceof UnavailableShardsException) { + logger.debug("retrying get_from_translog"); + observer.waitForNextChange(new ClusterStateObserver.Listener() { + @Override + public void onNewClusterState(ClusterState state) { + getFromTranslog(request, indexShard, state, observer, l); + } + + @Override + public void onClusterServiceClose() { + l.onFailure(new NodeClosedException(clusterService.localNode())); + } + + @Override + public void onTimeout(TimeValue timeout) { + l.onFailure(new ElasticsearchException("Timed out retrying get_from_translog", cause)); + } + }); + } else { + l.onFailure(e); + } + })); + } + + private void tryGetFromTranslog(GetRequest request, IndexShard indexShard, ClusterState state, ActionListener listener) { + ShardId shardId = indexShard.shardId(); + var node = getCurrentNodeOfPrimary(state, shardId); + TransportGetFromTranslogAction.Request getFromTranslogRequest = new TransportGetFromTranslogAction.Request(request, shardId); + getFromTranslogRequest.setParentTask(request.getParentTask()); + transportService.sendRequest( + node, + TransportGetFromTranslogAction.NAME, + getFromTranslogRequest, + new ActionListenerResponseHandler<>(listener.delegateFailure((l, r) -> { + if (r.getResult() != null) { + logger.debug("received result for real-time get for id '{}' from promotable shard", request.id()); + l.onResponse(new GetResponse(r.getResult())); + } else { + logger.debug( + "no result for real-time get for id '{}' from promotable shard (segment generation to wait for: {})", + request.id(), + r.segmentGeneration() + ); + if (r.segmentGeneration() == -1) { + // Nothing to wait for (no previous unsafe generation), just handle the Get locally. + ActionRunnable.supply(l, () -> shardOperation(request, shardId)).run(); + } else { + assert r.segmentGeneration() > -1L; + assert r.primaryTerm() > Engine.UNKNOWN_PRIMARY_TERM; + indexShard.waitForPrimaryTermAndGeneration( + r.primaryTerm(), + r.segmentGeneration(), + listener.delegateFailureAndWrap((ll, aLong) -> super.asyncShardOperation(request, shardId, ll)) + ); + } + } + }), TransportGetFromTranslogAction.Response::new, getExecutor(request, shardId)) + ); + } + static DiscoveryNode getCurrentNodeOfPrimary(ClusterState clusterState, ShardId shardId) { var shardRoutingTable = clusterState.routingTable().shardRoutingTable(shardId); if (shardRoutingTable.primaryShard() == null || shardRoutingTable.primaryShard().active() == false) { diff --git a/server/src/main/java/org/elasticsearch/action/get/TransportGetFromTranslogAction.java b/server/src/main/java/org/elasticsearch/action/get/TransportGetFromTranslogAction.java index 1b180874b433d..cd47531f81599 100644 --- a/server/src/main/java/org/elasticsearch/action/get/TransportGetFromTranslogAction.java +++ b/server/src/main/java/org/elasticsearch/action/get/TransportGetFromTranslogAction.java @@ -40,7 +40,6 @@ import java.io.IOException; import java.util.Objects; -// TODO(ES-5727): add a retry mechanism to TransportGetFromTranslogAction public class TransportGetFromTranslogAction extends HandledTransportAction< TransportGetFromTranslogAction.Request, TransportGetFromTranslogAction.Response> { diff --git a/server/src/main/java/org/elasticsearch/action/index/IndexRequest.java b/server/src/main/java/org/elasticsearch/action/index/IndexRequest.java index eda28eb4e139e..b1ad328abda92 100644 --- a/server/src/main/java/org/elasticsearch/action/index/IndexRequest.java +++ b/server/src/main/java/org/elasticsearch/action/index/IndexRequest.java @@ -165,7 +165,8 @@ public IndexRequest(@Nullable ShardId shardId, StreamInput in) throws IOExceptio isRetry = in.readBoolean(); autoGeneratedTimestamp = in.readLong(); if (in.readBoolean()) { - contentType = in.readEnum(XContentType.class); + // faster than StreamInput::readEnum, do not replace we read a lot of these instances at times + contentType = XContentType.ofOrdinal(in.readByte()); } else { contentType = null; } diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java b/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java index d3bc2d4d1c9e6..8426ac68df139 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java @@ -27,7 +27,6 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.RestActions; import org.elasticsearch.search.SearchHits; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.profile.SearchProfileResults; import org.elasticsearch.search.profile.SearchProfileShardResult; @@ -69,7 +68,7 @@ public class SearchResponse extends ActionResponse implements ChunkedToXContentO private static final ParseField NUM_REDUCE_PHASES = new ParseField("num_reduce_phases"); private final SearchHits hits; - private final Aggregations aggregations; + private final InternalAggregations aggregations; private final Suggest suggest; private final SearchProfileResults profileResults; private final boolean timedOut; @@ -120,7 +119,7 @@ public SearchResponse(StreamInput in) throws IOException { public SearchResponse( SearchHits hits, - Aggregations aggregations, + InternalAggregations aggregations, Suggest suggest, boolean timedOut, Boolean terminatedEarly, @@ -185,7 +184,7 @@ public SearchResponse( public SearchResponse( SearchHits hits, - Aggregations aggregations, + InternalAggregations aggregations, Suggest suggest, boolean timedOut, Boolean terminatedEarly, @@ -257,7 +256,7 @@ public SearchHits getHits() { * Aggregations in this response. "empty" aggregations could be * either {@code null} or {@link InternalAggregations#EMPTY}. */ - public @Nullable Aggregations getAggregations() { + public @Nullable InternalAggregations getAggregations() { return aggregations; } @@ -449,7 +448,7 @@ public static SearchResponse innerFromXContent(XContentParser parser) throws IOE ensureExpectedToken(Token.FIELD_NAME, parser.currentToken(), parser); String currentFieldName = parser.currentName(); SearchHits hits = null; - Aggregations aggs = null; + InternalAggregations aggs = null; Suggest suggest = null; SearchProfileResults profile = null; boolean timedOut = false; @@ -485,8 +484,8 @@ public static SearchResponse innerFromXContent(XContentParser parser) throws IOE } else if (token == Token.START_OBJECT) { if (SearchHits.Fields.HITS.equals(currentFieldName)) { hits = SearchHits.fromXContent(parser); - } else if (Aggregations.AGGREGATIONS_FIELD.equals(currentFieldName)) { - aggs = Aggregations.fromXContent(parser); + } else if (InternalAggregations.AGGREGATIONS_FIELD.equals(currentFieldName)) { + aggs = InternalAggregations.fromXContent(parser); } else if (Suggest.NAME.equals(currentFieldName)) { suggest = Suggest.fromXContent(parser); } else if (SearchProfileResults.PROFILE_FIELD.equals(currentFieldName)) { @@ -550,7 +549,7 @@ public static SearchResponse innerFromXContent(XContentParser parser) throws IOE public void writeTo(StreamOutput out) throws IOException { assert hasReferences(); hits.writeTo(out); - out.writeOptionalWriteable((InternalAggregations) aggregations); + out.writeOptionalWriteable(aggregations); out.writeOptionalWriteable(suggest); out.writeBoolean(timedOut); out.writeOptionalBoolean(terminatedEarly); diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchResponseMerger.java b/server/src/main/java/org/elasticsearch/action/search/SearchResponseMerger.java index 9db9d65bc3dac..ae8c749475c5d 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchResponseMerger.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchResponseMerger.java @@ -147,7 +147,7 @@ public SearchResponse getMergedResponse(Clusters clusters) { profileResults.putAll(searchResponse.getProfileResults()); if (searchResponse.hasAggregations()) { - InternalAggregations internalAggs = (InternalAggregations) searchResponse.getAggregations(); + InternalAggregations internalAggs = searchResponse.getAggregations(); aggs.add(internalAggs); } diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchResponseSections.java b/server/src/main/java/org/elasticsearch/action/search/SearchResponseSections.java index d52a585b3e792..a3763bf101b15 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchResponseSections.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchResponseSections.java @@ -11,7 +11,7 @@ import org.elasticsearch.core.AbstractRefCounted; import org.elasticsearch.core.RefCounted; import org.elasticsearch.search.SearchHits; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.profile.SearchProfileResults; import org.elasticsearch.search.profile.SearchProfileShardResult; import org.elasticsearch.search.suggest.Suggest; @@ -45,7 +45,7 @@ public class SearchResponseSections implements RefCounted { 1 ); protected final SearchHits hits; - protected final Aggregations aggregations; + protected final InternalAggregations aggregations; protected final Suggest suggest; protected final SearchProfileResults profileResults; protected final boolean timedOut; @@ -56,7 +56,7 @@ public class SearchResponseSections implements RefCounted { public SearchResponseSections( SearchHits hits, - Aggregations aggregations, + InternalAggregations aggregations, Suggest suggest, boolean timedOut, Boolean terminatedEarly, @@ -91,7 +91,7 @@ public final SearchHits hits() { return hits; } - public final Aggregations aggregations() { + public final InternalAggregations aggregations() { return aggregations; } diff --git a/server/src/main/java/org/elasticsearch/action/support/WriteRequest.java b/server/src/main/java/org/elasticsearch/action/support/WriteRequest.java index 0df640e3a50a1..64355a32c3a63 100644 --- a/server/src/main/java/org/elasticsearch/action/support/WriteRequest.java +++ b/server/src/main/java/org/elasticsearch/action/support/WriteRequest.java @@ -92,8 +92,10 @@ public static RefreshPolicy parse(String value) { throw new IllegalArgumentException("Unknown value for refresh: [" + value + "]."); } + private static final RefreshPolicy[] values = values(); + public static RefreshPolicy readFrom(StreamInput in) throws IOException { - return RefreshPolicy.values()[in.readByte()]; + return values[in.readByte()]; } @Override diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/Explanations.java b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/Explanations.java index 4549858c2508b..569335cc65a5d 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/allocation/Explanations.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/allocation/Explanations.java @@ -79,9 +79,11 @@ public static final class Rebalance { activities. The shard will be rebalanced when those activities finish. Please wait."""; public static final String CANNOT_REBALANCE_CAN_ALLOCATE = """ - Elasticsearch is allowed to allocate this shard to another node but it isn't allowed to rebalance the shard there. If you \ - expect this shard to be rebalanced to another node, find this node in the node-by-node explanation and address the reasons \ - which prevent Elasticsearch from rebalancing this shard there."""; + Elasticsearch is allowed to allocate this shard on another node, and there is at least one node to which it could move this \ + shard that would improve the overall cluster balance, but it isn't allowed to rebalance this shard there. If you expect this \ + shard to be rebalanced to another node, check the cluster-wide rebalancing decisions and address any reasons preventing \ + Elasticsearch from rebalancing shards within the cluster, and then find the expected node in the node-by-node explanation and \ + address the reasons which prevent Elasticsearch from moving this shard there."""; public static final String CANNOT_REBALANCE_CANNOT_ALLOCATE = """ Elasticsearch is not allowed to allocate or rebalance this shard to another node. If you expect this shard to be rebalanced to \ diff --git a/server/src/main/java/org/elasticsearch/common/geo/LuceneGeometriesUtils.java b/server/src/main/java/org/elasticsearch/common/geo/LuceneGeometriesUtils.java new file mode 100644 index 0000000000000..c9d4b1c534fef --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/geo/LuceneGeometriesUtils.java @@ -0,0 +1,449 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.common.geo; + +import org.apache.lucene.geo.LatLonGeometry; +import org.apache.lucene.geo.XYGeometry; +import org.elasticsearch.geometry.Circle; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.GeometryCollection; +import org.elasticsearch.geometry.GeometryVisitor; +import org.elasticsearch.geometry.Line; +import org.elasticsearch.geometry.LinearRing; +import org.elasticsearch.geometry.MultiLine; +import org.elasticsearch.geometry.MultiPoint; +import org.elasticsearch.geometry.MultiPolygon; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.geometry.Polygon; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.ShapeType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +public class LuceneGeometriesUtils { + + interface Quantizer { + double quantizeLat(double lat); + + double quantizeLon(double lon); + + double[] quantizeLats(double[] lats); + + double[] quantizeLons(double[] lons); + } + + static final Quantizer NOOP_QUANTIZER = new Quantizer() { + @Override + public double quantizeLat(double lat) { + return lat; + } + + @Override + public double quantizeLon(double lon) { + return lon; + } + + @Override + public double[] quantizeLats(double[] lats) { + return lats; + } + + @Override + public double[] quantizeLons(double[] lons) { + return lons; + } + }; + + static Quantizer LATLON_QUANTIZER = new Quantizer() { + @Override + public double quantizeLat(double lat) { + return GeoUtils.quantizeLat(lat); + } + + @Override + public double quantizeLon(double lon) { + return GeoUtils.quantizeLon(lon); + } + + @Override + public double[] quantizeLats(double[] lats) { + return Arrays.stream(lats).map(this::quantizeLat).toArray(); + } + + @Override + public double[] quantizeLons(double[] lons) { + return Arrays.stream(lons).map(this::quantizeLon).toArray(); + } + }; + + /** + * Transform an Elasticsearch {@link Geometry} into a lucene {@link LatLonGeometry} + * + * @param geometry the geometry to transform + * @param quantize if true, the coordinates of the geometry will be quantized using lucene quantization. + * This is useful for queries so the latitude and longitude values to match the values on the index. + * @param checker call for every {@link ShapeType} found in the Geometry. It allows to throw an error if a geometry is + * not supported. + * + * @return an array of {@link LatLonGeometry} + */ + public static LatLonGeometry[] toLatLonGeometry(Geometry geometry, boolean quantize, Consumer checker) { + if (geometry == null || geometry.isEmpty()) { + return new LatLonGeometry[0]; + } + if (GeometryNormalizer.needsNormalize(Orientation.CCW, geometry)) { + // make geometry lucene friendly + geometry = GeometryNormalizer.apply(Orientation.CCW, geometry); + } + final List geometries = new ArrayList<>(); + final Quantizer quantizer = quantize ? LATLON_QUANTIZER : NOOP_QUANTIZER; + geometry.visit(new GeometryVisitor<>() { + @Override + public Void visit(Circle circle) { + checker.accept(ShapeType.CIRCLE); + if (circle.isEmpty() == false) { + geometries.add(toLatLonCircle(circle, quantizer)); + } + return null; + } + + @Override + public Void visit(GeometryCollection collection) { + checker.accept(ShapeType.GEOMETRYCOLLECTION); + if (collection.isEmpty() == false) { + for (org.elasticsearch.geometry.Geometry shape : collection) { + shape.visit(this); + } + } + return null; + } + + @Override + public Void visit(org.elasticsearch.geometry.Line line) { + checker.accept(ShapeType.LINESTRING); + if (line.isEmpty() == false) { + geometries.add(toLatLonLine(line, quantizer)); + } + return null; + } + + @Override + public Void visit(LinearRing ring) { + throw new IllegalArgumentException("Found an unsupported shape LinearRing"); + } + + @Override + public Void visit(MultiLine multiLine) { + checker.accept(ShapeType.MULTILINESTRING); + if (multiLine.isEmpty() == false) { + for (Line line : multiLine) { + visit(line); + } + } + return null; + } + + @Override + public Void visit(MultiPoint multiPoint) { + checker.accept(ShapeType.MULTIPOINT); + if (multiPoint.isEmpty() == false) { + for (Point point : multiPoint) { + visit(point); + } + } + return null; + } + + @Override + public Void visit(MultiPolygon multiPolygon) { + checker.accept(ShapeType.MULTIPOLYGON); + if (multiPolygon.isEmpty() == false) { + for (Polygon polygon : multiPolygon) { + visit(polygon); + } + } + return null; + } + + @Override + public Void visit(Point point) { + checker.accept(ShapeType.POINT); + if (point.isEmpty() == false) { + geometries.add(toLatLonPoint(point, quantizer)); + } + return null; + } + + @Override + public Void visit(org.elasticsearch.geometry.Polygon polygon) { + checker.accept(ShapeType.POLYGON); + if (polygon.isEmpty() == false) { + geometries.add(toLatLonPolygon(polygon, quantizer)); + } + return null; + } + + @Override + public Void visit(Rectangle r) { + checker.accept(ShapeType.ENVELOPE); + if (r.isEmpty() == false) { + geometries.add(toLatLonRectangle(r, quantizer)); + } + return null; + } + }); + return geometries.toArray(new LatLonGeometry[0]); + } + + /** + * Transform an Elasticsearch {@link Point} into a lucene {@link org.apache.lucene.geo.Point} + */ + public static org.apache.lucene.geo.Point toLatLonPoint(Point point) { + return toLatLonPoint(point, NOOP_QUANTIZER); + } + + private static org.apache.lucene.geo.Point toLatLonPoint(Point point, Quantizer quantizer) { + return new org.apache.lucene.geo.Point(quantizer.quantizeLat(point.getLat()), quantizer.quantizeLon(point.getLon())); + } + + /** + * Transform an Elasticsearch {@link Line} into a lucene {@link org.apache.lucene.geo.Line} + */ + public static org.apache.lucene.geo.Line toLatLonLine(Line line) { + return toLatLonLine(line, NOOP_QUANTIZER); + } + + private static org.apache.lucene.geo.Line toLatLonLine(Line line, Quantizer quantizer) { + return new org.apache.lucene.geo.Line(quantizer.quantizeLats(line.getLats()), quantizer.quantizeLons(line.getLons())); + } + + /** + * Transform an Elasticsearch {@link Polygon} into a lucene {@link org.apache.lucene.geo.Polygon} + */ + public static org.apache.lucene.geo.Polygon toLatLonPolygon(Polygon polygon) { + return toLatLonPolygon(polygon, NOOP_QUANTIZER); + } + + private static org.apache.lucene.geo.Polygon toLatLonPolygon(Polygon polygon, Quantizer quantizer) { + org.apache.lucene.geo.Polygon[] holes = new org.apache.lucene.geo.Polygon[polygon.getNumberOfHoles()]; + for (int i = 0; i < holes.length; i++) { + holes[i] = new org.apache.lucene.geo.Polygon( + quantizer.quantizeLats(polygon.getHole(i).getY()), + quantizer.quantizeLons(polygon.getHole(i).getX()) + ); + } + return new org.apache.lucene.geo.Polygon( + quantizer.quantizeLats(polygon.getPolygon().getY()), + quantizer.quantizeLons(polygon.getPolygon().getX()), + holes + ); + + } + + /** + * Transform an Elasticsearch {@link Rectangle} into a lucene {@link org.apache.lucene.geo.Rectangle} + */ + public static org.apache.lucene.geo.Rectangle toLatLonRectangle(Rectangle rectangle) { + return toLatLonRectangle(rectangle, NOOP_QUANTIZER); + } + + private static org.apache.lucene.geo.Rectangle toLatLonRectangle(Rectangle r, Quantizer quantizer) { + return new org.apache.lucene.geo.Rectangle( + quantizer.quantizeLat(r.getMinLat()), + quantizer.quantizeLat(r.getMaxLat()), + quantizer.quantizeLon(r.getMinLon()), + quantizer.quantizeLon(r.getMaxLon()) + ); + } + + /** + * Transform an Elasticsearch {@link Circle} into a lucene {@link org.apache.lucene.geo.Circle} + */ + public static org.apache.lucene.geo.Circle toLatLonCircle(Circle circle) { + return toLatLonCircle(circle, NOOP_QUANTIZER); + } + + private static org.apache.lucene.geo.Circle toLatLonCircle(Circle circle, Quantizer quantizer) { + return new org.apache.lucene.geo.Circle( + quantizer.quantizeLat(circle.getLat()), + quantizer.quantizeLon(circle.getLon()), + circle.getRadiusMeters() + ); + } + + /** + * Transform an Elasticsearch {@link Geometry} into a lucene {@link XYGeometry} + * + * @param geometry the geometry to transform. + * @param checker call for every {@link ShapeType} found in the Geometry. It allows to throw an error if + * a geometry is not supported. + * @return an array of {@link XYGeometry} + */ + public static XYGeometry[] toXYGeometry(Geometry geometry, Consumer checker) { + if (geometry == null || geometry.isEmpty()) { + return new XYGeometry[0]; + } + final List geometries = new ArrayList<>(); + geometry.visit(new GeometryVisitor<>() { + @Override + public Void visit(Circle circle) { + checker.accept(ShapeType.CIRCLE); + if (circle.isEmpty() == false) { + geometries.add(toXYCircle(circle)); + } + return null; + } + + @Override + public Void visit(GeometryCollection collection) { + checker.accept(ShapeType.GEOMETRYCOLLECTION); + if (collection.isEmpty() == false) { + for (org.elasticsearch.geometry.Geometry shape : collection) { + shape.visit(this); + } + } + return null; + } + + @Override + public Void visit(org.elasticsearch.geometry.Line line) { + checker.accept(ShapeType.LINESTRING); + if (line.isEmpty() == false) { + geometries.add(toXYLine(line)); + } + return null; + } + + @Override + public Void visit(LinearRing ring) { + throw new IllegalArgumentException("Found an unsupported shape LinearRing"); + } + + @Override + public Void visit(MultiLine multiLine) { + checker.accept(ShapeType.MULTILINESTRING); + if (multiLine.isEmpty() == false) { + for (Line line : multiLine) { + visit(line); + } + } + return null; + } + + @Override + public Void visit(MultiPoint multiPoint) { + checker.accept(ShapeType.MULTIPOINT); + if (multiPoint.isEmpty() == false) { + for (Point point : multiPoint) { + visit(point); + } + } + return null; + } + + @Override + public Void visit(MultiPolygon multiPolygon) { + checker.accept(ShapeType.MULTIPOLYGON); + if (multiPolygon.isEmpty() == false) { + for (Polygon polygon : multiPolygon) { + visit(polygon); + } + } + return null; + } + + @Override + public Void visit(Point point) { + checker.accept(ShapeType.POINT); + if (point.isEmpty() == false) { + geometries.add(toXYPoint(point)); + } + return null; + } + + @Override + public Void visit(org.elasticsearch.geometry.Polygon polygon) { + checker.accept(ShapeType.POLYGON); + if (polygon.isEmpty() == false) { + geometries.add(toXYPolygon(polygon)); + } + return null; + } + + @Override + public Void visit(Rectangle r) { + checker.accept(ShapeType.ENVELOPE); + if (r.isEmpty() == false) { + geometries.add(toXYRectangle(r)); + } + return null; + } + }); + return geometries.toArray(new XYGeometry[0]); + } + + /** + * Transform an Elasticsearch {@link Point} into a lucene {@link org.apache.lucene.geo.XYPoint} + */ + public static org.apache.lucene.geo.XYPoint toXYPoint(Point point) { + return new org.apache.lucene.geo.XYPoint((float) point.getX(), (float) point.getY()); + } + + /** + * Transform an Elasticsearch {@link Line} into a lucene {@link org.apache.lucene.geo.XYLine} + */ + public static org.apache.lucene.geo.XYLine toXYLine(Line line) { + return new org.apache.lucene.geo.XYLine(doubleArrayToFloatArray(line.getX()), doubleArrayToFloatArray(line.getY())); + } + + /** + * Transform an Elasticsearch {@link Polygon} into a lucene {@link org.apache.lucene.geo.XYPolygon} + */ + public static org.apache.lucene.geo.XYPolygon toXYPolygon(Polygon polygon) { + org.apache.lucene.geo.XYPolygon[] holes = new org.apache.lucene.geo.XYPolygon[polygon.getNumberOfHoles()]; + for (int i = 0; i < holes.length; i++) { + holes[i] = new org.apache.lucene.geo.XYPolygon( + doubleArrayToFloatArray(polygon.getHole(i).getX()), + doubleArrayToFloatArray(polygon.getHole(i).getY()) + ); + } + return new org.apache.lucene.geo.XYPolygon( + doubleArrayToFloatArray(polygon.getPolygon().getX()), + doubleArrayToFloatArray(polygon.getPolygon().getY()), + holes + ); + } + + /** + * Transform an Elasticsearch {@link Rectangle} into a lucene {@link org.apache.lucene.geo.XYRectangle} + */ + public static org.apache.lucene.geo.XYRectangle toXYRectangle(Rectangle r) { + return new org.apache.lucene.geo.XYRectangle((float) r.getMinX(), (float) r.getMaxX(), (float) r.getMinY(), (float) r.getMaxY()); + } + + /** + * Transform an Elasticsearch {@link Circle} into a lucene {@link org.apache.lucene.geo.XYCircle} + */ + public static org.apache.lucene.geo.XYCircle toXYCircle(Circle circle) { + return new org.apache.lucene.geo.XYCircle((float) circle.getX(), (float) circle.getY(), (float) circle.getRadiusMeters()); + } + + static float[] doubleArrayToFloatArray(double[] array) { + float[] result = new float[array.length]; + for (int i = 0; i < array.length; ++i) { + result[i] = (float) array[i]; + } + return result; + } + + private LuceneGeometriesUtils() {} +} diff --git a/server/src/main/java/org/elasticsearch/index/IndexVersion.java b/server/src/main/java/org/elasticsearch/index/IndexVersion.java index f4edb8b1d4039..706a6ec8ccf02 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexVersion.java +++ b/server/src/main/java/org/elasticsearch/index/IndexVersion.java @@ -143,6 +143,14 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder.value(id); } + /** + * Returns a string representing the Elasticsearch release version of this index version, + * if applicable for this deployment, otherwise the raw version number. + */ + public String toReleaseVersion() { + return IndexVersions.VERSION_LOOKUP.apply(id); + } + @Override public String toString() { return Integer.toString(id); diff --git a/server/src/main/java/org/elasticsearch/index/IndexVersions.java b/server/src/main/java/org/elasticsearch/index/IndexVersions.java index f4be80513a553..1fd64671a53d7 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexVersions.java +++ b/server/src/main/java/org/elasticsearch/index/IndexVersions.java @@ -97,9 +97,11 @@ private static IndexVersion def(int id, Version luceneVersion) { public static final IndexVersion NORI_DUPLICATES = def(8_500_007, Version.LUCENE_9_9_0); public static final IndexVersion UPGRADE_LUCENE_9_9_1 = def(8_500_008, Version.LUCENE_9_9_1); public static final IndexVersion ES_VERSION_8_12_1 = def(8_500_009, Version.LUCENE_9_9_1); + public static final IndexVersion UPGRADE_8_12_1_LUCENE_9_9_2 = def(8_500_010, Version.LUCENE_9_9_2); public static final IndexVersion NEW_INDEXVERSION_FORMAT = def(8_501_00_0, Version.LUCENE_9_9_1); + public static final IndexVersion UPGRADE_LUCENE_9_9_2 = def(8_502_00_0, Version.LUCENE_9_9_2); - public static final IndexVersion UPGRADE_TO_LUCENE_9_10 = def(8_502_00_0, Version.LUCENE_9_10_0); + public static final IndexVersion UPGRADE_TO_LUCENE_9_10 = def(8_503_00_0, Version.LUCENE_9_10_0); /* * STOP! READ THIS FIRST! No, really, @@ -209,9 +211,8 @@ static Collection getAllVersions() { return VERSION_IDS.values(); } - private static final IntFunction VERSION_LOOKUP = ReleaseVersions.generateVersionsLookup(IndexVersions.class); + static final IntFunction VERSION_LOOKUP = ReleaseVersions.generateVersionsLookup(IndexVersions.class); - public static String toReleaseVersion(IndexVersion version) { - return VERSION_LOOKUP.apply(version.id()); - } + // no instance + private IndexVersions() {} } diff --git a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java index 842ec5ba3b467..c3f324fc49e82 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java +++ b/server/src/main/java/org/elasticsearch/index/engine/InternalEngine.java @@ -811,7 +811,7 @@ private GetResult getFromTranslog( index, mappingLookup, documentParser, - config().getAnalyzer(), + config(), translogInMemorySegmentsCount::incrementAndGet ); final Engine.Searcher searcher = new Engine.Searcher( diff --git a/server/src/main/java/org/elasticsearch/index/engine/TranslogDirectoryReader.java b/server/src/main/java/org/elasticsearch/index/engine/TranslogDirectoryReader.java index ab84166701c59..2d0c3e8bc1feb 100644 --- a/server/src/main/java/org/elasticsearch/index/engine/TranslogDirectoryReader.java +++ b/server/src/main/java/org/elasticsearch/index/engine/TranslogDirectoryReader.java @@ -8,7 +8,6 @@ package org.elasticsearch.index.engine; -import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.index.BaseTermsEnum; import org.apache.lucene.index.BinaryDocValues; import org.apache.lucene.index.ByteVectorValues; @@ -83,10 +82,10 @@ final class TranslogDirectoryReader extends DirectoryReader { Translog.Index operation, MappingLookup mappingLookup, DocumentParser documentParser, - Analyzer analyzer, + EngineConfig engineConfig, Runnable onSegmentCreated ) throws IOException { - this(new TranslogLeafReader(shardId, operation, mappingLookup, documentParser, analyzer, onSegmentCreated)); + this(new TranslogLeafReader(shardId, operation, mappingLookup, documentParser, engineConfig, onSegmentCreated)); } private TranslogDirectoryReader(TranslogLeafReader leafReader) throws IOException { @@ -208,7 +207,7 @@ private static class TranslogLeafReader extends LeafReader { private final Translog.Index operation; private final MappingLookup mappingLookup; private final DocumentParser documentParser; - private final Analyzer analyzer; + private final EngineConfig engineConfig; private final Directory directory; private final Runnable onSegmentCreated; @@ -220,14 +219,14 @@ private static class TranslogLeafReader extends LeafReader { Translog.Index operation, MappingLookup mappingLookup, DocumentParser documentParser, - Analyzer analyzer, + EngineConfig engineConfig, Runnable onSegmentCreated ) { this.shardId = shardId; this.operation = operation; this.mappingLookup = mappingLookup; this.documentParser = documentParser; - this.analyzer = analyzer; + this.engineConfig = engineConfig; this.onSegmentCreated = onSegmentCreated; this.directory = new ByteBuffersDirectory(); this.uid = Uid.encodeId(operation.id()); @@ -267,7 +266,10 @@ private LeafReader createInMemoryLeafReader() { parsedDocs.updateSeqID(operation.seqNo(), operation.primaryTerm()); parsedDocs.version().setLongValue(operation.version()); - final IndexWriterConfig writeConfig = new IndexWriterConfig(analyzer).setOpenMode(IndexWriterConfig.OpenMode.CREATE); + // To guarantee indexability, we configure the analyzer and codec using the main engine configuration + final IndexWriterConfig writeConfig = new IndexWriterConfig(engineConfig.getAnalyzer()).setOpenMode( + IndexWriterConfig.OpenMode.CREATE + ).setCodec(engineConfig.getCodec()); try (IndexWriter writer = new IndexWriter(directory, writeConfig)) { writer.addDocument(parsedDocs.rootDoc()); final DirectoryReader reader = open(writer); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java index 7ec9ec4fd947f..23879282799ab 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeIndexer.java @@ -13,6 +13,7 @@ import org.apache.lucene.index.IndexableField; import org.elasticsearch.common.geo.GeoUtils; import org.elasticsearch.common.geo.GeometryNormalizer; +import org.elasticsearch.common.geo.LuceneGeometriesUtils; import org.elasticsearch.common.geo.Orientation; import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; @@ -94,7 +95,7 @@ public Void visit(GeometryCollection collection) { @Override public Void visit(Line line) { - addFields(LatLonShape.createIndexableFields(name, toLuceneLine(line))); + addFields(LatLonShape.createIndexableFields(name, LuceneGeometriesUtils.toLatLonLine(line))); return null; } @@ -135,7 +136,7 @@ public Void visit(Point point) { @Override public Void visit(Polygon polygon) { - addFields(LatLonShape.createIndexableFields(name, toLucenePolygon(polygon), true)); + addFields(LatLonShape.createIndexableFields(name, LuceneGeometriesUtils.toLatLonPolygon(polygon), true)); return null; } @@ -199,22 +200,10 @@ private void addFields(IndexableField[] fields) { } } - private static org.apache.lucene.geo.Polygon toLucenePolygon(Polygon polygon) { - org.apache.lucene.geo.Polygon[] holes = new org.apache.lucene.geo.Polygon[polygon.getNumberOfHoles()]; - for (int i = 0; i < holes.length; i++) { - holes[i] = new org.apache.lucene.geo.Polygon(polygon.getHole(i).getY(), polygon.getHole(i).getX()); - } - return new org.apache.lucene.geo.Polygon(polygon.getPolygon().getY(), polygon.getPolygon().getX(), holes); - } - private static org.apache.lucene.geo.Polygon toLucenePolygon(Rectangle r) { return new org.apache.lucene.geo.Polygon( new double[] { r.getMinLat(), r.getMinLat(), r.getMaxLat(), r.getMaxLat(), r.getMinLat() }, new double[] { r.getMinLon(), r.getMaxLon(), r.getMaxLon(), r.getMinLon(), r.getMinLon() } ); } - - private static org.apache.lucene.geo.Line toLuceneLine(Line line) { - return new org.apache.lucene.geo.Line(line.getLats(), line.getLons()); - } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeQueryable.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeQueryable.java index beb594d9e9936..3947f009f1aec 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeQueryable.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeQueryable.java @@ -8,32 +8,18 @@ package org.elasticsearch.index.mapper; -import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.LatLonGeometry; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; -import org.elasticsearch.common.geo.GeometryNormalizer; -import org.elasticsearch.common.geo.Orientation; +import org.elasticsearch.common.geo.LuceneGeometriesUtils; import org.elasticsearch.common.geo.ShapeRelation; import org.elasticsearch.common.geo.SpatialStrategy; -import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; -import org.elasticsearch.geometry.GeometryCollection; -import org.elasticsearch.geometry.GeometryVisitor; -import org.elasticsearch.geometry.Line; -import org.elasticsearch.geometry.LinearRing; -import org.elasticsearch.geometry.MultiLine; -import org.elasticsearch.geometry.MultiPoint; -import org.elasticsearch.geometry.MultiPolygon; -import org.elasticsearch.geometry.Point; -import org.elasticsearch.geometry.Polygon; -import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.ShapeType; import org.elasticsearch.index.query.QueryShardException; import org.elasticsearch.index.query.SearchExecutionContext; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.function.Consumer; /** * Implemented by {@link org.elasticsearch.index.mapper.MappedFieldType} that support @@ -43,10 +29,18 @@ public interface GeoShapeQueryable { Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, LatLonGeometry... luceneGeometries); - default Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, Geometry shape) { + default Query geoShapeQuery(SearchExecutionContext context, String fieldName, ShapeRelation relation, Geometry geometry) { + final Consumer checker = relation == ShapeRelation.WITHIN ? t -> { + if (t == ShapeType.LINESTRING) { + // Line geometries and WITHIN relation is not supported by Lucene. Throw an error here + // to have same behavior for runtime fields. + throw new IllegalArgumentException("found an unsupported shape Line"); + } + } : t -> {}; final LatLonGeometry[] luceneGeometries; try { - luceneGeometries = toQuantizeLuceneGeometry(shape, relation); + // quantize the geometries to match the values on the index + luceneGeometries = LuceneGeometriesUtils.toLatLonGeometry(geometry, true, checker); } catch (IllegalArgumentException e) { throw new QueryShardException(context, "Exception creating query on Field [" + fieldName + "] " + e.getMessage(), e); } @@ -66,157 +60,4 @@ default Query geoShapeQuery( ) { return geoShapeQuery(context, fieldName, relation, shape); } - - private static double quantizeLat(double lat) { - return GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(lat)); - } - - private static double[] quantizeLats(double[] lats) { - return Arrays.stream(lats).map(GeoShapeQueryable::quantizeLat).toArray(); - } - - private static double quantizeLon(double lon) { - return GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(lon)); - } - - private static double[] quantizeLons(double[] lons) { - return Arrays.stream(lons).map(GeoShapeQueryable::quantizeLon).toArray(); - } - - /** - * transforms an Elasticsearch {@link Geometry} into a lucene {@link LatLonGeometry} and quantize - * the latitude and longitude values to match the values on the index. - */ - static LatLonGeometry[] toQuantizeLuceneGeometry(Geometry geometry, ShapeRelation relation) { - if (geometry == null) { - return new LatLonGeometry[0]; - } - if (GeometryNormalizer.needsNormalize(Orientation.CCW, geometry)) { - // make geometry lucene friendly - geometry = GeometryNormalizer.apply(Orientation.CCW, geometry); - } - if (geometry.isEmpty()) { - return new LatLonGeometry[0]; - } - final List geometries = new ArrayList<>(); - geometry.visit(new GeometryVisitor<>() { - @Override - public Void visit(Circle circle) { - if (circle.isEmpty() == false) { - geometries.add( - new org.apache.lucene.geo.Circle( - quantizeLat(circle.getLat()), - quantizeLon(circle.getLon()), - circle.getRadiusMeters() - ) - ); - } - return null; - } - - @Override - public Void visit(GeometryCollection collection) { - if (collection.isEmpty() == false) { - for (Geometry shape : collection) { - shape.visit(this); - } - } - return null; - } - - @Override - public Void visit(org.elasticsearch.geometry.Line line) { - if (line.isEmpty() == false) { - if (relation == ShapeRelation.WITHIN) { - // Line geometries and WITHIN relation is not supported by Lucene. Throw an error here - // to have same behavior for runtime fields. - throw new IllegalArgumentException("found an unsupported shape Line"); - } - geometries.add(new org.apache.lucene.geo.Line(quantizeLats(line.getLats()), quantizeLons(line.getLons()))); - } - return null; - } - - @Override - public Void visit(LinearRing ring) { - throw new IllegalArgumentException("Found an unsupported shape LinearRing"); - } - - @Override - public Void visit(MultiLine multiLine) { - if (multiLine.isEmpty() == false) { - for (Line line : multiLine) { - visit(line); - } - } - return null; - } - - @Override - public Void visit(MultiPoint multiPoint) { - if (multiPoint.isEmpty() == false) { - for (Point point : multiPoint) { - visit(point); - } - } - return null; - } - - @Override - public Void visit(MultiPolygon multiPolygon) { - if (multiPolygon.isEmpty() == false) { - for (Polygon polygon : multiPolygon) { - visit(polygon); - } - } - return null; - } - - @Override - public Void visit(Point point) { - if (point.isEmpty() == false) { - geometries.add(new org.apache.lucene.geo.Point(quantizeLat(point.getLat()), quantizeLon(point.getLon()))); - } - return null; - - } - - @Override - public Void visit(org.elasticsearch.geometry.Polygon polygon) { - if (polygon.isEmpty() == false) { - org.apache.lucene.geo.Polygon[] holes = new org.apache.lucene.geo.Polygon[polygon.getNumberOfHoles()]; - for (int i = 0; i < holes.length; i++) { - holes[i] = new org.apache.lucene.geo.Polygon( - quantizeLats(polygon.getHole(i).getY()), - quantizeLons(polygon.getHole(i).getX()) - ); - } - geometries.add( - new org.apache.lucene.geo.Polygon( - quantizeLats(polygon.getPolygon().getY()), - quantizeLons(polygon.getPolygon().getX()), - holes - ) - ); - } - return null; - } - - @Override - public Void visit(Rectangle r) { - if (r.isEmpty() == false) { - geometries.add( - new org.apache.lucene.geo.Rectangle( - quantizeLat(r.getMinLat()), - quantizeLat(r.getMaxLat()), - quantizeLon(r.getMinLon()), - quantizeLon(r.getMaxLon()) - ) - ); - } - return null; - } - }); - return geometries.toArray(new LatLonGeometry[0]); - } } diff --git a/server/src/main/java/org/elasticsearch/inference/InferenceService.java b/server/src/main/java/org/elasticsearch/inference/InferenceService.java index 235de51d22572..fdeb32de33877 100644 --- a/server/src/main/java/org/elasticsearch/inference/InferenceService.java +++ b/server/src/main/java/org/elasticsearch/inference/InferenceService.java @@ -78,7 +78,13 @@ default void init(Client client) {} * @param taskSettings Settings in the request to override the model's defaults * @param listener Inference result listener */ - void infer(Model model, List input, Map taskSettings, ActionListener listener); + void infer( + Model model, + List input, + Map taskSettings, + InputType inputType, + ActionListener listener + ); /** * Start or prepare the model for use. diff --git a/server/src/main/java/org/elasticsearch/inference/InputType.java b/server/src/main/java/org/elasticsearch/inference/InputType.java index ffc67995c1dda..19f28601409ac 100644 --- a/server/src/main/java/org/elasticsearch/inference/InputType.java +++ b/server/src/main/java/org/elasticsearch/inference/InputType.java @@ -15,9 +15,8 @@ */ public enum InputType { INGEST, - SEARCH; - - public static String NAME = "input_type"; + SEARCH, + UNSPECIFIED; @Override public String toString() { diff --git a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java index 1dae328752bdc..0795fef891f91 100644 --- a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java +++ b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java @@ -974,7 +974,7 @@ record PluginServiceInstances( repositoryService ); - final TimeValue metricsInterval = settings.getAsTime("tracing.apm.agent.metrics_interval", TimeValue.timeValueSeconds(10)); + final TimeValue metricsInterval = settings.getAsTime("telemetry.agent.metrics_interval", TimeValue.timeValueSeconds(10)); final NodeMetrics nodeMetrics = new NodeMetrics(telemetryProvider.getMeterRegistry(), nodeService, metricsInterval); final SearchService searchService = serviceProvider.newSearchService( diff --git a/server/src/main/java/org/elasticsearch/search/SearchService.java b/server/src/main/java/org/elasticsearch/search/SearchService.java index d5b2565187a3f..3b053e80d35b7 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchService.java +++ b/server/src/main/java/org/elasticsearch/search/SearchService.java @@ -1059,7 +1059,7 @@ protected SearchContext createContext( return context; } - public DefaultSearchContext createSearchContext(ShardSearchRequest request, TimeValue timeout) throws IOException { + public SearchContext createSearchContext(ShardSearchRequest request, TimeValue timeout) throws IOException { final IndexService indexService = indicesService.indexServiceSafe(request.shardId().getIndex()); final IndexShard indexShard = indexService.getShard(request.shardId().getId()); final Engine.SearcherSupplier reader = indexShard.acquireSearcherSupplier(); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/Aggregations.java b/server/src/main/java/org/elasticsearch/search/aggregations/Aggregations.java deleted file mode 100644 index 3e15488cc430b..0000000000000 --- a/server/src/main/java/org/elasticsearch/search/aggregations/Aggregations.java +++ /dev/null @@ -1,145 +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 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 or the Server - * Side Public License, v 1. - */ -package org.elasticsearch.search.aggregations; - -import org.apache.lucene.util.SetOnce; -import org.elasticsearch.common.ParsingException; -import org.elasticsearch.common.collect.Iterators; -import org.elasticsearch.common.util.Maps; -import org.elasticsearch.xcontent.ToXContentFragment; -import org.elasticsearch.xcontent.XContentBuilder; -import org.elasticsearch.xcontent.XContentParser; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; - -import static java.util.Collections.emptyMap; -import static java.util.Collections.unmodifiableMap; -import static org.elasticsearch.common.xcontent.XContentParserUtils.parseTypedKeysObject; - -/** - * Represents a set of {@link Aggregation}s - */ -public class Aggregations implements Iterable, ToXContentFragment { - - public static final String AGGREGATIONS_FIELD = "aggregations"; - - protected final List aggregations; - private Map aggregationsAsMap; - - public Aggregations(List aggregations) { - this.aggregations = aggregations; - if (aggregations.isEmpty()) { - aggregationsAsMap = emptyMap(); - } - } - - /** - * Iterates over the {@link Aggregation}s. - */ - @Override - public final Iterator iterator() { - return Iterators.map(aggregations.iterator(), p -> (Aggregation) p); - } - - /** - * The list of {@link Aggregation}s. - */ - public final List asList() { - return Collections.unmodifiableList(aggregations); - } - - /** - * Returns the {@link Aggregation}s keyed by aggregation name. - */ - public final Map asMap() { - return getAsMap(); - } - - /** - * Returns the {@link Aggregation}s keyed by aggregation name. - */ - public final Map getAsMap() { - if (aggregationsAsMap == null) { - Map newAggregationsAsMap = Maps.newMapWithExpectedSize(aggregations.size()); - for (Aggregation aggregation : aggregations) { - newAggregationsAsMap.put(aggregation.getName(), aggregation); - } - this.aggregationsAsMap = unmodifiableMap(newAggregationsAsMap); - } - return aggregationsAsMap; - } - - /** - * Returns the aggregation that is associated with the specified name. - */ - @SuppressWarnings("unchecked") - public final A get(String name) { - return (A) asMap().get(name); - } - - @Override - public final boolean equals(Object obj) { - if (obj == null || getClass() != obj.getClass()) { - return false; - } - return aggregations.equals(((Aggregations) obj).aggregations); - } - - @Override - public final int hashCode() { - return Objects.hash(getClass(), aggregations); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - if (aggregations.isEmpty()) { - return builder; - } - builder.startObject(AGGREGATIONS_FIELD); - toXContentInternal(builder, params); - return builder.endObject(); - } - - /** - * Directly write all the aggregations without their bounding object. Used by sub-aggregations (non top level aggs) - */ - public XContentBuilder toXContentInternal(XContentBuilder builder, Params params) throws IOException { - for (Aggregation aggregation : aggregations) { - aggregation.toXContent(builder, params); - } - return builder; - } - - public static Aggregations fromXContent(XContentParser parser) throws IOException { - final List aggregations = new ArrayList<>(); - XContentParser.Token token; - while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - if (token == XContentParser.Token.START_OBJECT) { - SetOnce typedAgg = new SetOnce<>(); - String currentField = parser.currentName(); - parseTypedKeysObject(parser, Aggregation.TYPED_KEYS_DELIMITER, Aggregation.class, typedAgg::set); - if (typedAgg.get() != null) { - aggregations.add(typedAgg.get()); - } else { - throw new ParsingException( - parser.getTokenLocation(), - String.format(Locale.ROOT, "Could not parse aggregation keyed as [%s]", currentField) - ); - } - } - } - return new Aggregations(aggregations); - } -} diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java b/server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java index c1c54f80987f0..0c299bce7c29d 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java @@ -7,32 +7,45 @@ */ package org.elasticsearch.search.aggregations; +import org.apache.lucene.util.SetOnce; +import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.util.Maps; import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import org.elasticsearch.search.aggregations.pipeline.SiblingPipelineAggregator; import org.elasticsearch.search.aggregations.support.AggregationPath; import org.elasticsearch.search.aggregations.support.SamplingContext; import org.elasticsearch.search.sort.SortValue; +import org.elasticsearch.xcontent.ToXContentFragment; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableMap; +import static org.elasticsearch.common.xcontent.XContentParserUtils.parseTypedKeysObject; + /** - * An internal implementation of {@link Aggregations}. + * Represents a set of {@link InternalAggregation}s */ -public final class InternalAggregations extends Aggregations implements Writeable { +public final class InternalAggregations implements Iterable, ToXContentFragment, Writeable { + + public static final String AGGREGATIONS_FIELD = "aggregations"; - public static final InternalAggregations EMPTY = new InternalAggregations(Collections.emptyList()); + public static final InternalAggregations EMPTY = new InternalAggregations(List.of()); private static final Comparator INTERNAL_AGG_COMPARATOR = (agg1, agg2) -> { if (agg1.canLeadReduction() == agg2.canLeadReduction()) { @@ -44,11 +57,115 @@ public final class InternalAggregations extends Aggregations implements Writeabl } }; + private final List aggregations; + private Map aggregationsAsMap; + /** * Constructs a new aggregation. */ private InternalAggregations(List aggregations) { - super(aggregations); + this.aggregations = aggregations; + if (aggregations.isEmpty()) { + aggregationsAsMap = Map.of(); + } + } + + /** + * Iterates over the {@link InternalAggregation}s. + */ + @Override + public Iterator iterator() { + return aggregations.iterator(); + } + + /** + * The list of {@link InternalAggregation}s. + */ + public List asList() { + return unmodifiableList(aggregations); + } + + /** + * Returns the {@link InternalAggregation}s keyed by aggregation name. + */ + public Map asMap() { + return getAsMap(); + } + + /** + * Returns the {@link InternalAggregation}s keyed by aggregation name. + */ + public Map getAsMap() { + if (aggregationsAsMap == null) { + Map newAggregationsAsMap = Maps.newMapWithExpectedSize(aggregations.size()); + for (InternalAggregation aggregation : aggregations) { + newAggregationsAsMap.put(aggregation.getName(), aggregation); + } + this.aggregationsAsMap = unmodifiableMap(newAggregationsAsMap); + } + return aggregationsAsMap; + } + + /** + * Returns the aggregation that is associated with the specified name. + */ + @SuppressWarnings("unchecked") + public A get(String name) { + return (A) asMap().get(name); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return aggregations.equals(((InternalAggregations) obj).aggregations); + } + + @Override + public int hashCode() { + return Objects.hash(getClass(), aggregations); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (aggregations.isEmpty()) { + return builder; + } + builder.startObject(AGGREGATIONS_FIELD); + toXContentInternal(builder, params); + return builder.endObject(); + } + + /** + * Directly write all the aggregations without their bounding object. Used by sub-aggregations (non top level aggs) + */ + public XContentBuilder toXContentInternal(XContentBuilder builder, Params params) throws IOException { + for (InternalAggregation aggregation : aggregations) { + aggregation.toXContent(builder, params); + } + return builder; + } + + public static InternalAggregations fromXContent(XContentParser parser) throws IOException { + final List aggregations = new ArrayList<>(); + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.START_OBJECT) { + SetOnce typedAgg = new SetOnce<>(); + String currentField = parser.currentName(); + parseTypedKeysObject(parser, Aggregation.TYPED_KEYS_DELIMITER, InternalAggregation.class, typedAgg::set); + if (typedAgg.get() != null) { + aggregations.add(typedAgg.get()); + } else { + throw new ParsingException( + parser.getTokenLocation(), + String.format(Locale.ROOT, "Could not parse aggregation keyed as [%s]", currentField) + ); + } + } + } + return new InternalAggregations(aggregations); } public static InternalAggregations from(List aggregations) { @@ -74,9 +191,8 @@ public List copyResults() { return new ArrayList<>(getInternalAggregations()); } - @SuppressWarnings("unchecked") private List getInternalAggregations() { - return (List) aggregations; + return aggregations; } /** @@ -138,12 +254,12 @@ public static InternalAggregations reduce(List aggregation // first we collect all aggregations of the same type and list them together Map> aggByName = new HashMap<>(); for (InternalAggregations aggregations : aggregationsList) { - for (Aggregation aggregation : aggregations.aggregations) { + for (InternalAggregation aggregation : aggregations.aggregations) { List aggs = aggByName.computeIfAbsent( aggregation.getName(), k -> new ArrayList<>(aggregationsList.size()) ); - aggs.add((InternalAggregation) aggregation); + aggs.add(aggregation); } } @@ -173,9 +289,7 @@ public static InternalAggregations reduce(List aggregation */ public static InternalAggregations finalizeSampling(InternalAggregations internalAggregations, SamplingContext samplingContext) { return from( - internalAggregations.aggregations.stream() - .map(agg -> ((InternalAggregation) agg).finalizeSampling(samplingContext)) - .collect(Collectors.toList()) + internalAggregations.aggregations.stream().map(agg -> agg.finalizeSampling(samplingContext)).collect(Collectors.toList()) ); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java b/server/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java index 4d519d678d96b..dda632e7aa020 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/InternalMultiBucketAggregation.java @@ -211,7 +211,7 @@ public Object getProperty(String containingAggName, List path) { if (path.isEmpty()) { return this; } - Aggregations aggregations = getAggregations(); + InternalAggregations aggregations = getAggregations(); String aggName = path.get(0); if (aggName.equals("_count")) { if (path.size() > 1) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/InternalRandomSampler.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/InternalRandomSampler.java index b444a1ef8f4d7..5b72b1396def2 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/InternalRandomSampler.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/InternalRandomSampler.java @@ -85,9 +85,7 @@ public InternalAggregation reduce(List aggregations, Aggreg InternalAggregations aggs = InternalAggregations.reduce(subAggregationsList, reduceContext); if (reduceContext.isFinalReduce() && aggs != null) { SamplingContext context = buildContext(); - aggs = InternalAggregations.from( - aggs.asList().stream().map(agg -> ((InternalAggregation) agg).finalizeSampling(context)).toList() - ); + aggs = InternalAggregations.from(aggs.asList().stream().map(agg -> agg.finalizeSampling(context)).toList()); } return newAggregation(getName(), docCount, aggs); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/BucketMetricsPipelineAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/BucketMetricsPipelineAggregator.java index 5661edce6eb89..59317944930ec 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/BucketMetricsPipelineAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/BucketMetricsPipelineAggregator.java @@ -12,8 +12,8 @@ import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.AggregationReduceContext; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; import org.elasticsearch.search.aggregations.bucket.InternalSingleBucketAggregation; import org.elasticsearch.search.aggregations.pipeline.BucketHelpers.GapPolicy; @@ -46,7 +46,7 @@ public abstract class BucketMetricsPipelineAggregator extends SiblingPipelineAgg } @Override - public final InternalAggregation doReduce(Aggregations aggregations, AggregationReduceContext context) { + public final InternalAggregation doReduce(InternalAggregations aggregations, AggregationReduceContext context) { preCollection(); List parsedPath = AggregationPath.parse(bucketsPaths()[0]).getPathElements(); for (Aggregation aggregation : aggregations) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/BucketScriptPipelineAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/BucketScriptPipelineAggregator.java index c5e52448223c0..7f18b87adce3e 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/BucketScriptPipelineAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/BucketScriptPipelineAggregator.java @@ -80,7 +80,6 @@ public InternalAggregation reduce(InternalAggregation aggregation, AggregationRe newBuckets.add(bucket); } else { final List aggs = StreamSupport.stream(bucket.getAggregations().spliterator(), false) - .map((p) -> (InternalAggregation) p) .collect(Collectors.toCollection(ArrayList::new)); InternalSimpleValue simpleValue = new InternalSimpleValue(name(), returned.doubleValue(), formatter, metadata()); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/CumulativeSumPipelineAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/CumulativeSumPipelineAggregator.java index c51c60bf24ee5..2e2c46ac0b38a 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/CumulativeSumPipelineAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/CumulativeSumPipelineAggregator.java @@ -54,7 +54,6 @@ public InternalAggregation reduce(InternalAggregation aggregation, AggregationRe } List aggs = StreamSupport.stream(bucket.getAggregations().spliterator(), false) - .map((p) -> (InternalAggregation) p) .collect(Collectors.toCollection(ArrayList::new)); aggs.add(new InternalSimpleValue(name(), sum, formatter, metadata())); Bucket newBucket = factory.createBucket(factory.getKey(bucket), bucket.getDocCount(), InternalAggregations.from(aggs)); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/SerialDiffPipelineAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/SerialDiffPipelineAggregator.java index 7225d7652b3b8..c7eb662efebd5 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/SerialDiffPipelineAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/SerialDiffPipelineAggregator.java @@ -85,7 +85,6 @@ public InternalAggregation reduce(InternalAggregation aggregation, AggregationRe double diff = thisBucketValue - lagValue; List aggs = StreamSupport.stream(bucket.getAggregations().spliterator(), false) - .map((p) -> (InternalAggregation) p) .collect(Collectors.toCollection(ArrayList::new)); aggs.add(new InternalSimpleValue(name(), diff, formatter, metadata())); newBucket = factory.createBucket(factory.getKey(bucket), bucket.getDocCount(), InternalAggregations.from(aggs)); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/SiblingPipelineAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/SiblingPipelineAggregator.java index 9c63e13afa039..7b82cd38881df 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/SiblingPipelineAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/SiblingPipelineAggregator.java @@ -9,7 +9,6 @@ package org.elasticsearch.search.aggregations.pipeline; import org.elasticsearch.search.aggregations.AggregationReduceContext; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregations; @@ -30,5 +29,5 @@ public InternalAggregation reduce(InternalAggregation aggregation, AggregationRe }); } - public abstract InternalAggregation doReduce(Aggregations aggregations, AggregationReduceContext context); + public abstract InternalAggregation doReduce(InternalAggregations aggregations, AggregationReduceContext context); } diff --git a/server/src/test/java/org/elasticsearch/action/bulk/BulkRequestBuilderTests.java b/server/src/test/java/org/elasticsearch/action/bulk/BulkRequestBuilderTests.java new file mode 100644 index 0000000000000..8843801e528a3 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/bulk/BulkRequestBuilderTests.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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.action.bulk; + +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.test.ESTestCase; + +public class BulkRequestBuilderTests extends ESTestCase { + + public void testValidation() { + BulkRequestBuilder bulkRequestBuilder = new BulkRequestBuilder(null, null); + bulkRequestBuilder.add(new IndexRequestBuilder(null, randomAlphaOfLength(10))); + bulkRequestBuilder.add(new IndexRequest()); + expectThrows(IllegalStateException.class, bulkRequestBuilder::request); + + bulkRequestBuilder = new BulkRequestBuilder(null, null); + bulkRequestBuilder.add(new IndexRequestBuilder(null, randomAlphaOfLength(10))); + bulkRequestBuilder.setTimeout(randomTimeValue()); + bulkRequestBuilder.setTimeout(TimeValue.timeValueSeconds(randomIntBetween(1, 30))); + expectThrows(IllegalStateException.class, bulkRequestBuilder::request); + + bulkRequestBuilder = new BulkRequestBuilder(null, null); + bulkRequestBuilder.add(new IndexRequestBuilder(null, randomAlphaOfLength(10))); + bulkRequestBuilder.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values()).getValue()); + bulkRequestBuilder.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values())); + expectThrows(IllegalStateException.class, bulkRequestBuilder::request); + } +} diff --git a/server/src/test/java/org/elasticsearch/action/index/IndexRequestBuilderTests.java b/server/src/test/java/org/elasticsearch/action/index/IndexRequestBuilderTests.java index e2f67d9387ff5..9af522524abc9 100644 --- a/server/src/test/java/org/elasticsearch/action/index/IndexRequestBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/action/index/IndexRequestBuilderTests.java @@ -53,16 +53,20 @@ public void testSetSource() throws Exception { indexRequestBuilder.setSource(source); assertEquals(EXPECTED_SOURCE, XContentHelper.convertToJson(indexRequestBuilder.request().source(), true)); + indexRequestBuilder = new IndexRequestBuilder(this.testClient); indexRequestBuilder.setSource(source, XContentType.JSON); assertEquals(EXPECTED_SOURCE, XContentHelper.convertToJson(indexRequestBuilder.request().source(), true)); + indexRequestBuilder = new IndexRequestBuilder(this.testClient); indexRequestBuilder.setSource("SomeKey", "SomeValue"); assertEquals(EXPECTED_SOURCE, XContentHelper.convertToJson(indexRequestBuilder.request().source(), true)); // force the Object... setter + indexRequestBuilder = new IndexRequestBuilder(this.testClient); indexRequestBuilder.setSource((Object) "SomeKey", "SomeValue"); assertEquals(EXPECTED_SOURCE, XContentHelper.convertToJson(indexRequestBuilder.request().source(), true)); + indexRequestBuilder = new IndexRequestBuilder(this.testClient); ByteArrayOutputStream docOut = new ByteArrayOutputStream(); XContentBuilder doc = XContentFactory.jsonBuilder(docOut).startObject().field("SomeKey", "SomeValue").endObject(); doc.close(); @@ -72,6 +76,7 @@ public void testSetSource() throws Exception { XContentHelper.convertToJson(indexRequestBuilder.request().source(), true, indexRequestBuilder.request().getContentType()) ); + indexRequestBuilder = new IndexRequestBuilder(this.testClient); doc = XContentFactory.jsonBuilder().startObject().field("SomeKey", "SomeValue").endObject(); doc.close(); indexRequestBuilder.setSource(doc); diff --git a/server/src/test/java/org/elasticsearch/common/geo/LuceneGeometriesUtilsTests.java b/server/src/test/java/org/elasticsearch/common/geo/LuceneGeometriesUtilsTests.java new file mode 100644 index 0000000000000..96cc73e2cff4c --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/geo/LuceneGeometriesUtilsTests.java @@ -0,0 +1,476 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.common.geo; + +import org.apache.lucene.geo.LatLonGeometry; +import org.apache.lucene.geo.XYGeometry; +import org.elasticsearch.geo.GeometryTestUtils; +import org.elasticsearch.geo.ShapeTestUtils; +import org.elasticsearch.geometry.Circle; +import org.elasticsearch.geometry.Geometry; +import org.elasticsearch.geometry.GeometryCollection; +import org.elasticsearch.geometry.Line; +import org.elasticsearch.geometry.LinearRing; +import org.elasticsearch.geometry.MultiLine; +import org.elasticsearch.geometry.MultiPoint; +import org.elasticsearch.geometry.MultiPolygon; +import org.elasticsearch.geometry.Point; +import org.elasticsearch.geometry.Polygon; +import org.elasticsearch.geometry.Rectangle; +import org.elasticsearch.geometry.ShapeType; +import org.elasticsearch.test.ESTestCase; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; + +public class LuceneGeometriesUtilsTests extends ESTestCase { + + public void testLatLonPoint() { + Point point = GeometryTestUtils.randomPoint(); + { + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(point, false, t -> assertEquals(ShapeType.POINT, t)); + assertEquals(1, geometries.length); + assertLatLonPoint(point, geometries[0]); + } + { + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(point, true, t -> assertEquals(ShapeType.POINT, t)); + assertEquals(1, geometries.length); + assertLatLonPoint(quantize(point), geometries[0]); + } + } + + public void testLatLonMultiPoint() { + MultiPoint multiPoint = GeometryTestUtils.randomMultiPoint(randomBoolean()); + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(multiPoint, false, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTIPOINT, t); + } else { + assertEquals(ShapeType.POINT, t); + } + }); + assertEquals(multiPoint.size(), geometries.length); + for (int i = 0; i < multiPoint.size(); i++) { + assertLatLonPoint(multiPoint.get(i), geometries[i]); + } + } + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(multiPoint, true, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTIPOINT, t); + } else { + assertEquals(ShapeType.POINT, t); + } + }); + assertEquals(multiPoint.size(), geometries.length); + for (int i = 0; i < multiPoint.size(); i++) { + assertLatLonPoint(quantize(multiPoint.get(i)), geometries[i]); + } + } + } + + private void assertLatLonPoint(Point point, LatLonGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.Point.class)); + org.apache.lucene.geo.Point lalonPoint = (org.apache.lucene.geo.Point) geometry; + assertThat(lalonPoint.getLon(), equalTo(point.getLon())); + assertThat(lalonPoint.getLat(), equalTo(point.getLat())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toLatLonPoint(point))); + } + + public void testXYPoint() { + Point point = ShapeTestUtils.randomPoint(); + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(point, t -> assertEquals(ShapeType.POINT, t)); + assertEquals(1, geometries.length); + assertXYPoint(point, geometries[0]); + assertThat(geometries[0], instanceOf(org.apache.lucene.geo.XYPoint.class)); + } + + public void testXYMultiPoint() { + MultiPoint multiPoint = ShapeTestUtils.randomMultiPoint(randomBoolean()); + int[] counter = new int[] { 0 }; + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(multiPoint, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTIPOINT, t); + } else { + assertEquals(ShapeType.POINT, t); + } + }); + assertEquals(multiPoint.size(), geometries.length); + for (int i = 0; i < multiPoint.size(); i++) { + assertXYPoint(multiPoint.get(i), geometries[i]); + } + } + + private void assertXYPoint(Point point, XYGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.XYPoint.class)); + org.apache.lucene.geo.XYPoint xyPoint = (org.apache.lucene.geo.XYPoint) geometry; + assertThat(xyPoint.getX(), equalTo((float) point.getX())); + assertThat(xyPoint.getY(), equalTo((float) point.getY())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toXYPoint(point))); + } + + public void testLatLonLine() { + Line line = GeometryTestUtils.randomLine(randomBoolean()); + { + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(line, false, t -> assertEquals(ShapeType.LINESTRING, t)); + assertEquals(1, geometries.length); + assertLatLonLine(line, geometries[0]); + } + { + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(line, true, t -> assertEquals(ShapeType.LINESTRING, t)); + assertEquals(1, geometries.length); + assertLatLonLine(quantize(line), geometries[0]); + } + } + + public void testLatLonMultiLine() { + MultiLine multiLine = GeometryTestUtils.randomMultiLine(randomBoolean()); + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(multiLine, false, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTILINESTRING, t); + } else { + assertEquals(ShapeType.LINESTRING, t); + } + }); + assertEquals(multiLine.size(), geometries.length); + for (int i = 0; i < multiLine.size(); i++) { + assertLatLonLine(multiLine.get(i), geometries[i]); + } + } + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(multiLine, true, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTILINESTRING, t); + } else { + assertEquals(ShapeType.LINESTRING, t); + } + }); + assertEquals(multiLine.size(), geometries.length); + for (int i = 0; i < multiLine.size(); i++) { + assertLatLonLine(quantize(multiLine.get(i)), geometries[i]); + } + } + } + + private void assertLatLonLine(Line line, LatLonGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.Line.class)); + org.apache.lucene.geo.Line lalonLine = (org.apache.lucene.geo.Line) geometry; + assertThat(lalonLine.getLons(), equalTo(line.getLons())); + assertThat(lalonLine.getLats(), equalTo(line.getLats())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toLatLonLine(line))); + } + + public void testXYLine() { + Line line = ShapeTestUtils.randomLine(randomBoolean()); + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(line, t -> assertEquals(ShapeType.LINESTRING, t)); + assertEquals(1, geometries.length); + assertXYLine(line, geometries[0]); + } + + public void testXYMultiLine() { + MultiLine multiLine = ShapeTestUtils.randomMultiLine(randomBoolean()); + int[] counter = new int[] { 0 }; + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(multiLine, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTILINESTRING, t); + } else { + assertEquals(ShapeType.LINESTRING, t); + } + }); + assertEquals(multiLine.size(), geometries.length); + for (int i = 0; i < multiLine.size(); i++) { + assertXYLine(multiLine.get(i), geometries[i]); + } + } + + private void assertXYLine(Line line, XYGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.XYLine.class)); + org.apache.lucene.geo.XYLine xyLine = (org.apache.lucene.geo.XYLine) geometry; + assertThat(xyLine.getX(), equalTo(LuceneGeometriesUtils.doubleArrayToFloatArray(line.getLons()))); + assertThat(xyLine.getY(), equalTo(LuceneGeometriesUtils.doubleArrayToFloatArray(line.getLats()))); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toXYLine(line))); + } + + public void testLatLonPolygon() { + Polygon polygon = validRandomPolygon(randomBoolean()); + { + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(polygon, false, t -> assertEquals(ShapeType.POLYGON, t)); + assertEquals(1, geometries.length); + assertLatLonPolygon(polygon, geometries[0]); + } + { + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(polygon, true, t -> assertEquals(ShapeType.POLYGON, t)); + assertEquals(1, geometries.length); + assertLatLonPolygon(quantize(polygon), geometries[0]); + } + } + + public void testLatLonMultiPolygon() { + MultiPolygon multiPolygon = validRandomMultiPolygon(randomBoolean()); + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(multiPolygon, false, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTIPOLYGON, t); + } else { + assertEquals(ShapeType.POLYGON, t); + } + }); + assertEquals(multiPolygon.size(), geometries.length); + for (int i = 0; i < multiPolygon.size(); i++) { + assertLatLonPolygon(multiPolygon.get(i), geometries[i]); + } + } + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(multiPolygon, true, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTIPOLYGON, t); + } else { + assertEquals(ShapeType.POLYGON, t); + } + }); + assertEquals(multiPolygon.size(), geometries.length); + for (int i = 0; i < multiPolygon.size(); i++) { + assertLatLonPolygon(quantize(multiPolygon.get(i)), geometries[i]); + } + } + } + + private void assertLatLonPolygon(Polygon polygon, LatLonGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.Polygon.class)); + org.apache.lucene.geo.Polygon lalonPolygon = (org.apache.lucene.geo.Polygon) geometry; + assertThat(lalonPolygon.getPolyLons(), equalTo(polygon.getPolygon().getLons())); + assertThat(lalonPolygon.getPolyLats(), equalTo(polygon.getPolygon().getLats())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toLatLonPolygon(polygon))); + } + + public void testXYPolygon() { + Polygon polygon = ShapeTestUtils.randomPolygon(randomBoolean()); + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(polygon, t -> assertEquals(ShapeType.POLYGON, t)); + assertEquals(1, geometries.length); + assertXYPolygon(polygon, geometries[0]); + } + + public void testXYMultiPolygon() { + MultiPolygon multiPolygon = ShapeTestUtils.randomMultiPolygon(randomBoolean()); + int[] counter = new int[] { 0 }; + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(multiPolygon, t -> { + if (counter[0]++ == 0) { + assertEquals(ShapeType.MULTIPOLYGON, t); + } else { + assertEquals(ShapeType.POLYGON, t); + } + }); + assertEquals(multiPolygon.size(), geometries.length); + for (int i = 0; i < multiPolygon.size(); i++) { + assertXYPolygon(multiPolygon.get(i), geometries[i]); + } + } + + private void assertXYPolygon(Polygon polygon, XYGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.XYPolygon.class)); + org.apache.lucene.geo.XYPolygon xyPolygon = (org.apache.lucene.geo.XYPolygon) geometry; + assertThat(xyPolygon.getPolyX(), equalTo(LuceneGeometriesUtils.doubleArrayToFloatArray(polygon.getPolygon().getX()))); + assertThat(xyPolygon.getPolyY(), equalTo(LuceneGeometriesUtils.doubleArrayToFloatArray(polygon.getPolygon().getY()))); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toXYPolygon(polygon))); + } + + public void testLatLonGeometryCollection() { + boolean hasZ = randomBoolean(); + Point point = GeometryTestUtils.randomPoint(hasZ); + Line line = GeometryTestUtils.randomLine(hasZ); + Polygon polygon = validRandomPolygon(hasZ); + GeometryCollection geometryCollection = new GeometryCollection<>(List.of(point, line, polygon)); + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(geometryCollection, false, t -> { + if (counter[0] == 0) { + assertEquals(ShapeType.GEOMETRYCOLLECTION, t); + } else if (counter[0] == 1) { + assertEquals(ShapeType.POINT, t); + } else if (counter[0] == 2) { + assertEquals(ShapeType.LINESTRING, t); + } else if (counter[0] == 3) { + assertEquals(ShapeType.POLYGON, t); + } else { + fail("Unexpected counter value"); + } + counter[0]++; + }); + assertEquals(geometryCollection.size(), geometries.length); + assertLatLonPoint(point, geometries[0]); + assertLatLonLine(line, geometries[1]); + assertLatLonPolygon(polygon, geometries[2]); + } + { + int[] counter = new int[] { 0 }; + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(geometryCollection, true, t -> { + if (counter[0] == 0) { + assertEquals(ShapeType.GEOMETRYCOLLECTION, t); + } else if (counter[0] == 1) { + assertEquals(ShapeType.POINT, t); + } else if (counter[0] == 2) { + assertEquals(ShapeType.LINESTRING, t); + } else if (counter[0] == 3) { + assertEquals(ShapeType.POLYGON, t); + } else { + fail("Unexpected counter value"); + } + counter[0]++; + }); + assertEquals(geometryCollection.size(), geometries.length); + assertLatLonPoint(quantize(point), geometries[0]); + assertLatLonLine(quantize(line), geometries[1]); + assertLatLonPolygon(quantize(polygon), geometries[2]); + } + } + + public void testXYGeometryCollection() { + boolean hasZ = randomBoolean(); + Point point = ShapeTestUtils.randomPoint(hasZ); + Line line = ShapeTestUtils.randomLine(hasZ); + Polygon polygon = ShapeTestUtils.randomPolygon(hasZ); + GeometryCollection geometryCollection = new GeometryCollection<>(List.of(point, line, polygon)); + int[] counter = new int[] { 0 }; + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(geometryCollection, t -> { + if (counter[0] == 0) { + assertEquals(ShapeType.GEOMETRYCOLLECTION, t); + } else if (counter[0] == 1) { + assertEquals(ShapeType.POINT, t); + } else if (counter[0] == 2) { + assertEquals(ShapeType.LINESTRING, t); + } else if (counter[0] == 3) { + assertEquals(ShapeType.POLYGON, t); + } else { + fail("Unexpected counter value"); + } + counter[0]++; + }); + assertEquals(geometryCollection.size(), geometries.length); + assertXYPoint(point, geometries[0]); + assertXYLine(line, geometries[1]); + assertXYPolygon(polygon, geometries[2]); + } + + private Polygon validRandomPolygon(boolean hasLat) { + return randomValueOtherThanMany( + polygon -> GeometryNormalizer.needsNormalize(Orientation.CCW, polygon), + () -> GeometryTestUtils.randomPolygon(hasLat) + ); + } + + public void testLatLonRectangle() { + Rectangle rectangle = GeometryTestUtils.randomRectangle(); + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(rectangle, false, t -> assertEquals(ShapeType.ENVELOPE, t)); + assertEquals(1, geometries.length); + assertLatLonRectangle(rectangle, geometries[0]); + } + + private void assertLatLonRectangle(Rectangle rectangle, LatLonGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.Rectangle.class)); + org.apache.lucene.geo.Rectangle lalonRectangle = (org.apache.lucene.geo.Rectangle) geometry; + assertThat(lalonRectangle.maxLon, equalTo(rectangle.getMaxLon())); + assertThat(lalonRectangle.minLon, equalTo(rectangle.getMinLon())); + assertThat(lalonRectangle.maxLat, equalTo(rectangle.getMaxLat())); + assertThat(lalonRectangle.minLat, equalTo(rectangle.getMinLat())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toLatLonRectangle(rectangle))); + } + + public void testXYRectangle() { + Rectangle rectangle = ShapeTestUtils.randomRectangle(); + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(rectangle, t -> assertEquals(ShapeType.ENVELOPE, t)); + assertEquals(1, geometries.length); + assertXYRectangle(rectangle, geometries[0]); + } + + private void assertXYRectangle(Rectangle rectangle, XYGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.XYRectangle.class)); + org.apache.lucene.geo.XYRectangle xyRectangle = (org.apache.lucene.geo.XYRectangle) geometry; + assertThat(xyRectangle.maxX, equalTo((float) rectangle.getMaxX())); + assertThat(xyRectangle.minX, equalTo((float) rectangle.getMinX())); + assertThat(xyRectangle.maxY, equalTo((float) rectangle.getMaxY())); + assertThat(xyRectangle.minY, equalTo((float) rectangle.getMinY())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toXYRectangle(rectangle))); + } + + public void testLatLonCircle() { + Circle circle = GeometryTestUtils.randomCircle(randomBoolean()); + LatLonGeometry[] geometries = LuceneGeometriesUtils.toLatLonGeometry(circle, false, t -> assertEquals(ShapeType.CIRCLE, t)); + assertEquals(1, geometries.length); + assertLatLonCircle(circle, geometries[0]); + } + + private void assertLatLonCircle(Circle circle, LatLonGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.Circle.class)); + org.apache.lucene.geo.Circle lalonCircle = (org.apache.lucene.geo.Circle) geometry; + assertThat(lalonCircle.getLon(), equalTo(circle.getLon())); + assertThat(lalonCircle.getLat(), equalTo(circle.getLat())); + assertThat(lalonCircle.getRadius(), equalTo(circle.getRadiusMeters())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toLatLonCircle(circle))); + } + + public void testXYCircle() { + Circle circle = ShapeTestUtils.randomCircle(randomBoolean()); + XYGeometry[] geometries = LuceneGeometriesUtils.toXYGeometry(circle, t -> assertEquals(ShapeType.CIRCLE, t)); + assertEquals(1, geometries.length); + assertXYCircle(circle, geometries[0]); + } + + private void assertXYCircle(Circle circle, XYGeometry geometry) { + assertThat(geometry, instanceOf(org.apache.lucene.geo.XYCircle.class)); + org.apache.lucene.geo.XYCircle xyCircle = (org.apache.lucene.geo.XYCircle) geometry; + assertThat(xyCircle.getX(), equalTo((float) circle.getX())); + assertThat(xyCircle.getY(), equalTo((float) circle.getY())); + assertThat(xyCircle.getRadius(), equalTo((float) circle.getRadiusMeters())); + assertThat(geometry, equalTo(LuceneGeometriesUtils.toXYCircle(circle))); + } + + private MultiPolygon validRandomMultiPolygon(boolean hasLat) { + // make sure we don't generate a polygon that gets splitted across the dateline + return randomValueOtherThanMany( + multiPolygon -> GeometryNormalizer.needsNormalize(Orientation.CCW, multiPolygon), + () -> GeometryTestUtils.randomMultiPolygon(hasLat) + ); + } + + private Point quantize(Point point) { + return new Point(GeoUtils.quantizeLon(point.getLon()), GeoUtils.quantizeLat(point.getLat())); + } + + private Line quantize(Line line) { + return new Line( + LuceneGeometriesUtils.LATLON_QUANTIZER.quantizeLons(line.getLons()), + LuceneGeometriesUtils.LATLON_QUANTIZER.quantizeLats(line.getLats()) + ); + } + + private Polygon quantize(Polygon polygon) { + List holes = new ArrayList<>(polygon.getNumberOfHoles()); + for (int i = 0; i < polygon.getNumberOfHoles(); i++) { + holes.add(quantize(polygon.getHole(i))); + } + return new Polygon(quantize(polygon.getPolygon()), holes); + } + + private LinearRing quantize(LinearRing linearRing) { + return new LinearRing( + LuceneGeometriesUtils.LATLON_QUANTIZER.quantizeLons(linearRing.getLons()), + LuceneGeometriesUtils.LATLON_QUANTIZER.quantizeLats(linearRing.getLats()) + ); + } +} diff --git a/server/src/test/java/org/elasticsearch/index/shard/ShardGetServiceTests.java b/server/src/test/java/org/elasticsearch/index/shard/ShardGetServiceTests.java index 9e1be4c629b4a..20493ee576c0a 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/ShardGetServiceTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/ShardGetServiceTests.java @@ -24,6 +24,7 @@ import org.elasticsearch.xcontent.XContentType; import java.io.IOException; +import java.util.Arrays; import java.util.function.LongSupplier; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_PRIMARY_TERM; @@ -114,6 +115,20 @@ public void testGetFromTranslogWithSyntheticSource() throws IOException { runGetFromTranslogWithOptions(docToIndex, sourceOptions, expectedFetchedSource, "\"long\"", 7L, true); } + public void testGetFromTranslogWithDenseVector() throws IOException { + float[] vector = new float[2048]; + for (int i = 0; i < vector.length; i++) { + vector[i] = randomFloat(); + } + String docToIndex = Strings.format(""" + { + "bar": %s, + "foo": "foo" + } + """, Arrays.toString(vector)); + runGetFromTranslogWithOptions(docToIndex, "\"enabled\": true", docToIndex, "\"text\"", "foo", "\"dense_vector\"", false); + } + private void runGetFromTranslogWithOptions( String docToIndex, String sourceOptions, @@ -122,23 +137,48 @@ private void runGetFromTranslogWithOptions( Object expectedFooVal, boolean sourceOnlyFetchCreatesInMemoryReader ) throws IOException { - IndexMetadata metadata = IndexMetadata.builder("test").putMapping(Strings.format(""" - { - "properties": { - "foo": { - "type": %s, - "store": true - }, - "bar": { "type": %s } - }, - "_source": { %s } - } - }""", fieldType, fieldType, sourceOptions)).settings(indexSettings(IndexVersion.current(), 1, 1)).primaryTerm(0, 1).build(); + runGetFromTranslogWithOptions( + docToIndex, + sourceOptions, + expectedResult, + fieldType, + expectedFooVal, + fieldType, + sourceOnlyFetchCreatesInMemoryReader + ); + } + + private void runGetFromTranslogWithOptions( + String docToIndex, + String sourceOptions, + String expectedResult, + String fieldTypeFoo, + Object expectedFooVal, + String fieldTypeBar, + boolean sourceOnlyFetchCreatesInMemoryReader + ) throws IOException { + IndexMetadata metadata = IndexMetadata.builder("test") + .putMapping(Strings.format(""" + { + "properties": { + "foo": { + "type": %s, + "store": true + }, + "bar": { "type": %s } + }, + "_source": { %s } + } + }""", fieldTypeFoo, fieldTypeBar, sourceOptions)) + .settings(indexSettings(IndexVersion.current(), 1, 1)) + .primaryTerm(0, 1) + .build(); IndexShard primary = newShard(new ShardId(metadata.getIndex(), 0), true, "n1", metadata, EngineTestCase.randomReaderWrapper()); recoverShardFromStore(primary); LongSupplier translogInMemorySegmentCount = ((InternalEngine) primary.getEngine()).translogInMemorySegmentsCount::get; long translogInMemorySegmentCountExpected = 0; - indexDoc(primary, "test", "0", docToIndex); + Engine.IndexResult res = indexDoc(primary, "test", "0", docToIndex); + assertTrue(res.isCreated()); assertTrue(primary.getEngine().refreshNeeded()); GetResult testGet = primary.getService().getForUpdate("0", UNASSIGNED_SEQ_NO, UNASSIGNED_PRIMARY_TERM); assertFalse(testGet.getFields().containsKey(RoutingFieldMapper.NAME)); diff --git a/server/src/test/java/org/elasticsearch/indices/breaker/HierarchyCircuitBreakerServiceTests.java b/server/src/test/java/org/elasticsearch/indices/breaker/HierarchyCircuitBreakerServiceTests.java index 15f6d0ed377fa..8d2255df9e7e8 100644 --- a/server/src/test/java/org/elasticsearch/indices/breaker/HierarchyCircuitBreakerServiceTests.java +++ b/server/src/test/java/org/elasticsearch/indices/breaker/HierarchyCircuitBreakerServiceTests.java @@ -628,14 +628,17 @@ void overLimitTriggered(boolean leader) { })).toList(); threads.forEach(Thread::start); - safeAwait(barrier); int iterationCount = randomIntBetween(1, 5); + int lastIterationTriggerCount = leaderTriggerCount.get(); + + safeAwait(barrier); for (int i = 0; i < iterationCount; ++i) { memoryUsage.set(randomLongBetween(0, 100)); safeAwait(countDown.get()); assertThat(leaderTriggerCount.get(), lessThanOrEqualTo(i + 1)); - assertThat(leaderTriggerCount.get(), greaterThanOrEqualTo(i / 2 + 1)); + assertThat(leaderTriggerCount.get(), greaterThanOrEqualTo(lastIterationTriggerCount)); + lastIterationTriggerCount = leaderTriggerCount.get(); time.addAndGet(randomLongBetween(interval, interval * 2)); countDown.set(new CountDownLatch(randomIntBetween(1, 20))); } diff --git a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java index bfb62e6fed197..057f253f0e50e 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java +++ b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java @@ -1252,7 +1252,7 @@ public void testCreateSearchContext() throws IOException { nowInMillis, clusterAlias ); - try (DefaultSearchContext searchContext = service.createSearchContext(request, new TimeValue(System.currentTimeMillis()))) { + try (SearchContext searchContext = service.createSearchContext(request, new TimeValue(System.currentTimeMillis()))) { SearchShardTarget searchShardTarget = searchContext.shardTarget(); SearchExecutionContext searchExecutionContext = searchContext.getSearchExecutionContext(); String expectedIndexName = clusterAlias == null ? index : clusterAlias + ":" + index; diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationsTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationsTests.java index 70378268dde30..b5927d71bd782 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationsTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/InternalAggregationsTests.java @@ -247,7 +247,7 @@ public void testNonFinalReduceTopLevelPipelineAggs() { ); List aggs = singletonList(InternalAggregations.from(Collections.singletonList(terms))); InternalAggregations reducedAggs = InternalAggregations.topLevelReduce(aggs, maxBucketReduceContext().forPartialReduction()); - assertEquals(1, reducedAggs.aggregations.size()); + assertEquals(1, reducedAggs.asList().size()); } public void testFinalReduceTopLevelPipelineAggs() { @@ -268,7 +268,7 @@ public void testFinalReduceTopLevelPipelineAggs() { InternalAggregations aggs = InternalAggregations.from(Collections.singletonList(terms)); InternalAggregations reducedAggs = InternalAggregations.topLevelReduce(List.of(aggs), maxBucketReduceContext().forFinalReduction()); - assertEquals(2, reducedAggs.aggregations.size()); + assertEquals(2, reducedAggs.asList().size()); } private AggregationReduceContext.Builder maxBucketReduceContext() { @@ -317,7 +317,7 @@ private void writeToAndReadFrom(InternalAggregations aggregations, TransportVers try (StreamInput in = new NamedWriteableAwareStreamInput(StreamInput.wrap(serializedAggs.bytes), registry)) { in.setTransportVersion(version); InternalAggregations deserialized = InternalAggregations.readFrom(in); - assertEquals(aggregations.aggregations, deserialized.aggregations); + assertEquals(aggregations.asList(), deserialized.asList()); if (iteration < 2) { writeToAndReadFrom(deserialized, version, iteration + 1); } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/RareTermsAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/RareTermsAggregatorTests.java index ad7a6c47ef5e4..2d240f74b91a4 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/RareTermsAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/RareTermsAggregatorTests.java @@ -43,9 +43,9 @@ import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationBuilder; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.AggregatorTestCase; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; import org.elasticsearch.search.aggregations.MultiBucketConsumerService; import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation; @@ -178,7 +178,7 @@ public void testEmbeddedMaxAgg() throws IOException { assertThat(bucket.getKey(), equalTo(1L)); assertThat(bucket.getDocCount(), equalTo(1L)); - Aggregations children = bucket.getAggregations(); + InternalAggregations children = bucket.getAggregations(); assertThat(children.asList().size(), equalTo(1)); assertThat(children.asList().get(0).getName(), equalTo("the_max")); assertThat(((Max) (children.asList().get(0))).value(), equalTo(1.0)); @@ -192,7 +192,7 @@ public void testEmbeddedMaxAgg() throws IOException { assertThat(bucket.getKey(), equalTo("1")); assertThat(bucket.getDocCount(), equalTo(1L)); - Aggregations children = bucket.getAggregations(); + InternalAggregations children = bucket.getAggregations(); assertThat(children.asList().size(), equalTo(1)); assertThat(children.asList().get(0).getName(), equalTo("the_max")); assertThat(((Max) (children.asList().get(0))).value(), equalTo(1.0)); @@ -292,7 +292,7 @@ public void testNestedTerms() throws IOException { assertThat(bucket.getKey(), equalTo(1L)); assertThat(bucket.getDocCount(), equalTo(1L)); - Aggregations children = bucket.getAggregations(); + InternalAggregations children = bucket.getAggregations(); assertThat(children.asList().size(), equalTo(1)); assertThat(children.asList().get(0).getName(), equalTo("the_terms")); assertThat(((Terms) (children.asList().get(0))).getBuckets().size(), equalTo(1)); @@ -308,7 +308,7 @@ public void testNestedTerms() throws IOException { assertThat(bucket.getKey(), equalTo("1")); assertThat(bucket.getDocCount(), equalTo(1L)); - Aggregations children = bucket.getAggregations(); + InternalAggregations children = bucket.getAggregations(); assertThat(children.asList().size(), equalTo(1)); assertThat(children.asList().get(0).getName(), equalTo("the_terms")); assertThat(((Terms) (children.asList().get(0))).getBuckets().size(), equalTo(1)); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/AvgBucketAggregatorTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/AvgBucketAggregatorTests.java index 05fcb45c71ee9..8e6d9b5788c54 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/AvgBucketAggregatorTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/pipeline/AvgBucketAggregatorTests.java @@ -25,10 +25,9 @@ import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.NumberFieldMapper; import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.search.aggregations.Aggregation; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.AggregatorTestCase; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; @@ -119,12 +118,12 @@ public void testSameAggNames() throws IOException { // Finally, reduce the pipeline agg PipelineAggregator avgBucketAgg = avgBucketBuilder.createInternal(Collections.emptyMap()); - List reducedAggs = new ArrayList<>(2); + List reducedAggs = new ArrayList<>(2); // Histo has to go first to exercise the bug reducedAggs.add(histogramResult); reducedAggs.add(avgResult); - Aggregations aggregations = new Aggregations(reducedAggs); + InternalAggregations aggregations = InternalAggregations.from(reducedAggs); InternalAggregation pipelineResult = ((AvgBucketPipelineAggregator) avgBucketAgg).doReduce(aggregations, null); assertNotNull(pipelineResult); } @@ -174,10 +173,10 @@ public void testComplicatedBucketPath() throws IOException { // Finally, reduce the pipeline agg PipelineAggregator avgBucketAgg = avgBucketBuilder.createInternal(Collections.emptyMap()); - List reducedAggs = new ArrayList<>(4); + List reducedAggs = new ArrayList<>(4); reducedAggs.add(filterResult); - Aggregations aggregations = new Aggregations(reducedAggs); + InternalAggregations aggregations = InternalAggregations.from(reducedAggs); InternalAggregation pipelineResult = ((AvgBucketPipelineAggregator) avgBucketAgg).doReduce(aggregations, null); assertNotNull(pipelineResult); } diff --git a/server/src/test/java/org/elasticsearch/search/query/QuerySearchResultTests.java b/server/src/test/java/org/elasticsearch/search/query/QuerySearchResultTests.java index 516ffeb9418bd..949f4b9e0677b 100644 --- a/server/src/test/java/org/elasticsearch/search/query/QuerySearchResultTests.java +++ b/server/src/test/java/org/elasticsearch/search/query/QuerySearchResultTests.java @@ -22,7 +22,7 @@ import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.SearchShardTarget; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.InternalAggregationsTests; import org.elasticsearch.search.internal.AliasFilter; import org.elasticsearch.search.internal.ShardSearchContextId; @@ -115,8 +115,8 @@ public void testSerialization() throws Exception { assertEquals(querySearchResult.hasAggs(), deserialized.hasAggs()); if (deserialized.hasAggs()) { assertThat(deserialized.aggregations().isSerialized(), is(delayed)); - Aggregations aggs = querySearchResult.consumeAggs(); - Aggregations deserializedAggs = deserialized.consumeAggs(); + InternalAggregations aggs = querySearchResult.consumeAggs(); + InternalAggregations deserializedAggs = deserialized.consumeAggs(); assertEquals(aggs.asList(), deserializedAggs.asList()); assertThat(deserialized.aggregations(), is(nullValue())); } diff --git a/server/src/test/java/org/elasticsearch/test/search/aggregations/bucket/SharedSignificantTermsTestMethods.java b/server/src/test/java/org/elasticsearch/test/search/aggregations/bucket/SharedSignificantTermsTestMethods.java index 0b84f14c56ecb..93dd7bc618756 100644 --- a/server/src/test/java/org/elasticsearch/test/search/aggregations/bucket/SharedSignificantTermsTestMethods.java +++ b/server/src/test/java/org/elasticsearch/test/search/aggregations/bucket/SharedSignificantTermsTestMethods.java @@ -9,7 +9,7 @@ package org.elasticsearch.test.search.aggregations.bucket; import org.elasticsearch.action.index.IndexRequestBuilder; -import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.bucket.terms.SignificantTerms; import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; import org.elasticsearch.search.aggregations.bucket.terms.Terms; @@ -55,7 +55,7 @@ private static void checkSignificantTermsAggregationCorrect(ESIntegTestCase test StringTerms classes = response.getAggregations().get("class"); Assert.assertThat(classes.getBuckets().size(), equalTo(2)); for (Terms.Bucket classBucket : classes.getBuckets()) { - Map aggs = classBucket.getAggregations().asMap(); + Map aggs = classBucket.getAggregations().asMap(); Assert.assertTrue(aggs.containsKey("sig_terms")); SignificantTerms agg = (SignificantTerms) aggs.get("sig_terms"); Assert.assertThat(agg.getBuckets().size(), equalTo(1)); diff --git a/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/MetricsApmIT.java b/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/MetricsApmIT.java index 70ce86a1d91a6..9980c0a25a5dd 100644 --- a/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/MetricsApmIT.java +++ b/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/MetricsApmIT.java @@ -46,8 +46,8 @@ public class MetricsApmIT extends ESRestTestCase { .module("test-apm-integration") .module("apm") .setting("telemetry.metrics.enabled", "true") - .setting("tracing.apm.agent.metrics_interval", "1s") - .setting("tracing.apm.agent.server_url", "http://127.0.0.1:" + mockApmServer.getPort()) + .setting("telemetry.agent.metrics_interval", "1s") + .setting("telemetry.agent.server_url", "http://127.0.0.1:" + mockApmServer.getPort()) .build(); @Override diff --git a/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/TracesApmIT.java b/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/TracesApmIT.java index 79816114cc38f..93ed525b38b59 100644 --- a/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/TracesApmIT.java +++ b/test/external-modules/apm-integration/src/javaRestTest/java/org/elasticsearch/test/apmintegration/TracesApmIT.java @@ -51,9 +51,9 @@ public class TracesApmIT extends ESRestTestCase { .module("test-apm-integration") .module("apm") .setting("telemetry.metrics.enabled", "false") - .setting("tracing.apm.enabled", "true") - .setting("tracing.apm.agent.metrics_interval", "1s") - .setting("tracing.apm.agent.server_url", "http://127.0.0.1:" + mockApmServer.getPort()) + .setting("telemetry.tracing.enabled", "true") + .setting("telemetry.agent.metrics_interval", "1s") + .setting("telemetry.agent.server_url", "http://127.0.0.1:" + mockApmServer.getPort()) .build(); @Override diff --git a/test/framework/src/main/java/org/elasticsearch/search/MockSearchService.java b/test/framework/src/main/java/org/elasticsearch/search/MockSearchService.java index c1c4d70e0b906..aa1889e15d594 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/MockSearchService.java +++ b/test/framework/src/main/java/org/elasticsearch/search/MockSearchService.java @@ -11,6 +11,7 @@ import org.elasticsearch.action.search.SearchShardTask; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.util.BigArrays; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.indices.ExecutorSelector; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.indices.breaker.CircuitBreakerService; @@ -41,6 +42,7 @@ public static class TestPlugin extends Plugin {} private static final Map ACTIVE_SEARCH_CONTEXTS = new ConcurrentHashMap<>(); private Consumer onPutContext = context -> {}; + private Consumer onRemoveContext = context -> {}; private Consumer onCreateSearchContext = context -> {}; @@ -110,6 +112,7 @@ protected void putReaderContext(ReaderContext context) { protected ReaderContext removeReaderContext(long id) { final ReaderContext removed = super.removeReaderContext(id); if (removed != null) { + onRemoveContext.accept(removed); removeActiveContext(removed); } return removed; @@ -119,6 +122,10 @@ public void setOnPutContext(Consumer onPutContext) { this.onPutContext = onPutContext; } + public void setOnRemoveContext(Consumer onRemoveContext) { + this.onRemoveContext = onRemoveContext; + } + public void setOnCreateSearchContext(Consumer onCreateSearchContext) { this.onCreateSearchContext = onCreateSearchContext; } @@ -141,6 +148,14 @@ protected SearchContext createContext( return searchContext; } + @Override + public SearchContext createSearchContext(ShardSearchRequest request, TimeValue timeout) throws IOException { + SearchContext searchContext = super.createSearchContext(request, timeout); + onPutContext.accept(searchContext.readerContext()); + searchContext.addReleasable(() -> onRemoveContext.accept(searchContext.readerContext())); + return searchContext; + } + public void setOnCheckCancelled(Function onCheckCancelled) { this.onCheckCancelled = onCheckCancelled; } diff --git a/x-pack/libs/es-opensaml-security-api/build.gradle b/x-pack/libs/es-opensaml-security-api/build.gradle index 95064f6730133..416be7a785dd5 100644 --- a/x-pack/libs/es-opensaml-security-api/build.gradle +++ b/x-pack/libs/es-opensaml-security-api/build.gradle @@ -7,6 +7,7 @@ */ apply plugin: 'elasticsearch.build' +apply plugin: 'elasticsearch.publish' apply plugin: 'com.github.johnrengelman.shadow' dependencies { diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/cumulativecardinality/CumulativeCardinalityPipelineAggregator.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/cumulativecardinality/CumulativeCardinalityPipelineAggregator.java index c100d57dfb3d1..e71cedf381886 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/cumulativecardinality/CumulativeCardinalityPipelineAggregator.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/cumulativecardinality/CumulativeCardinalityPipelineAggregator.java @@ -59,7 +59,6 @@ public InternalAggregation reduce(InternalAggregation aggregation, AggregationRe } List aggs = StreamSupport.stream(bucket.getAggregations().spliterator(), false) - .map((p) -> (InternalAggregation) p) .collect(Collectors.toList()); aggs.add(new InternalSimpleLongValue(name(), cardinality, formatter, metadata())); Bucket newBucket = factory.createBucket(factory.getKey(bucket), bucket.getDocCount(), InternalAggregations.from(aggs)); diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/movingPercentiles/MovingPercentilesPipelineAggregator.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/movingPercentiles/MovingPercentilesPipelineAggregator.java index 3dc364b1ec131..663299df54f8b 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/movingPercentiles/MovingPercentilesPipelineAggregator.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/movingPercentiles/MovingPercentilesPipelineAggregator.java @@ -101,11 +101,7 @@ private void reduceTDigest( } if (state != null) { - List aggs = bucket.getAggregations() - .asList() - .stream() - .map((p) -> (InternalAggregation) p) - .collect(Collectors.toList()); + List aggs = bucket.getAggregations().asList().stream().collect(Collectors.toList()); aggs.add(new InternalTDigestPercentiles(name(), config.keys, state, config.keyed, config.formatter, metadata())); newBucket = factory.createBucket(factory.getKey(bucket), bucket.getDocCount(), InternalAggregations.from(aggs)); } @@ -151,11 +147,7 @@ private void reduceHDR( } if (state != null) { - List aggs = bucket.getAggregations() - .asList() - .stream() - .map((p) -> (InternalAggregation) p) - .collect(Collectors.toList()); + List aggs = new ArrayList<>(bucket.getAggregations().asList()); aggs.add(new InternalHDRPercentiles(name(), config.keys, state, config.keyed, config.formatter, metadata())); newBucket = factory.createBucket(factory.getKey(bucket), bucket.getDocCount(), InternalAggregations.from(aggs)); } diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/normalize/NormalizePipelineAggregator.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/normalize/NormalizePipelineAggregator.java index edbd750cdcc52..adb8b691a83ea 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/normalize/NormalizePipelineAggregator.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/normalize/NormalizePipelineAggregator.java @@ -71,7 +71,6 @@ public InternalAggregation reduce(InternalAggregation aggregation, AggregationRe } List aggs = StreamSupport.stream(bucket.getAggregations().spliterator(), false) - .map((p) -> (InternalAggregation) p) .collect(Collectors.toList()); aggs.add(new InternalSimpleValue(name(), normalizedBucketValue, formatter, metadata())); InternalMultiBucketAggregation.InternalBucket newBucket = originalAgg.createBucket(InternalAggregations.from(aggs), bucket); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/inference/action/InferenceAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/inference/action/InferenceAction.java index 1fc477927d7b7..2ddba3446d79a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/inference/action/InferenceAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/inference/action/InferenceAction.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.core.inference.action; import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; @@ -60,6 +61,8 @@ public static Request parseRequest(String inferenceEntityId, String taskType, XC Request.Builder builder = PARSER.apply(parser, null); builder.setInferenceEntityId(inferenceEntityId); builder.setTaskType(taskType); + // For rest requests we won't know what the input type is + builder.setInputType(InputType.UNSPECIFIED); return builder.build(); } @@ -96,7 +99,7 @@ public Request(StreamInput in) throws IOException { if (in.getTransportVersion().onOrAfter(TransportVersions.ML_INFERENCE_REQUEST_INPUT_TYPE_ADDED)) { this.inputType = in.readEnum(InputType.class); } else { - this.inputType = InputType.INGEST; + this.inputType = InputType.UNSPECIFIED; } } @@ -146,11 +149,22 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(input.get(0)); } out.writeGenericMap(taskSettings); + // in version ML_INFERENCE_REQUEST_INPUT_TYPE_ADDED the input type enum was added, so we only want to write the enum if we're + // at that version or later if (out.getTransportVersion().onOrAfter(TransportVersions.ML_INFERENCE_REQUEST_INPUT_TYPE_ADDED)) { - out.writeEnum(inputType); + out.writeEnum(getInputTypeToWrite(out.getTransportVersion())); } } + private InputType getInputTypeToWrite(TransportVersion version) { + // in version ML_INFERENCE_REQUEST_INPUT_TYPE_UNSPECIFIED_ADDED the UNSPECIFIED value was added, so if we're before that + // version other nodes won't know about it, so set it to INGEST instead + if (version.before(TransportVersions.ML_INFERENCE_REQUEST_INPUT_TYPE_UNSPECIFIED_ADDED) && inputType == InputType.UNSPECIFIED) { + return InputType.INGEST; + } + return inputType; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -173,6 +187,7 @@ public static class Builder { private TaskType taskType; private String inferenceEntityId; private List input; + private InputType inputType = InputType.UNSPECIFIED; private Map taskSettings = Map.of(); private Builder() {} @@ -197,13 +212,18 @@ public Builder setInput(List input) { return this; } + public Builder setInputType(InputType inputType) { + this.inputType = inputType; + return this; + } + public Builder setTaskSettings(Map taskSettings) { this.taskSettings = taskSettings; return this; } public Request build() { - return new Request(taskType, inferenceEntityId, input, taskSettings, InputType.INGEST); + return new Request(taskType, inferenceEntityId, input, taskSettings, inputType); } } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/EvaluationMetric.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/EvaluationMetric.java index bc24ca129635e..a31e83d8246fd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/EvaluationMetric.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/EvaluationMetric.java @@ -10,7 +10,7 @@ import org.elasticsearch.common.io.stream.NamedWriteable; import org.elasticsearch.core.Tuple; import org.elasticsearch.search.aggregations.AggregationBuilder; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.xcontent.ToXContentObject; @@ -45,7 +45,7 @@ public interface EvaluationMetric extends ToXContentObject, NamedWriteable { * Processes given aggregations as a step towards computing result * @param aggs aggregations from {@link SearchResponse} */ - void process(Aggregations aggs); + void process(InternalAggregations aggs); /** * Gets the evaluation result for this metric. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Accuracy.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Accuracy.java index 346996a742cf1..0a1778a6a6f30 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Accuracy.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Accuracy.java @@ -14,7 +14,7 @@ import org.elasticsearch.script.Script; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; import org.elasticsearch.xcontent.ConstructingObjectParser; @@ -124,7 +124,7 @@ public final Tuple, List> a } @Override - public void process(Aggregations aggs) { + public void process(InternalAggregations aggs) { if (overallAccuracy.get() == null && aggs.get(OVERALL_ACCURACY_AGG_NAME) instanceof NumericMetricsAggregation.SingleValue) { NumericMetricsAggregation.SingleValue overallAccuracyAgg = aggs.get(OVERALL_ACCURACY_AGG_NAME); overallAccuracy.set(overallAccuracyAgg.value()); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AucRoc.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AucRoc.java index f7e80e7fcf972..5bdd85e34a7c7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AucRoc.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AucRoc.java @@ -15,7 +15,7 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.search.aggregations.bucket.nested.Nested; @@ -175,7 +175,7 @@ public Tuple, List> aggs( } @Override - public void process(Aggregations aggs) { + public void process(InternalAggregations aggs) { if (result.get() != null) { return; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java index 5279f026722af..e385e9d9d78d2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrix.java @@ -16,8 +16,8 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.BucketOrder; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.filter.Filters; import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator.KeyedFilter; @@ -183,7 +183,7 @@ public final Tuple, List> a } @Override - public void process(Aggregations aggs) { + public void process(InternalAggregations aggs) { if (topActualClassNames.get() == null && aggs.get(aggName(STEP_1_AGGREGATE_BY_ACTUAL_CLASS)) != null) { Terms termsAgg = aggs.get(aggName(STEP_1_AGGREGATE_BY_ACTUAL_CLASS)); topActualClassNames.set(termsAgg.getBuckets().stream().map(Terms.Bucket::getKeyAsString).sorted().collect(Collectors.toList())); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java index 5b9cffd48f284..6936164ceb07e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Precision.java @@ -16,8 +16,8 @@ import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.BucketOrder; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.PipelineAggregatorBuilders; import org.elasticsearch.search.aggregations.bucket.filter.Filters; @@ -140,7 +140,7 @@ public final Tuple, List> a } @Override - public void process(Aggregations aggs) { + public void process(InternalAggregations aggs) { final Aggregation classNamesAgg = aggs.get(ACTUAL_CLASSES_NAMES_AGG_NAME); if (topActualClassNames.get() == null && classNamesAgg instanceof Terms topActualClassesAgg) { if (topActualClassesAgg.getSumOfOtherDocCounts() > 0) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java index 646af7848cf23..6aaabc13c86c9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/Recall.java @@ -15,8 +15,8 @@ import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.BucketOrder; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.PipelineAggregatorBuilders; import org.elasticsearch.search.aggregations.bucket.terms.Terms; @@ -119,7 +119,7 @@ public final Tuple, List> a } @Override - public void process(Aggregations aggs) { + public void process(InternalAggregations aggs) { final Aggregation byClass = aggs.get(BY_ACTUAL_CLASS_AGG_NAME); final Aggregation avgRecall = aggs.get(AVG_RECALL_AGG_NAME); if (result.get() == null diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/AbstractConfusionMatrixMetric.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/AbstractConfusionMatrixMetric.java index 99d7853ddab3a..83b6fe58498e5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/AbstractConfusionMatrixMetric.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/AbstractConfusionMatrixMetric.java @@ -15,7 +15,7 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentBuilder; @@ -92,7 +92,7 @@ public Tuple, List> aggs( } @Override - public void process(Aggregations aggs) { + public void process(InternalAggregations aggs) { result = evaluate(aggs); } @@ -103,7 +103,7 @@ public Optional getResult() { protected abstract List aggsAt(String actualField, String predictedProbabilityField); - protected abstract EvaluationMetricResult evaluate(Aggregations aggs); + protected abstract EvaluationMetricResult evaluate(InternalAggregations aggs); enum Condition { TP(true, true), diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/AucRoc.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/AucRoc.java index e15148b5fd7e1..c06edb66b301a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/AucRoc.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/AucRoc.java @@ -14,7 +14,7 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.xcontent.ConstructingObjectParser; @@ -155,7 +155,7 @@ public Tuple, List> aggs( } @Override - public void process(Aggregations aggs) { + public void process(InternalAggregations aggs) { if (result.get() != null) { return; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/ConfusionMatrix.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/ConfusionMatrix.java index bf13b882f3e98..f902274fdc7f2 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/ConfusionMatrix.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/ConfusionMatrix.java @@ -9,7 +9,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.search.aggregations.AggregationBuilder; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; @@ -87,7 +87,7 @@ protected List aggsAt(String actualField, String predictedPr } @Override - public EvaluationMetricResult evaluate(Aggregations aggs) { + public EvaluationMetricResult evaluate(InternalAggregations aggs) { long[] tp = new long[thresholds.length]; long[] fp = new long[thresholds.length]; long[] tn = new long[thresholds.length]; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/Precision.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/Precision.java index fcbf1c6216239..d2364faaf7859 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/Precision.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/Precision.java @@ -8,7 +8,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.search.aggregations.AggregationBuilder; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; @@ -83,7 +83,7 @@ protected List aggsAt(String actualField, String predictedPr } @Override - public EvaluationMetricResult evaluate(Aggregations aggs) { + public EvaluationMetricResult evaluate(InternalAggregations aggs) { double[] precisions = new double[thresholds.length]; for (int i = 0; i < thresholds.length; i++) { double threshold = thresholds[i]; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/Recall.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/Recall.java index 07f0cdbb6c17a..8291bcdac30c1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/Recall.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/Recall.java @@ -8,7 +8,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.search.aggregations.AggregationBuilder; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; @@ -83,7 +83,7 @@ protected List aggsAt(String actualField, String predictedPr } @Override - public EvaluationMetricResult evaluate(Aggregations aggs) { + public EvaluationMetricResult evaluate(InternalAggregations aggs) { double[] recalls = new double[thresholds.length]; for (int i = 0; i < thresholds.length; i++) { double threshold = thresholds[i]; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/Huber.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/Huber.java index 28802148220b6..4e8ba57ffbc95 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/Huber.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/Huber.java @@ -14,7 +14,7 @@ import org.elasticsearch.script.Script; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; import org.elasticsearch.xcontent.ConstructingObjectParser; @@ -118,7 +118,7 @@ public Tuple, List> aggs( } @Override - public void process(Aggregations aggs) { + public void process(InternalAggregations aggs) { NumericMetricsAggregation.SingleValue value = aggs.get(AGG_NAME); result = value == null ? new Result(0.0) : new Result(value.value()); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredError.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredError.java index 2a50383494abe..d43ff3e5390b9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredError.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredError.java @@ -13,7 +13,7 @@ import org.elasticsearch.script.Script; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; import org.elasticsearch.xcontent.ObjectParser; @@ -97,7 +97,7 @@ public Tuple, List> aggs( } @Override - public void process(Aggregations aggs) { + public void process(InternalAggregations aggs) { NumericMetricsAggregation.SingleValue value = aggs.get(AGG_NAME); result = value == null ? new Result(0.0) : new Result(value.value()); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredLogarithmicError.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredLogarithmicError.java index 9ca3e39d53c4b..00afd2acff200 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredLogarithmicError.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredLogarithmicError.java @@ -14,7 +14,7 @@ import org.elasticsearch.script.Script; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.NumericMetricsAggregation; import org.elasticsearch.xcontent.ConstructingObjectParser; @@ -113,7 +113,7 @@ public Tuple, List> aggs( } @Override - public void process(Aggregations aggs) { + public void process(InternalAggregations aggs) { NumericMetricsAggregation.SingleValue value = aggs.get(AGG_NAME); result = value == null ? new Result(0.0) : new Result(value.value()); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquared.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquared.java index fa41661771f62..2e1251abecda1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquared.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquared.java @@ -13,7 +13,7 @@ import org.elasticsearch.script.Script; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.aggregations.metrics.ExtendedStats; import org.elasticsearch.search.aggregations.metrics.ExtendedStatsAggregationBuilder; @@ -100,7 +100,7 @@ public Tuple, List> aggs( } @Override - public void process(Aggregations aggs) { + public void process(InternalAggregations aggs) { NumericMetricsAggregation.SingleValue residualSumOfSquares = aggs.get(SS_RES); ExtendedStats extendedStats = aggs.get(ExtendedStatsAggregationBuilder.NAME + "_actual"); // extendedStats.getVariance() is the statistical sumOfSquares divided by count diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java index 4637ca7edd8dd..dd2baca058102 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java @@ -333,7 +333,7 @@ public class ClusterPrivilegeResolver { public static final NamedClusterPrivilege WRITE_CONNECTOR_SECRETS = new ActionClusterPrivilege( "write_connector_secrets", - Set.of("cluster:admin/xpack/connector/secret/post") + Set.of("cluster:admin/xpack/connector/secret/post", "cluster:admin/xpack/connector/secret/delete") ); private static final Map VALUES = sortByAccessLevel( diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/MockAggregations.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/MockAggregations.java index 519cd06204dab..368823d0f64af 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/MockAggregations.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/MockAggregations.java @@ -7,13 +7,12 @@ package org.elasticsearch.xpack.core.ml.dataframe.evaluation; import org.elasticsearch.search.aggregations.InternalAggregations; -import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.search.aggregations.bucket.filter.Filters; +import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter; import org.elasticsearch.search.aggregations.bucket.filter.InternalFilters; import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; -import org.elasticsearch.search.aggregations.bucket.terms.Terms; -import org.elasticsearch.search.aggregations.metrics.ExtendedStats; import org.elasticsearch.search.aggregations.metrics.InternalCardinality; +import org.elasticsearch.search.aggregations.metrics.InternalExtendedStats; import org.elasticsearch.search.aggregations.metrics.InternalNumericMetricsAggregation; import java.util.Collections; @@ -25,7 +24,7 @@ public final class MockAggregations { - public static Terms mockTerms(String name) { + public static StringTerms mockTerms(String name) { return mockTerms(name, Collections.emptyList(), 0); } @@ -44,7 +43,7 @@ public static StringTerms.Bucket mockTermsBucket(String key, InternalAggregation return bucket; } - public static Filters mockFilters(String name) { + public static InternalFilters mockFilters(String name) { return mockFilters(name, Collections.emptyList()); } @@ -68,8 +67,8 @@ public static InternalFilters.InternalBucket mockFiltersBucket(String key, long return bucket; } - public static Filter mockFilter(String name, long docCount) { - Filter agg = mock(Filter.class); + public static InternalFilter mockFilter(String name, long docCount) { + InternalFilter agg = mock(InternalFilter.class); when(agg.getName()).thenReturn(name); when(agg.getDocCount()).thenReturn(docCount); return agg; @@ -89,8 +88,8 @@ public static InternalCardinality mockCardinality(String name, long value) { return agg; } - public static ExtendedStats mockExtendedStats(String name, double variance, long count) { - ExtendedStats agg = mock(ExtendedStats.class); + public static InternalExtendedStats mockExtendedStats(String name, double variance, long count) { + InternalExtendedStats agg = mock(InternalExtendedStats.class); when(agg.getName()).thenReturn(name); when(agg.getVariance()).thenReturn(variance); when(agg.getCount()).thenReturn(count); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyTests.java index 50277084cba1e..3bf1ff171e422 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/AccuracyTests.java @@ -8,7 +8,6 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; @@ -129,7 +128,7 @@ public void testProcess() { } public void testProcess_GivenCardinalityTooHigh() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( List.of( mockTerms( "accuracy_" + MulticlassConfusionMatrix.STEP_1_AGGREGATE_BY_ACTUAL_CLASS, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationTests.java index cc101626667b2..b797961e58b33 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/ClassificationTests.java @@ -19,7 +19,7 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.aggregations.AggregationBuilder; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.AbstractXContentSerializingTestCase; @@ -65,7 +65,7 @@ protected NamedXContentRegistry xContentRegistry() { public static Classification createRandom() { List metrics = randomSubsetOf( Arrays.asList( - AccuracyTests.createRandom(), + // AccuracyTests.createRandom(), AucRocTests.createRandom(), PrecisionTests.createRandom(), RecallTests.createRandom(), @@ -341,7 +341,7 @@ public Tuple, List> aggs( } @Override - public void process(Aggregations aggs) { + public void process(InternalAggregations aggs) { if (result != null) { return; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java index 5ab62fd628199..e8e71b8721c26 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/MulticlassConfusionMatrixTests.java @@ -10,7 +10,6 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.core.Tuple; import org.elasticsearch.search.aggregations.AggregationBuilder; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.test.AbstractXContentSerializingTestCase; @@ -102,7 +101,7 @@ public void testAggs() { } public void testProcess() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( List.of( mockTerms( MulticlassConfusionMatrix.STEP_1_AGGREGATE_BY_ACTUAL_CLASS, @@ -172,7 +171,7 @@ public void testProcess() { } public void testProcess_OtherClassesCountGreaterThanZero() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( List.of( mockTerms( MulticlassConfusionMatrix.STEP_1_AGGREGATE_BY_ACTUAL_CLASS, @@ -257,7 +256,7 @@ public void testProcess_MoreThanTwoStepsNeeded() { mockCardinality(MulticlassConfusionMatrix.STEP_1_CARDINALITY_OF_ACTUAL_CLASS, 2L) ) ); - Aggregations aggsStep2 = new Aggregations( + InternalAggregations aggsStep2 = InternalAggregations.from( List.of( mockFilters( MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_ACTUAL_CLASS, @@ -302,7 +301,7 @@ public void testProcess_MoreThanTwoStepsNeeded() { ) ) ); - Aggregations aggsStep3 = new Aggregations( + InternalAggregations aggsStep3 = InternalAggregations.from( List.of( mockFilters( MulticlassConfusionMatrix.STEP_2_AGGREGATE_BY_ACTUAL_CLASS, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/PrecisionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/PrecisionTests.java index d4261d81fea2c..f44efff28c034 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/PrecisionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/PrecisionTests.java @@ -8,7 +8,7 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationFields; @@ -63,7 +63,7 @@ public static Precision createRandom() { } public void testProcess() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList( mockTerms(Precision.ACTUAL_CLASSES_NAMES_AGG_NAME), mockFilters(Precision.BY_PREDICTED_CLASS_AGG_NAME), @@ -81,7 +81,7 @@ public void testProcess() { public void testProcess_GivenMissingAgg() { { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList(mockFilters(Precision.BY_PREDICTED_CLASS_AGG_NAME), mockSingleValue("some_other_single_metric_agg", 0.2377)) ); Precision precision = new Precision(); @@ -89,7 +89,7 @@ public void testProcess_GivenMissingAgg() { assertThat(precision.getResult(), isEmpty()); } { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList( mockSingleValue(Precision.AVG_PRECISION_AGG_NAME, 0.8123), mockSingleValue("some_other_single_metric_agg", 0.2377) @@ -103,7 +103,7 @@ public void testProcess_GivenMissingAgg() { public void testProcess_GivenAggOfWrongType() { { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList(mockFilters(Precision.BY_PREDICTED_CLASS_AGG_NAME), mockFilters(Precision.AVG_PRECISION_AGG_NAME)) ); Precision precision = new Precision(); @@ -111,7 +111,7 @@ public void testProcess_GivenAggOfWrongType() { assertThat(precision.getResult(), isEmpty()); } { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList( mockSingleValue(Precision.BY_PREDICTED_CLASS_AGG_NAME, 1.0), mockSingleValue(Precision.AVG_PRECISION_AGG_NAME, 0.8123) @@ -124,7 +124,7 @@ public void testProcess_GivenAggOfWrongType() { } public void testProcess_GivenCardinalityTooHigh() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Collections.singletonList(mockTerms(Precision.ACTUAL_CLASSES_NAMES_AGG_NAME, Collections.emptyList(), 1)) ); Precision precision = new Precision(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/RecallTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/RecallTests.java index 5f446083612df..8ba6e48082b71 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/RecallTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/classification/RecallTests.java @@ -8,7 +8,7 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationFields; @@ -62,7 +62,7 @@ public static Recall createRandom() { } public void testProcess() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList( mockTerms(Recall.BY_ACTUAL_CLASS_AGG_NAME), mockSingleValue(Recall.AVG_RECALL_AGG_NAME, 0.8123), @@ -79,7 +79,7 @@ public void testProcess() { public void testProcess_GivenMissingAgg() { { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList(mockTerms(Recall.BY_ACTUAL_CLASS_AGG_NAME), mockSingleValue("some_other_single_metric_agg", 0.2377)) ); Recall recall = new Recall(); @@ -87,7 +87,7 @@ public void testProcess_GivenMissingAgg() { assertThat(recall.getResult(), isEmpty()); } { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList(mockSingleValue(Recall.AVG_RECALL_AGG_NAME, 0.8123), mockSingleValue("some_other_single_metric_agg", 0.2377)) ); Recall recall = new Recall(); @@ -98,7 +98,7 @@ public void testProcess_GivenMissingAgg() { public void testProcess_GivenAggOfWrongType() { { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList(mockTerms(Recall.BY_ACTUAL_CLASS_AGG_NAME), mockTerms(Recall.AVG_RECALL_AGG_NAME)) ); Recall recall = new Recall(); @@ -106,7 +106,7 @@ public void testProcess_GivenAggOfWrongType() { assertThat(recall.getResult(), isEmpty()); } { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList(mockSingleValue(Recall.BY_ACTUAL_CLASS_AGG_NAME, 1.0), mockSingleValue(Recall.AVG_RECALL_AGG_NAME, 0.8123)) ); Recall recall = new Recall(); @@ -116,7 +116,7 @@ public void testProcess_GivenAggOfWrongType() { } public void testProcess_GivenCardinalityTooHigh() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList( mockTerms(Recall.BY_ACTUAL_CLASS_AGG_NAME, Collections.emptyList(), 1), mockSingleValue(Recall.AVG_RECALL_AGG_NAME, 0.8123) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/ConfusionMatrixTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/ConfusionMatrixTests.java index acbd647f7bfa2..1557bd71f98b5 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/ConfusionMatrixTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/ConfusionMatrixTests.java @@ -8,7 +8,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -53,7 +53,7 @@ public static ConfusionMatrix createRandom() { } public void testEvaluate() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList( mockFilter("confusion_matrix_at_0.25_TP", 1L), mockFilter("confusion_matrix_at_0.25_FP", 2L), diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/PrecisionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/PrecisionTests.java index 299aa76f05fde..bc198eaf3c7db 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/PrecisionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/PrecisionTests.java @@ -8,7 +8,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -53,7 +53,7 @@ public static Precision createRandom() { } public void testEvaluate() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList( mockFilter("precision_at_0.25_TP", 1L), mockFilter("precision_at_0.25_FP", 4L), @@ -73,7 +73,9 @@ public void testEvaluate() { } public void testEvaluate_GivenZeroTpAndFp() { - Aggregations aggs = new Aggregations(Arrays.asList(mockFilter("precision_at_1.0_TP", 0L), mockFilter("precision_at_1.0_FP", 0L))); + InternalAggregations aggs = InternalAggregations.from( + Arrays.asList(mockFilter("precision_at_1.0_TP", 0L), mockFilter("precision_at_1.0_FP", 0L)) + ); Precision precision = new Precision(Arrays.asList(1.0)); EvaluationMetricResult result = precision.evaluate(aggs); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/RecallTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/RecallTests.java index fb4ab46675eca..569b73417414e 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/RecallTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/outlierdetection/RecallTests.java @@ -8,7 +8,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -53,7 +53,7 @@ public static Recall createRandom() { } public void testEvaluate() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList( mockFilter("recall_at_0.25_TP", 1L), mockFilter("recall_at_0.25_FN", 4L), @@ -73,7 +73,9 @@ public void testEvaluate() { } public void testEvaluate_GivenZeroTpAndFp() { - Aggregations aggs = new Aggregations(Arrays.asList(mockFilter("recall_at_1.0_TP", 0L), mockFilter("recall_at_1.0_FN", 0L))); + InternalAggregations aggs = InternalAggregations.from( + Arrays.asList(mockFilter("recall_at_1.0_TP", 0L), mockFilter("recall_at_1.0_FN", 0L)) + ); Recall recall = new Recall(Arrays.asList(1.0)); EvaluationMetricResult result = recall.evaluate(aggs); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/HuberTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/HuberTests.java index 8e7f4ddd36253..4a8485e8d138f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/HuberTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/HuberTests.java @@ -8,7 +8,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -47,7 +47,7 @@ public static Huber createRandom() { } public void testEvaluate() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList(mockSingleValue("regression_huber", 0.8123), mockSingleValue("some_other_single_metric_agg", 0.2377)) ); @@ -60,7 +60,9 @@ public void testEvaluate() { } public void testEvaluate_GivenMissingAggs() { - Aggregations aggs = new Aggregations(Collections.singletonList(mockSingleValue("some_other_single_metric_agg", 0.2377))); + InternalAggregations aggs = InternalAggregations.from( + Collections.singletonList(mockSingleValue("some_other_single_metric_agg", 0.2377)) + ); Huber huber = new Huber((Double) null); huber.process(aggs); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredErrorTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredErrorTests.java index c6c0d00dd240f..551a5f017c120 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredErrorTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredErrorTests.java @@ -8,7 +8,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -47,7 +47,7 @@ public static MeanSquaredError createRandom() { } public void testEvaluate() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList(mockSingleValue("regression_mse", 0.8123), mockSingleValue("some_other_single_metric_agg", 0.2377)) ); @@ -60,7 +60,9 @@ public void testEvaluate() { } public void testEvaluate_GivenMissingAggs() { - Aggregations aggs = new Aggregations(Collections.singletonList(mockSingleValue("some_other_single_metric_agg", 0.2377))); + InternalAggregations aggs = InternalAggregations.from( + Collections.singletonList(mockSingleValue("some_other_single_metric_agg", 0.2377)) + ); MeanSquaredError mse = new MeanSquaredError(); mse.process(aggs); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredLogarithmicErrorTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredLogarithmicErrorTests.java index beb39e46fa5f1..d2bb30fb169b1 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredLogarithmicErrorTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/MeanSquaredLogarithmicErrorTests.java @@ -8,7 +8,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -47,7 +47,7 @@ public static MeanSquaredLogarithmicError createRandom() { } public void testEvaluate() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList(mockSingleValue("regression_msle", 0.8123), mockSingleValue("some_other_single_metric_agg", 0.2377)) ); @@ -60,7 +60,9 @@ public void testEvaluate() { } public void testEvaluate_GivenMissingAggs() { - Aggregations aggs = new Aggregations(Collections.singletonList(mockSingleValue("some_other_single_metric_agg", 0.2377))); + InternalAggregations aggs = InternalAggregations.from( + Collections.singletonList(mockSingleValue("some_other_single_metric_agg", 0.2377)) + ); MeanSquaredLogarithmicError msle = new MeanSquaredLogarithmicError((Double) null); msle.process(aggs); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquaredTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquaredTests.java index 644979379703c..710810d2d168e 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquaredTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/evaluation/regression/RSquaredTests.java @@ -8,7 +8,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.test.AbstractXContentSerializingTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ml.dataframe.evaluation.EvaluationMetricResult; @@ -48,7 +48,7 @@ public static RSquared createRandom() { } public void testEvaluate() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList( mockSingleValue("residual_sum_of_squares", 10_111), mockExtendedStats("extended_stats_actual", 155.23, 1000), @@ -66,7 +66,7 @@ public void testEvaluate() { } public void testEvaluateWithZeroCount() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList( mockSingleValue("residual_sum_of_squares", 0), mockExtendedStats("extended_stats_actual", 0.0, 0), @@ -83,7 +83,7 @@ public void testEvaluateWithZeroCount() { } public void testEvaluateWithSingleCountZeroVariance() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList( mockSingleValue("residual_sum_of_squares", 1), mockExtendedStats("extended_stats_actual", 0.0, 1), @@ -100,7 +100,9 @@ public void testEvaluateWithSingleCountZeroVariance() { } public void testEvaluate_GivenMissingAggs() { - Aggregations aggs = new Aggregations(Collections.singletonList(mockSingleValue("some_other_single_metric_agg", 0.2377))); + InternalAggregations aggs = InternalAggregations.from( + (Collections.singletonList(mockSingleValue("some_other_single_metric_agg", 0.2377))) + ); RSquared rSquared = new RSquared(); rSquared.process(aggs); @@ -110,7 +112,7 @@ public void testEvaluate_GivenMissingAggs() { } public void testEvaluate_GivenMissingExtendedStatsAgg() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList(mockSingleValue("some_other_single_metric_agg", 0.2377), mockSingleValue("residual_sum_of_squares", 0.2377)) ); @@ -122,7 +124,7 @@ public void testEvaluate_GivenMissingExtendedStatsAgg() { } public void testEvaluate_GivenMissingResidualSumOfSquaresAgg() { - Aggregations aggs = new Aggregations( + InternalAggregations aggs = InternalAggregations.from( Arrays.asList(mockSingleValue("some_other_single_metric_agg", 0.2377), mockExtendedStats("extended_stats_actual", 100, 50)) ); diff --git a/x-pack/plugin/downsample/src/test/java/org/elasticsearch/xpack/downsample/DownsampleActionSingleNodeTests.java b/x-pack/plugin/downsample/src/test/java/org/elasticsearch/xpack/downsample/DownsampleActionSingleNodeTests.java index 95de6e3ab2027..28eb9ae66a4e0 100644 --- a/x-pack/plugin/downsample/src/test/java/org/elasticsearch/xpack/downsample/DownsampleActionSingleNodeTests.java +++ b/x-pack/plugin/downsample/src/test/java/org/elasticsearch/xpack/downsample/DownsampleActionSingleNodeTests.java @@ -60,9 +60,9 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchResponseUtils; -import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationBuilder; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; import org.elasticsearch.search.aggregations.bucket.histogram.InternalDateHistogram; @@ -1059,7 +1059,7 @@ private RolloverResponse rollover(String dataStreamName) throws ExecutionExcepti return response; } - private Aggregations aggregate(final String index, AggregationBuilder aggregationBuilder) { + private InternalAggregations aggregate(final String index, AggregationBuilder aggregationBuilder) { var resp = client().prepareSearch(index).addAggregation(aggregationBuilder).get(); try { return resp.getAggregations(); @@ -1138,8 +1138,8 @@ private void assertDownsampleIndexAggregations( Map labelFields ) { final AggregationBuilder aggregations = buildAggregations(config, metricFields, labelFields, config.getTimestampField()); - Aggregations origResp = aggregate(sourceIndex, aggregations); - Aggregations downsampleResp = aggregate(downsampleIndex, aggregations); + InternalAggregations origResp = aggregate(sourceIndex, aggregations); + InternalAggregations downsampleResp = aggregate(downsampleIndex, aggregations); assertEquals(origResp.asMap().keySet(), downsampleResp.asMap().keySet()); StringTerms originalTsIdTermsAggregation = (StringTerms) origResp.getAsMap().values().stream().toList().get(0); @@ -1164,25 +1164,25 @@ private void assertDownsampleIndexAggregations( InternalDateHistogram.Bucket downsampleDateHistogramBucket = downsampleDateHistogramBuckets.get(i); assertEquals(originalDateHistogramBucket.getKeyAsString(), downsampleDateHistogramBucket.getKeyAsString()); - Aggregations originalAggregations = originalDateHistogramBucket.getAggregations(); - Aggregations downsampleAggregations = downsampleDateHistogramBucket.getAggregations(); + InternalAggregations originalAggregations = originalDateHistogramBucket.getAggregations(); + InternalAggregations downsampleAggregations = downsampleDateHistogramBucket.getAggregations(); assertEquals(originalAggregations.asList().size(), downsampleAggregations.asList().size()); - List nonTopHitsOriginalAggregations = originalAggregations.asList() + List nonTopHitsOriginalAggregations = originalAggregations.asList() .stream() .filter(agg -> agg.getType().equals("top_hits") == false) .toList(); - List nonTopHitsDownsampleAggregations = downsampleAggregations.asList() + List nonTopHitsDownsampleAggregations = downsampleAggregations.asList() .stream() .filter(agg -> agg.getType().equals("top_hits") == false) .toList(); assertEquals(nonTopHitsOriginalAggregations, nonTopHitsDownsampleAggregations); - List topHitsOriginalAggregations = originalAggregations.asList() + List topHitsOriginalAggregations = originalAggregations.asList() .stream() .filter(agg -> agg.getType().equals("top_hits")) .toList(); - List topHitsDownsampleAggregations = downsampleAggregations.asList() + List topHitsDownsampleAggregations = downsampleAggregations.asList() .stream() .filter(agg -> agg.getType().equals("top_hits")) .toList(); @@ -1224,7 +1224,7 @@ private void assertDownsampleIndexAggregations( ); Object originalLabelValue = originalHit.getDocumentFields().values().stream().toList().get(0).getValue(); Object downsampleLabelValue = downsampleHit.getDocumentFields().values().stream().toList().get(0).getValue(); - Optional labelAsMetric = nonTopHitsOriginalAggregations.stream() + Optional labelAsMetric = nonTopHitsOriginalAggregations.stream() .filter(agg -> agg.getName().equals("metric_" + downsampleTopHits.getName())) .findFirst(); // NOTE: this check is possible only if the label can be indexed as a metric (the label is a numeric field) diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/335_connector_update_configuration.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/335_connector_update_configuration.yml index df4a640a0495d..5a7ab14dc6386 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/335_connector_update_configuration.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/335_connector_update_configuration.yml @@ -57,7 +57,7 @@ setup: connector_id: test-connector body: configuration: - some_field: + some_new_field: default_value: null depends_on: - field: some_field @@ -92,20 +92,22 @@ setup: connector.get: connector_id: test-connector - - match: { configuration.some_field.value: 456 } + - is_false: configuration.some_field # configuration.some_field doesn't exist + + - match: { configuration.some_new_field.value: 456 } - match: { status: configured } - - match: { configuration.some_field.validations.0.constraint: [123, 456, 789] } - - match: { configuration.some_field.validations.0.type: included_in } - - match: { configuration.some_field.validations.1.constraint: ["string 1", "string 2", "string 3"] } - - match: { configuration.some_field.validations.1.type: included_in } - - match: { configuration.some_field.validations.2.constraint: 0 } - - match: { configuration.some_field.validations.2.type: greater_than } - - match: { configuration.some_field.validations.3.constraint: 42 } - - match: { configuration.some_field.validations.3.type: less_than } - - match: { configuration.some_field.validations.4.constraint: int } - - match: { configuration.some_field.validations.4.type: list_type } - - match: { configuration.some_field.validations.5.constraint: "\\d+" } - - match: { configuration.some_field.validations.5.type: regex } + - match: { configuration.some_new_field.validations.0.constraint: [123, 456, 789] } + - match: { configuration.some_new_field.validations.0.type: included_in } + - match: { configuration.some_new_field.validations.1.constraint: ["string 1", "string 2", "string 3"] } + - match: { configuration.some_new_field.validations.1.type: included_in } + - match: { configuration.some_new_field.validations.2.constraint: 0 } + - match: { configuration.some_new_field.validations.2.type: greater_than } + - match: { configuration.some_new_field.validations.3.constraint: 42 } + - match: { configuration.some_new_field.validations.3.type: less_than } + - match: { configuration.some_new_field.validations.4.constraint: int } + - match: { configuration.some_new_field.validations.4.type: list_type } + - match: { configuration.some_new_field.validations.5.constraint: "\\d+" } + - match: { configuration.some_new_field.validations.5.type: regex } --- "Update Connector Configuration with null tooltip": diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/510_connector_secret_get.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/510_connector_secret_get.yml index 4b2d3777ffe9d..8fd676bb977b6 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/510_connector_secret_get.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/510_connector_secret_get.yml @@ -53,7 +53,7 @@ setup: catch: unauthorized --- -'Get connector secret - Missing secret id': +'Get connector secret - Secret does not exist': - do: connector_secret.get: id: non-existing-secret-id diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/520_connector_secret_delete.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/520_connector_secret_delete.yml new file mode 100644 index 0000000000000..ed50fc55a81e0 --- /dev/null +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/520_connector_secret_delete.yml @@ -0,0 +1,71 @@ +setup: + - skip: + version: " - 8.12.99" + reason: Introduced in 8.13.0 + +--- +'Delete connector secret - admin': + - do: + connector_secret.post: + body: + value: my-secret + - set: { id: id } + - match: { id: $id } + + - do: + connector_secret.delete: + id: $id + - match: { deleted: true } + + - do: + connector_secret.get: + id: $id + catch: missing + +--- +'Delete connector secret - user with privileges': + - skip: + features: headers + + - do: + headers: { Authorization: "Basic ZW50c2VhcmNoLXVzZXI6ZW50c2VhcmNoLXVzZXItcGFzc3dvcmQ=" } # user + connector_secret.post: + body: + value: my-secret + - set: { id: id } + - match: { id: $id } + - do: + headers: { Authorization: "Basic ZW50c2VhcmNoLXVzZXI6ZW50c2VhcmNoLXVzZXItcGFzc3dvcmQ=" } # user + connector_secret.delete: + id: $id + - match: { deleted: true } + - do: + headers: { Authorization: "Basic ZW50c2VhcmNoLXVzZXI6ZW50c2VhcmNoLXVzZXItcGFzc3dvcmQ=" } # user + connector_secret.get: + id: $id + catch: missing + +--- +'Delete connector secret - user without privileges': + - skip: + features: headers + + - do: + headers: { Authorization: "Basic ZW50c2VhcmNoLXVzZXI6ZW50c2VhcmNoLXVzZXItcGFzc3dvcmQ=" } # user + connector_secret.post: + body: + value: my-secret + - set: { id: id } + - match: { id: $id } + - do: + headers: { Authorization: "Basic ZW50c2VhcmNoLXVucHJpdmlsZWdlZDplbnRzZWFyY2gtdW5wcml2aWxlZ2VkLXVzZXI=" } # unprivileged + connector_secret.delete: + id: $id + catch: unauthorized + +--- +'Delete connector secret - Secret does not exist': + - do: + connector_secret.delete: + id: non-existing-secret-id + catch: missing diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/EnterpriseSearch.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/EnterpriseSearch.java index d344bd60a22bd..3933e7923d6b9 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/EnterpriseSearch.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/EnterpriseSearch.java @@ -90,10 +90,13 @@ import org.elasticsearch.xpack.application.connector.action.UpdateConnectorServiceTypeAction; import org.elasticsearch.xpack.application.connector.secrets.ConnectorSecretsFeature; import org.elasticsearch.xpack.application.connector.secrets.ConnectorSecretsIndexService; +import org.elasticsearch.xpack.application.connector.secrets.action.DeleteConnectorSecretAction; import org.elasticsearch.xpack.application.connector.secrets.action.GetConnectorSecretAction; import org.elasticsearch.xpack.application.connector.secrets.action.PostConnectorSecretAction; +import org.elasticsearch.xpack.application.connector.secrets.action.RestDeleteConnectorSecretAction; import org.elasticsearch.xpack.application.connector.secrets.action.RestGetConnectorSecretAction; import org.elasticsearch.xpack.application.connector.secrets.action.RestPostConnectorSecretAction; +import org.elasticsearch.xpack.application.connector.secrets.action.TransportDeleteConnectorSecretAction; import org.elasticsearch.xpack.application.connector.secrets.action.TransportGetConnectorSecretAction; import org.elasticsearch.xpack.application.connector.secrets.action.TransportPostConnectorSecretAction; import org.elasticsearch.xpack.application.connector.syncjob.action.CancelConnectorSyncJobAction; @@ -271,6 +274,7 @@ protected XPackLicenseState getLicenseState() { if (ConnectorSecretsFeature.isEnabled()) { actionHandlers.addAll( List.of( + new ActionHandler<>(DeleteConnectorSecretAction.INSTANCE, TransportDeleteConnectorSecretAction.class), new ActionHandler<>(GetConnectorSecretAction.INSTANCE, TransportGetConnectorSecretAction.class), new ActionHandler<>(PostConnectorSecretAction.INSTANCE, TransportPostConnectorSecretAction.class) ) @@ -355,7 +359,9 @@ public List getRestHandlers( } if (ConnectorSecretsFeature.isEnabled()) { - restHandlers.addAll(List.of(new RestGetConnectorSecretAction(), new RestPostConnectorSecretAction())); + restHandlers.addAll( + List.of(new RestGetConnectorSecretAction(), new RestPostConnectorSecretAction(), new RestDeleteConnectorSecretAction()) + ); } return Collections.unmodifiableList(restHandlers); diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/Connector.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/Connector.java index fdbf27929789f..b7ddf560247ed 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/Connector.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/Connector.java @@ -356,34 +356,38 @@ public static Connector fromXContent(XContentParser parser, String docId) throws return PARSER.parse(parser, docId); } + public void toInnerXContent(XContentBuilder builder, Params params) throws IOException { + // The "id": connectorId is included in GET and LIST responses to provide the connector's docID. + // Note: This ID is not written to the Elasticsearch index; it's only for API response purposes. + if (connectorId != null) { + builder.field(ID_FIELD.getPreferredName(), connectorId); + } + builder.field(API_KEY_ID_FIELD.getPreferredName(), apiKeyId); + builder.xContentValuesMap(CONFIGURATION_FIELD.getPreferredName(), configuration); + builder.xContentValuesMap(CUSTOM_SCHEDULING_FIELD.getPreferredName(), customScheduling); + builder.field(DESCRIPTION_FIELD.getPreferredName(), description); + builder.field(ERROR_FIELD.getPreferredName(), error); + builder.field(FEATURES_FIELD.getPreferredName(), features); + builder.xContentList(FILTERING_FIELD.getPreferredName(), filtering); + builder.field(INDEX_NAME_FIELD.getPreferredName(), indexName); + builder.field(IS_NATIVE_FIELD.getPreferredName(), isNative); + builder.field(LANGUAGE_FIELD.getPreferredName(), language); + builder.field(LAST_SEEN_FIELD.getPreferredName(), lastSeen); + syncInfo.toXContent(builder, params); + builder.field(NAME_FIELD.getPreferredName(), name); + builder.field(PIPELINE_FIELD.getPreferredName(), pipeline); + builder.field(SCHEDULING_FIELD.getPreferredName(), scheduling); + builder.field(SERVICE_TYPE_FIELD.getPreferredName(), serviceType); + builder.field(SYNC_CURSOR_FIELD.getPreferredName(), syncCursor); + builder.field(STATUS_FIELD.getPreferredName(), status.toString()); + builder.field(SYNC_NOW_FIELD.getPreferredName(), syncNow); + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); { - // The "id": connectorId is included in GET and LIST responses to provide the connector's docID. - // Note: This ID is not written to the Elasticsearch index; it's only for API response purposes. - if (connectorId != null) { - builder.field(ID_FIELD.getPreferredName(), connectorId); - } - builder.field(API_KEY_ID_FIELD.getPreferredName(), apiKeyId); - builder.xContentValuesMap(CONFIGURATION_FIELD.getPreferredName(), configuration); - builder.xContentValuesMap(CUSTOM_SCHEDULING_FIELD.getPreferredName(), customScheduling); - builder.field(DESCRIPTION_FIELD.getPreferredName(), description); - builder.field(ERROR_FIELD.getPreferredName(), error); - builder.field(FEATURES_FIELD.getPreferredName(), features); - builder.xContentList(FILTERING_FIELD.getPreferredName(), filtering); - builder.field(INDEX_NAME_FIELD.getPreferredName(), indexName); - builder.field(IS_NATIVE_FIELD.getPreferredName(), isNative); - builder.field(LANGUAGE_FIELD.getPreferredName(), language); - builder.field(LAST_SEEN_FIELD.getPreferredName(), lastSeen); - syncInfo.toXContent(builder, params); - builder.field(NAME_FIELD.getPreferredName(), name); - builder.field(PIPELINE_FIELD.getPreferredName(), pipeline); - builder.field(SCHEDULING_FIELD.getPreferredName(), scheduling); - builder.field(SERVICE_TYPE_FIELD.getPreferredName(), serviceType); - builder.field(SYNC_CURSOR_FIELD.getPreferredName(), syncCursor); - builder.field(STATUS_FIELD.getPreferredName(), status.toString()); - builder.field(SYNC_NOW_FIELD.getPreferredName(), syncNow); + toInnerXContent(builder, params); } builder.endObject(); return builder; diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorIndexService.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorIndexService.java index d92074dacc129..cf6c3190a37b4 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorIndexService.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorIndexService.java @@ -26,11 +26,12 @@ import org.elasticsearch.client.internal.OriginSettingClient; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.query.MatchAllQueryBuilder; +import org.elasticsearch.script.Script; +import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.application.connector.action.PostConnectorAction; import org.elasticsearch.xpack.application.connector.action.PutConnectorAction; import org.elasticsearch.xpack.application.connector.action.UpdateConnectorConfigurationAction; @@ -47,6 +48,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.function.BiConsumer; @@ -175,7 +177,7 @@ private Connector createConnectorWithDefaultValues( * @param connectorId The id of the connector object. * @param listener The action listener to invoke on response/failure. */ - public void getConnector(String connectorId, ActionListener listener) { + public void getConnector(String connectorId, ActionListener listener) { try { final GetRequest getRequest = new GetRequest(CONNECTOR_INDEX_NAME).id(connectorId).realtime(true); @@ -185,11 +187,11 @@ public void getConnector(String connectorId, ActionListener listener) return; } try { - final Connector connector = Connector.fromXContentBytes( - getResponse.getSourceAsBytesRef(), - connectorId, - XContentType.JSON - ); + final ConnectorSearchResult connector = new ConnectorSearchResult.Builder().setId(connectorId) + .setResultBytes(getResponse.getSourceAsBytesRef()) + .setResultMap(getResponse.getSourceAsMap()) + .build(); + l.onResponse(connector); } catch (Exception e) { listener.onFailure(e); @@ -269,6 +271,8 @@ public void onFailure(Exception e) { /** * Updates the {@link ConnectorConfiguration} property of a {@link Connector}. + * The update process is non-additive; it completely replaces all existing configuration fields with the new configuration mapping, + * thereby deleting any old configurations. * * @param request Request for updating connector configuration property. * @param listener Listener to respond to a successful response or an error. @@ -276,19 +280,32 @@ public void onFailure(Exception e) { public void updateConnectorConfiguration(UpdateConnectorConfigurationAction.Request request, ActionListener listener) { try { String connectorId = request.getConnectorId(); - final UpdateRequest updateRequest = new UpdateRequest(CONNECTOR_INDEX_NAME, connectorId).doc( - new IndexRequest(CONNECTOR_INDEX_NAME).opType(DocWriteRequest.OpType.INDEX) - .id(connectorId) - .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) - .source( - Map.of( - Connector.CONFIGURATION_FIELD.getPreferredName(), - request.getConfiguration(), - Connector.STATUS_FIELD.getPreferredName(), - ConnectorStatus.CONFIGURED.toString() - ) - ) + + String updateConfigurationScript = String.format( + Locale.ROOT, + """ + ctx._source.%s = params.%s; + ctx._source.%s = params.%s; + """, + Connector.CONFIGURATION_FIELD.getPreferredName(), + Connector.CONFIGURATION_FIELD.getPreferredName(), + Connector.STATUS_FIELD.getPreferredName(), + Connector.STATUS_FIELD.getPreferredName() + ); + Script script = new Script( + ScriptType.INLINE, + "painless", + updateConfigurationScript, + Map.of( + Connector.CONFIGURATION_FIELD.getPreferredName(), + request.getConfiguration(), + Connector.STATUS_FIELD.getPreferredName(), + ConnectorStatus.CONFIGURED.toString() + ) ); + final UpdateRequest updateRequest = new UpdateRequest(CONNECTOR_INDEX_NAME, connectorId).script(script) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + clientWithOrigin.update( updateRequest, new DelegatingIndexNotFoundActionListener<>(connectorId, listener, (l, updateResponse) -> { @@ -567,7 +584,9 @@ public void updateConnectorServiceType(UpdateConnectorServiceTypeAction.Request String connectorId = request.getConnectorId(); getConnector(connectorId, listener.delegateFailure((l, connector) -> { - ConnectorStatus prevStatus = connector.getStatus(); + ConnectorStatus prevStatus = ConnectorStatus.connectorStatus( + (String) connector.getResultMap().get(Connector.STATUS_FIELD.getPreferredName()) + ); ConnectorStatus newStatus = prevStatus == ConnectorStatus.CREATED ? ConnectorStatus.CREATED : ConnectorStatus.NEEDS_CONFIGURATION; @@ -603,20 +622,23 @@ public void updateConnectorServiceType(UpdateConnectorServiceTypeAction.Request } private static ConnectorIndexService.ConnectorResult mapSearchResponseToConnectorList(SearchResponse response) { - final List connectorResults = Arrays.stream(response.getHits().getHits()) + final List connectorResults = Arrays.stream(response.getHits().getHits()) .map(ConnectorIndexService::hitToConnector) .toList(); return new ConnectorIndexService.ConnectorResult(connectorResults, (int) response.getHits().getTotalHits().value); } - private static Connector hitToConnector(SearchHit searchHit) { + private static ConnectorSearchResult hitToConnector(SearchHit searchHit) { // todo: don't return sensitive data from configuration in list endpoint - return Connector.fromXContentBytes(searchHit.getSourceRef(), searchHit.getId(), XContentType.JSON); + return new ConnectorSearchResult.Builder().setId(searchHit.getId()) + .setResultBytes(searchHit.getSourceRef()) + .setResultMap(searchHit.getSourceAsMap()) + .build(); } - public record ConnectorResult(List connectors, long totalResults) {} + public record ConnectorResult(List connectors, long totalResults) {} /** * Listeners that checks failures for IndexNotFoundException, and transforms them in ResourceNotFoundException, diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorSearchResult.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorSearchResult.java new file mode 100644 index 0000000000000..d054542e0865a --- /dev/null +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorSearchResult.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.application.connector; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.StreamInput; + +import java.io.IOException; +import java.util.Map; + +public class ConnectorSearchResult extends ConnectorsAPISearchResult { + + public ConnectorSearchResult(StreamInput in) throws IOException { + super(in); + } + + private ConnectorSearchResult(BytesReference resultBytes, Map resultMap, String id) { + super(resultBytes, resultMap, id); + } + + public static class Builder { + + private BytesReference resultBytes; + private Map resultMap; + private String id; + + public Builder setResultBytes(BytesReference resultBytes) { + this.resultBytes = resultBytes; + return this; + } + + public Builder setResultMap(Map resultMap) { + this.resultMap = resultMap; + return this; + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public ConnectorSearchResult build() { + return new ConnectorSearchResult(resultBytes, resultMap, id); + } + } +} diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorsAPISearchResult.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorsAPISearchResult.java new file mode 100644 index 0000000000000..a00e3748565d8 --- /dev/null +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorsAPISearchResult.java @@ -0,0 +1,89 @@ +/* + * 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.application.connector; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xpack.application.connector.syncjob.ConnectorSyncJob; + +import java.io.IOException; +import java.util.Map; +import java.util.Objects; + +/** + * Represents the outcome of a search query in the connectors and sync job index, encapsulating the search result. + * It includes a raw byte reference to the result which can be deserialized into a {@link Connector} or {@link ConnectorSyncJob} object, + * and a result map for returning the data without strict deserialization. + */ +public class ConnectorsAPISearchResult implements Writeable, ToXContentObject { + + private final BytesReference resultBytes; + private final Map resultMap; + private final String docId; + + protected ConnectorsAPISearchResult(BytesReference resultBytes, Map resultMap, String id) { + this.resultBytes = resultBytes; + this.resultMap = resultMap; + this.docId = id; + } + + public ConnectorsAPISearchResult(StreamInput in) throws IOException { + this.resultBytes = in.readBytesReference(); + this.resultMap = in.readGenericMap(); + this.docId = in.readString(); + } + + public BytesReference getSourceRef() { + return resultBytes; + } + + public Map getResultMap() { + return resultMap; + } + + public String getDocId() { + return docId; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + { + builder.field("id", docId); + builder.mapContents(resultMap); + } + builder.endObject(); + return builder; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeBytesReference(resultBytes); + out.writeGenericMap(resultMap); + out.writeString(docId); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConnectorsAPISearchResult that = (ConnectorsAPISearchResult) o; + return Objects.equals(resultBytes, that.resultBytes) + && Objects.equals(resultMap, that.resultMap) + && Objects.equals(docId, that.docId); + } + + @Override + public int hashCode() { + return Objects.hash(resultBytes, resultMap, docId); + } +} diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/GetConnectorAction.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/GetConnectorAction.java index 88eacc8f437b4..a9792458f1963 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/GetConnectorAction.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/GetConnectorAction.java @@ -19,7 +19,7 @@ import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xpack.application.connector.Connector; +import org.elasticsearch.xpack.application.connector.ConnectorSearchResult; import java.io.IOException; import java.util.Objects; @@ -110,15 +110,15 @@ public static Request parse(XContentParser parser) { public static class Response extends ActionResponse implements ToXContentObject { - private final Connector connector; + private final ConnectorSearchResult connector; - public Response(Connector connector) { + public Response(ConnectorSearchResult connector) { this.connector = connector; } public Response(StreamInput in) throws IOException { super(in); - this.connector = new Connector(in); + this.connector = new ConnectorSearchResult(in); } @Override @@ -131,10 +131,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return connector.toXContent(builder, params); } - public static GetConnectorAction.Response fromXContent(XContentParser parser, String docId) throws IOException { - return new GetConnectorAction.Response(Connector.fromXContent(parser, docId)); - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/ListConnectorAction.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/ListConnectorAction.java index 3b286569ce881..b4a3a2c0d3632 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/ListConnectorAction.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/action/ListConnectorAction.java @@ -18,7 +18,7 @@ import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xpack.application.connector.Connector; +import org.elasticsearch.xpack.application.connector.ConnectorSearchResult; import org.elasticsearch.xpack.core.action.util.PageParams; import org.elasticsearch.xpack.core.action.util.QueryPage; @@ -105,14 +105,14 @@ public static class Response extends ActionResponse implements ToXContentObject public static final ParseField RESULT_FIELD = new ParseField("results"); - final QueryPage queryPage; + final QueryPage queryPage; public Response(StreamInput in) throws IOException { super(in); - this.queryPage = new QueryPage<>(in, Connector::new); + this.queryPage = new QueryPage<>(in, ConnectorSearchResult::new); } - public Response(List items, Long totalResults) { + public Response(List items, Long totalResults) { this.queryPage = new QueryPage<>(items, totalResults, RESULT_FIELD); } @@ -126,7 +126,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return queryPage.toXContent(builder, params); } - public QueryPage queryPage() { + public QueryPage queryPage() { return queryPage; } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsIndexService.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsIndexService.java index 633909ac2aa89..c994fc1155277 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsIndexService.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsIndexService.java @@ -10,11 +10,13 @@ import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.DocWriteResponse; import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateRequest; import org.elasticsearch.client.internal.Client; import org.elasticsearch.client.internal.OriginSettingClient; import org.elasticsearch.indices.SystemIndexDescriptor; import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xpack.application.connector.secrets.action.DeleteConnectorSecretResponse; import org.elasticsearch.xpack.application.connector.secrets.action.GetConnectorSecretResponse; import org.elasticsearch.xpack.application.connector.secrets.action.PostConnectorSecretRequest; import org.elasticsearch.xpack.application.connector.secrets.action.PostConnectorSecretResponse; @@ -93,4 +95,19 @@ public void createSecret(PostConnectorSecretRequest request, ActionListener listener) { + try { + clientWithOrigin.prepareDelete(CONNECTOR_SECRETS_INDEX_NAME, id) + .execute(listener.delegateFailureAndWrap((delegate, deleteResponse) -> { + if (deleteResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) { + delegate.onFailure(new ResourceNotFoundException("No secret with id [" + id + "]")); + return; + } + delegate.onResponse(new DeleteConnectorSecretResponse(deleteResponse.getResult() == DocWriteResponse.Result.DELETED)); + })); + } catch (Exception e) { + listener.onFailure(e); + } + } } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretAction.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretAction.java new file mode 100644 index 0000000000000..b97911a350972 --- /dev/null +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretAction.java @@ -0,0 +1,19 @@ +/* + * 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.application.connector.secrets.action; + +import org.elasticsearch.action.ActionType; + +public class DeleteConnectorSecretAction { + + public static final String NAME = "cluster:admin/xpack/connector/secret/delete"; + + public static final ActionType INSTANCE = new ActionType<>(NAME); + + private DeleteConnectorSecretAction() {/* no instances */} +} diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretRequest.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretRequest.java new file mode 100644 index 0000000000000..183362f64ea8f --- /dev/null +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretRequest.java @@ -0,0 +1,67 @@ +/* + * 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.application.connector.secrets.action; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Objects; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +public class DeleteConnectorSecretRequest extends ActionRequest { + + private final String id; + + public DeleteConnectorSecretRequest(String id) { + this.id = Objects.requireNonNull(id); + } + + public DeleteConnectorSecretRequest(StreamInput in) throws IOException { + super(in); + this.id = in.readString(); + } + + public String id() { + return id; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(id); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + + if (Strings.isNullOrEmpty(id)) { + validationException = addValidationError("id missing", validationException); + } + + return validationException; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeleteConnectorSecretRequest that = (DeleteConnectorSecretRequest) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretResponse.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretResponse.java new file mode 100644 index 0000000000000..7568d3f193779 --- /dev/null +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretResponse.java @@ -0,0 +1,60 @@ +/* + * 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.application.connector.secrets.action; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +public class DeleteConnectorSecretResponse extends ActionResponse implements ToXContentObject { + + private final boolean deleted; + + public DeleteConnectorSecretResponse(boolean deleted) { + this.deleted = deleted; + } + + public DeleteConnectorSecretResponse(StreamInput in) throws IOException { + super(in); + this.deleted = in.readBoolean(); + } + + public boolean isDeleted() { + return deleted; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeBoolean(deleted); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("deleted", deleted); + return builder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeleteConnectorSecretResponse that = (DeleteConnectorSecretResponse) o; + return deleted == that.deleted; + } + + @Override + public int hashCode() { + return Objects.hash(deleted); + } +} diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/RestDeleteConnectorSecretAction.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/RestDeleteConnectorSecretAction.java new file mode 100644 index 0000000000000..cd1c9b5f19498 --- /dev/null +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/RestDeleteConnectorSecretAction.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.xpack.application.connector.secrets.action; + +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.Scope; +import org.elasticsearch.rest.ServerlessScope; +import org.elasticsearch.rest.action.RestToXContentListener; + +import java.io.IOException; +import java.util.List; + +@ServerlessScope(Scope.INTERNAL) +public class RestDeleteConnectorSecretAction extends BaseRestHandler { + + @Override + public String getName() { + return "connector_delete_secret"; + } + + @Override + public List routes() { + return List.of(new Route(RestRequest.Method.DELETE, "/_connector/_secret/{id}")); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + final String id = request.param("id"); + return restChannel -> client.execute( + DeleteConnectorSecretAction.INSTANCE, + new DeleteConnectorSecretRequest(id), + new RestToXContentListener<>(restChannel) + ); + } +} diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/TransportDeleteConnectorSecretAction.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/TransportDeleteConnectorSecretAction.java new file mode 100644 index 0000000000000..7c87598440cfd --- /dev/null +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/TransportDeleteConnectorSecretAction.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.xpack.application.connector.secrets.action; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.application.connector.secrets.ConnectorSecretsIndexService; + +public class TransportDeleteConnectorSecretAction extends HandledTransportAction< + DeleteConnectorSecretRequest, + DeleteConnectorSecretResponse> { + + private final ConnectorSecretsIndexService connectorSecretsIndexService; + + @Inject + public TransportDeleteConnectorSecretAction(TransportService transportService, ActionFilters actionFilters, Client client) { + super( + DeleteConnectorSecretAction.NAME, + transportService, + actionFilters, + DeleteConnectorSecretRequest::new, + EsExecutors.DIRECT_EXECUTOR_SERVICE + ); + this.connectorSecretsIndexService = new ConnectorSecretsIndexService(client); + } + + protected void doExecute(Task task, DeleteConnectorSecretRequest request, ActionListener listener) { + connectorSecretsIndexService.deleteSecret(request.id(), listener); + } +} diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJob.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJob.java index 48f3f2a117d63..fb34035e5400b 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJob.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJob.java @@ -200,7 +200,7 @@ private ConnectorSyncJob( this.createdAt = createdAt; this.deletedDocumentCount = deletedDocumentCount; this.error = error; - this.id = Objects.requireNonNull(id, "[id] cannot be null"); + this.id = id; this.indexedDocumentCount = indexedDocumentCount; this.indexedDocumentVolume = indexedDocumentVolume; this.jobType = Objects.requireNonNullElse(jobType, ConnectorSyncJobType.FULL); @@ -235,10 +235,10 @@ public ConnectorSyncJob(StreamInput in) throws IOException { } @SuppressWarnings("unchecked") - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "connector_sync_job", true, - (args) -> { + (args, docId) -> { int i = 0; return new Builder().setCancellationRequestedAt((Instant) args[i++]) .setCanceledAt((Instant) args[i++]) @@ -247,7 +247,7 @@ public ConnectorSyncJob(StreamInput in) throws IOException { .setCreatedAt((Instant) args[i++]) .setDeletedDocumentCount((Long) args[i++]) .setError((String) args[i++]) - .setId((String) args[i++]) + .setId(docId) .setIndexedDocumentCount((Long) args[i++]) .setIndexedDocumentVolume((Long) args[i++]) .setJobType((ConnectorSyncJobType) args[i++]) @@ -295,7 +295,6 @@ public ConnectorSyncJob(StreamInput in) throws IOException { ); PARSER.declareLong(constructorArg(), DELETED_DOCUMENT_COUNT_FIELD); PARSER.declareStringOrNull(optionalConstructorArg(), ERROR_FIELD); - PARSER.declareString(constructorArg(), ID_FIELD); PARSER.declareLong(constructorArg(), INDEXED_DOCUMENT_COUNT_FIELD); PARSER.declareLong(constructorArg(), INDEXED_DOCUMENT_VOLUME_FIELD); PARSER.declareField( @@ -383,16 +382,16 @@ public ConnectorSyncJob(StreamInput in) throws IOException { ); } - public static ConnectorSyncJob fromXContentBytes(BytesReference source, XContentType xContentType) { + public static ConnectorSyncJob fromXContentBytes(BytesReference source, String docId, XContentType xContentType) { try (XContentParser parser = XContentHelper.createParser(XContentParserConfiguration.EMPTY, source, xContentType)) { - return ConnectorSyncJob.fromXContent(parser); + return ConnectorSyncJob.fromXContent(parser, docId); } catch (IOException e) { throw new ElasticsearchParseException("Failed to parse a connector sync job document.", e); } } - public static ConnectorSyncJob fromXContent(XContentParser parser) throws IOException { - return PARSER.parse(parser, null); + public static ConnectorSyncJob fromXContent(XContentParser parser, String docId) throws IOException { + return PARSER.parse(parser, docId); } public static Connector syncJobConnectorFromXContentBytes(BytesReference source, String connectorId, XContentType xContentType) { @@ -479,70 +478,73 @@ public String getWorkerHostname() { return workerHostname; } - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); + public void toInnerXContent(XContentBuilder builder, Params params) throws IOException { + if (cancelationRequestedAt != null) { + builder.field(CANCELATION_REQUESTED_AT_FIELD.getPreferredName(), cancelationRequestedAt); + } + if (canceledAt != null) { + builder.field(CANCELED_AT_FIELD.getPreferredName(), canceledAt); + } + if (completedAt != null) { + builder.field(COMPLETED_AT_FIELD.getPreferredName(), completedAt); + } + + builder.startObject(CONNECTOR_FIELD.getPreferredName()); { - if (cancelationRequestedAt != null) { - builder.field(CANCELATION_REQUESTED_AT_FIELD.getPreferredName(), cancelationRequestedAt); - } - if (canceledAt != null) { - builder.field(CANCELED_AT_FIELD.getPreferredName(), canceledAt); + if (connector.getConnectorId() != null) { + builder.field(Connector.ID_FIELD.getPreferredName(), connector.getConnectorId()); } - if (completedAt != null) { - builder.field(COMPLETED_AT_FIELD.getPreferredName(), completedAt); + if (connector.getSyncJobFiltering() != null) { + builder.field(Connector.FILTERING_FIELD.getPreferredName(), connector.getSyncJobFiltering()); } - - builder.startObject(CONNECTOR_FIELD.getPreferredName()); - { - if (connector.getConnectorId() != null) { - builder.field(Connector.ID_FIELD.getPreferredName(), connector.getConnectorId()); - } - if (connector.getSyncJobFiltering() != null) { - builder.field(Connector.FILTERING_FIELD.getPreferredName(), connector.getSyncJobFiltering()); - } - if (connector.getIndexName() != null) { - builder.field(Connector.INDEX_NAME_FIELD.getPreferredName(), connector.getIndexName()); - } - if (connector.getLanguage() != null) { - builder.field(Connector.LANGUAGE_FIELD.getPreferredName(), connector.getLanguage()); - } - if (connector.getPipeline() != null) { - builder.field(Connector.PIPELINE_FIELD.getPreferredName(), connector.getPipeline()); - } - if (connector.getServiceType() != null) { - builder.field(Connector.SERVICE_TYPE_FIELD.getPreferredName(), connector.getServiceType()); - } - if (connector.getConfiguration() != null) { - builder.field(Connector.CONFIGURATION_FIELD.getPreferredName(), connector.getConfiguration()); - } + if (connector.getIndexName() != null) { + builder.field(Connector.INDEX_NAME_FIELD.getPreferredName(), connector.getIndexName()); } - builder.endObject(); - - builder.field(CREATED_AT_FIELD.getPreferredName(), createdAt); - builder.field(DELETED_DOCUMENT_COUNT_FIELD.getPreferredName(), deletedDocumentCount); - if (error != null) { - builder.field(ERROR_FIELD.getPreferredName(), error); + if (connector.getLanguage() != null) { + builder.field(Connector.LANGUAGE_FIELD.getPreferredName(), connector.getLanguage()); } - builder.field(ID_FIELD.getPreferredName(), id); - builder.field(INDEXED_DOCUMENT_COUNT_FIELD.getPreferredName(), indexedDocumentCount); - builder.field(INDEXED_DOCUMENT_VOLUME_FIELD.getPreferredName(), indexedDocumentVolume); - builder.field(JOB_TYPE_FIELD.getPreferredName(), jobType); - if (lastSeen != null) { - builder.field(LAST_SEEN_FIELD.getPreferredName(), lastSeen); + if (connector.getPipeline() != null) { + builder.field(Connector.PIPELINE_FIELD.getPreferredName(), connector.getPipeline()); } - builder.field(METADATA_FIELD.getPreferredName(), metadata); - if (startedAt != null) { - builder.field(STARTED_AT_FIELD.getPreferredName(), startedAt); + if (connector.getServiceType() != null) { + builder.field(Connector.SERVICE_TYPE_FIELD.getPreferredName(), connector.getServiceType()); } - builder.field(STATUS_FIELD.getPreferredName(), status); - builder.field(TOTAL_DOCUMENT_COUNT_FIELD.getPreferredName(), totalDocumentCount); - builder.field(TRIGGER_METHOD_FIELD.getPreferredName(), triggerMethod); - if (workerHostname != null) { - builder.field(WORKER_HOSTNAME_FIELD.getPreferredName(), workerHostname); + if (connector.getConfiguration() != null) { + builder.field(Connector.CONFIGURATION_FIELD.getPreferredName(), connector.getConfiguration()); } } builder.endObject(); + + builder.field(CREATED_AT_FIELD.getPreferredName(), createdAt); + builder.field(DELETED_DOCUMENT_COUNT_FIELD.getPreferredName(), deletedDocumentCount); + if (error != null) { + builder.field(ERROR_FIELD.getPreferredName(), error); + } + builder.field(INDEXED_DOCUMENT_COUNT_FIELD.getPreferredName(), indexedDocumentCount); + builder.field(INDEXED_DOCUMENT_VOLUME_FIELD.getPreferredName(), indexedDocumentVolume); + builder.field(JOB_TYPE_FIELD.getPreferredName(), jobType); + if (lastSeen != null) { + builder.field(LAST_SEEN_FIELD.getPreferredName(), lastSeen); + } + builder.field(METADATA_FIELD.getPreferredName(), metadata); + if (startedAt != null) { + builder.field(STARTED_AT_FIELD.getPreferredName(), startedAt); + } + builder.field(STATUS_FIELD.getPreferredName(), status); + builder.field(TOTAL_DOCUMENT_COUNT_FIELD.getPreferredName(), totalDocumentCount); + builder.field(TRIGGER_METHOD_FIELD.getPreferredName(), triggerMethod); + if (workerHostname != null) { + builder.field(WORKER_HOSTNAME_FIELD.getPreferredName(), workerHostname); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + { + toInnerXContent(builder, params); + } + builder.endObject(); return builder; } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexService.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexService.java index 01a297a11103b..b6d20b9f0e777 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexService.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexService.java @@ -97,14 +97,11 @@ public void createConnectorSyncJob( ); try { - String syncJobId = generateId(); - final IndexRequest indexRequest = new IndexRequest(CONNECTOR_SYNC_JOB_INDEX_NAME).id(syncJobId) - .opType(DocWriteRequest.OpType.INDEX) + final IndexRequest indexRequest = new IndexRequest(CONNECTOR_SYNC_JOB_INDEX_NAME).opType(DocWriteRequest.OpType.INDEX) .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); - ConnectorSyncJob syncJob = new ConnectorSyncJob.Builder().setId(syncJobId) - .setJobType(jobType) + ConnectorSyncJob syncJob = new ConnectorSyncJob.Builder().setJobType(jobType) .setTriggerMethod(triggerMethod) .setStatus(ConnectorSyncJob.DEFAULT_INITIAL_STATUS) .setConnector(connector) @@ -195,7 +192,7 @@ public void checkInConnectorSyncJob(String connectorSyncJobId, ActionListener listener) { + public void getConnectorSyncJob(String connectorSyncJobId, ActionListener listener) { final GetRequest getRequest = new GetRequest(CONNECTOR_SYNC_JOB_INDEX_NAME).id(connectorSyncJobId).realtime(true); try { @@ -208,11 +205,10 @@ public void getConnectorSyncJob(String connectorSyncJobId, ActionListener connectorSyncJobs = Arrays.stream(searchResponse.getHits().getHits()) + final List connectorSyncJobs = Arrays.stream(searchResponse.getHits().getHits()) .map(ConnectorSyncJobIndexService::hitToConnectorSyncJob) .toList(); @@ -346,13 +342,17 @@ private ConnectorSyncJobsResult mapSearchResponseToConnectorSyncJobsList(SearchR ); } - private static ConnectorSyncJob hitToConnectorSyncJob(SearchHit searchHit) { + private static ConnectorSyncJobSearchResult hitToConnectorSyncJob(SearchHit searchHit) { // TODO: don't return sensitive data from configuration inside connector in list endpoint - return ConnectorSyncJob.fromXContentBytes(searchHit.getSourceRef(), XContentType.JSON); + return new ConnectorSyncJobSearchResult.Builder().setId(searchHit.getId()) + .setResultBytes(searchHit.getSourceRef()) + .setResultMap(searchHit.getSourceAsMap()) + .build(); + } - public record ConnectorSyncJobsResult(List connectorSyncJobs, long totalResults) {} + public record ConnectorSyncJobsResult(List connectorSyncJobs, long totalResults) {} /** * Updates the ingestion stats of the {@link ConnectorSyncJob} in the underlying index. diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobSearchResult.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobSearchResult.java new file mode 100644 index 0000000000000..7ab2719dcbea2 --- /dev/null +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobSearchResult.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.application.connector.syncjob; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.xpack.application.connector.ConnectorsAPISearchResult; + +import java.io.IOException; +import java.util.Map; + +public class ConnectorSyncJobSearchResult extends ConnectorsAPISearchResult { + + public ConnectorSyncJobSearchResult(StreamInput in) throws IOException { + super(in); + } + + private ConnectorSyncJobSearchResult(BytesReference resultBytes, Map resultMap, String id) { + super(resultBytes, resultMap, id); + } + + public static class Builder { + + private BytesReference resultBytes; + private Map resultMap; + private String id; + + public Builder setResultBytes(BytesReference resultBytes) { + this.resultBytes = resultBytes; + return this; + } + + public Builder setResultMap(Map resultMap) { + this.resultMap = resultMap; + return this; + } + + public Builder setId(String id) { + this.id = id; + return this; + } + + public ConnectorSyncJobSearchResult build() { + return new ConnectorSyncJobSearchResult(resultBytes, resultMap, id); + } + } +} diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/action/GetConnectorSyncJobAction.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/action/GetConnectorSyncJobAction.java index 9e21ba7e94f1f..31441883f061c 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/action/GetConnectorSyncJobAction.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/action/GetConnectorSyncJobAction.java @@ -19,8 +19,8 @@ import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xpack.application.connector.syncjob.ConnectorSyncJob; import org.elasticsearch.xpack.application.connector.syncjob.ConnectorSyncJobConstants; +import org.elasticsearch.xpack.application.connector.syncjob.ConnectorSyncJobSearchResult; import java.io.IOException; import java.util.Objects; @@ -110,15 +110,15 @@ public static Request parse(XContentParser parser) { } public static class Response extends ActionResponse implements ToXContentObject { - private final ConnectorSyncJob connectorSyncJob; + private final ConnectorSyncJobSearchResult connectorSyncJob; - public Response(ConnectorSyncJob connectorSyncJob) { + public Response(ConnectorSyncJobSearchResult connectorSyncJob) { this.connectorSyncJob = connectorSyncJob; } public Response(StreamInput in) throws IOException { super(in); - this.connectorSyncJob = new ConnectorSyncJob(in); + this.connectorSyncJob = new ConnectorSyncJobSearchResult(in); } @Override diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/action/ListConnectorSyncJobsAction.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/action/ListConnectorSyncJobsAction.java index 298eee466bfb2..c81df8b642b37 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/action/ListConnectorSyncJobsAction.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/action/ListConnectorSyncJobsAction.java @@ -20,6 +20,7 @@ import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.application.connector.ConnectorSyncStatus; import org.elasticsearch.xpack.application.connector.syncjob.ConnectorSyncJob; +import org.elasticsearch.xpack.application.connector.syncjob.ConnectorSyncJobSearchResult; import org.elasticsearch.xpack.core.action.util.PageParams; import org.elasticsearch.xpack.core.action.util.QueryPage; @@ -133,14 +134,14 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws public static class Response extends ActionResponse implements ToXContentObject { public static final ParseField RESULTS_FIELD = new ParseField("results"); - final QueryPage queryPage; + final QueryPage queryPage; public Response(StreamInput in) throws IOException { super(in); - this.queryPage = new QueryPage<>(in, ConnectorSyncJob::new); + this.queryPage = new QueryPage<>(in, ConnectorSyncJobSearchResult::new); } - public Response(List items, Long totalResults) { + public Response(List items, Long totalResults) { this.queryPage = new QueryPage<>(items, totalResults, RESULTS_FIELD); } diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorIndexServiceTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorIndexServiceTests.java index 542ea948c12df..c043bfd4453d8 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorIndexServiceTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorIndexServiceTests.java @@ -12,8 +12,16 @@ import org.elasticsearch.action.DocWriteResponse; import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.update.UpdateResponse; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.plugins.Plugin; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.script.MockScriptEngine; +import org.elasticsearch.script.MockScriptPlugin; +import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptEngine; +import org.elasticsearch.script.UpdateScript; import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.application.connector.action.PostConnectorAction; import org.elasticsearch.xpack.application.connector.action.PutConnectorAction; import org.elasticsearch.xpack.application.connector.action.UpdateConnectorConfigurationAction; @@ -29,11 +37,14 @@ import org.junit.Before; import java.util.ArrayList; +import java.util.Collection; +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.AtomicReference; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -51,6 +62,13 @@ public void setup() { this.connectorIndexService = new ConnectorIndexService(client()); } + @Override + protected Collection> getPlugins() { + List> plugins = new ArrayList<>(super.getPlugins()); + plugins.add(MockPainlessScriptEngine.TestPlugin.class); + return plugins; + } + public void testPutConnector() throws Exception { Connector connector = ConnectorTestUtils.getRandomConnector(); String connectorId = randomUUID(); @@ -92,21 +110,16 @@ public void testUpdateConnectorConfiguration() throws Exception { DocWriteResponse resp = buildRequestAndAwaitPutConnector(connectorId, connector); assertThat(resp.status(), anyOf(equalTo(RestStatus.CREATED), equalTo(RestStatus.OK))); - Map connectorConfiguration = connector.getConfiguration() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, entry -> ConnectorTestUtils.getRandomConnectorConfigurationField())); - UpdateConnectorConfigurationAction.Request updateConfigurationRequest = new UpdateConnectorConfigurationAction.Request( connectorId, - connectorConfiguration + connector.getConfiguration() ); DocWriteResponse updateResponse = awaitUpdateConnectorConfiguration(updateConfigurationRequest); assertThat(updateResponse.status(), equalTo(RestStatus.OK)); - Connector indexedConnector = awaitGetConnector(connectorId); - assertThat(connectorConfiguration, equalTo(indexedConnector.getConfiguration())); - assertThat(indexedConnector.getStatus(), equalTo(ConnectorStatus.CONFIGURED)); + + // Configuration update is handled via painless script. ScriptEngine is mocked for unit tests. + // More comprehensive tests are defined in yamlRestTest. } public void testUpdateConnectorPipeline() throws Exception { @@ -401,7 +414,13 @@ private Connector awaitGetConnector(String connectorId) throws Exception { final AtomicReference exc = new AtomicReference<>(null); connectorIndexService.getConnector(connectorId, new ActionListener<>() { @Override - public void onResponse(Connector connector) { + public void onResponse(ConnectorSearchResult connectorResult) { + // Serialize the sourceRef to Connector class for unit tests + Connector connector = Connector.fromXContentBytes( + connectorResult.getSourceRef(), + connectorResult.getDocId(), + XContentType.JSON + ); resp.set(connector); latch.countDown(); } @@ -700,4 +719,44 @@ public void onFailure(Exception e) { return resp.get(); } + /** + * Update configuration action is handled via painless script. This implementation mocks the painless script engine + * for unit tests. + */ + private static class MockPainlessScriptEngine extends MockScriptEngine { + + public static final String NAME = "painless"; + + public static class TestPlugin extends MockScriptPlugin { + @Override + public ScriptEngine getScriptEngine(Settings settings, Collection> contexts) { + return new ConnectorIndexServiceTests.MockPainlessScriptEngine(); + } + + @Override + protected Map, Object>> pluginScripts() { + return Collections.emptyMap(); + } + } + + @Override + public String getType() { + return NAME; + } + + @Override + public T compile(String name, String script, ScriptContext context, Map options) { + if (context.instanceClazz.equals(UpdateScript.class)) { + UpdateScript.Factory factory = (params, ctx) -> new UpdateScript(params, ctx) { + @Override + public void execute() { + + } + }; + return context.factoryClazz.cast(factory); + } + throw new IllegalArgumentException("mock painless does not know how to handle context [" + context.name + "]"); + } + } + } diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTestUtils.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTestUtils.java index 74b84e914a942..ecfcfcf9e4af4 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTestUtils.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTestUtils.java @@ -7,6 +7,9 @@ package org.elasticsearch.xpack.application.connector; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.application.connector.action.PostConnectorAction; import org.elasticsearch.xpack.application.connector.action.PutConnectorAction; import org.elasticsearch.xpack.application.connector.configuration.ConfigurationDependency; @@ -24,6 +27,7 @@ import org.elasticsearch.xpack.application.connector.filtering.FilteringValidationState; import org.elasticsearch.xpack.core.scheduler.Cron; +import java.io.IOException; import java.time.Instant; import java.util.Collections; import java.util.HashMap; @@ -262,6 +266,30 @@ public static Connector getRandomConnector() { .build(); } + private static BytesReference convertConnectorToBytesReference(Connector connector) { + try { + return XContentHelper.toXContent((builder, params) -> { + connector.toInnerXContent(builder, params); + return builder; + }, XContentType.JSON, null, false); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Map convertConnectorToGenericMap(Connector connector) { + return XContentHelper.convertToMap(convertConnectorToBytesReference(connector), true, XContentType.JSON).v2(); + } + + public static ConnectorSearchResult getRandomConnectorSearchResult() { + Connector connector = getRandomConnector(); + + return new ConnectorSearchResult.Builder().setResultBytes(convertConnectorToBytesReference(connector)) + .setResultMap(convertConnectorToGenericMap(connector)) + .setId(randomAlphaOfLength(10)) + .build(); + } + private static ConnectorFeatures.FeatureEnabled randomConnectorFeatureEnabled() { return new ConnectorFeatures.FeatureEnabled(randomBoolean()); } diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/GetConnectorActionResponseBWCSerializingTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/GetConnectorActionResponseBWCSerializingTests.java index 168e9ec8f433e..cc47e9d35afb0 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/GetConnectorActionResponseBWCSerializingTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/GetConnectorActionResponseBWCSerializingTests.java @@ -9,16 +9,12 @@ import org.elasticsearch.TransportVersion; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xpack.application.connector.Connector; import org.elasticsearch.xpack.application.connector.ConnectorTestUtils; -import org.elasticsearch.xpack.core.ml.AbstractBWCSerializationTestCase; +import org.elasticsearch.xpack.core.ml.AbstractBWCWireSerializationTestCase; import java.io.IOException; -public class GetConnectorActionResponseBWCSerializingTests extends AbstractBWCSerializationTestCase { - - private Connector connector; +public class GetConnectorActionResponseBWCSerializingTests extends AbstractBWCWireSerializationTestCase { @Override protected Writeable.Reader instanceReader() { @@ -27,8 +23,7 @@ protected Writeable.Reader instanceReader() { @Override protected GetConnectorAction.Response createTestInstance() { - this.connector = ConnectorTestUtils.getRandomConnector(); - return new GetConnectorAction.Response(this.connector); + return new GetConnectorAction.Response(ConnectorTestUtils.getRandomConnectorSearchResult()); } @Override @@ -36,11 +31,6 @@ protected GetConnectorAction.Response mutateInstance(GetConnectorAction.Response return randomValueOtherThan(instance, this::createTestInstance); } - @Override - protected GetConnectorAction.Response doParseInstance(XContentParser parser) throws IOException { - return GetConnectorAction.Response.fromXContent(parser, connector.getConnectorId()); - } - @Override protected GetConnectorAction.Response mutateInstanceForVersion(GetConnectorAction.Response instance, TransportVersion version) { return instance; diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/ListConnectorActionResponseBWCSerializingTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/ListConnectorActionResponseBWCSerializingTests.java index 1e4ee0d086462..ac8c85def542e 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/ListConnectorActionResponseBWCSerializingTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/action/ListConnectorActionResponseBWCSerializingTests.java @@ -22,7 +22,10 @@ protected Writeable.Reader instanceReader() { @Override protected ListConnectorAction.Response createTestInstance() { - return new ListConnectorAction.Response(randomList(10, ConnectorTestUtils::getRandomConnector), randomLongBetween(0, 100)); + return new ListConnectorAction.Response( + randomList(10, ConnectorTestUtils::getRandomConnectorSearchResult), + randomLongBetween(0, 100) + ); } @Override diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsIndexServiceTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsIndexServiceTests.java index f9a548a47feb3..b93c83c6494f3 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsIndexServiceTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsIndexServiceTests.java @@ -7,8 +7,10 @@ package org.elasticsearch.xpack.application.connector.secrets; +import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.xpack.application.connector.secrets.action.DeleteConnectorSecretResponse; import org.elasticsearch.xpack.application.connector.secrets.action.GetConnectorSecretResponse; import org.elasticsearch.xpack.application.connector.secrets.action.PostConnectorSecretRequest; import org.elasticsearch.xpack.application.connector.secrets.action.PostConnectorSecretResponse; @@ -42,6 +44,18 @@ public void testCreateAndGetConnectorSecret() throws Exception { assertThat(gotSecret.value(), notNullValue()); } + public void testDeleteConnectorSecret() throws Exception { + PostConnectorSecretRequest createSecretRequest = ConnectorSecretsTestUtils.getRandomPostConnectorSecretRequest(); + PostConnectorSecretResponse createdSecret = awaitPostConnectorSecret(createSecretRequest); + + String secretIdToDelete = createdSecret.id(); + DeleteConnectorSecretResponse resp = awaitDeleteConnectorSecret(secretIdToDelete); + assertThat(resp.isDeleted(), equalTo(true)); + + expectThrows(ResourceNotFoundException.class, () -> awaitGetConnectorSecret(secretIdToDelete)); + expectThrows(ResourceNotFoundException.class, () -> awaitDeleteConnectorSecret(secretIdToDelete)); + } + private PostConnectorSecretResponse awaitPostConnectorSecret(PostConnectorSecretRequest secretRequest) throws Exception { CountDownLatch latch = new CountDownLatch(1); @@ -101,4 +115,31 @@ public void onFailure(Exception e) { assertNotNull("Received null response from get request", resp.get()); return resp.get(); } + + private DeleteConnectorSecretResponse awaitDeleteConnectorSecret(String connectorSecretId) throws Exception { + CountDownLatch latch = new CountDownLatch(1); + final AtomicReference resp = new AtomicReference<>(null); + final AtomicReference exc = new AtomicReference<>(null); + + connectorSecretsIndexService.deleteSecret(connectorSecretId, new ActionListener() { + @Override + public void onResponse(DeleteConnectorSecretResponse response) { + resp.set(response); + latch.countDown(); + } + + @Override + public void onFailure(Exception e) { + exc.set(e); + latch.countDown(); + } + }); + + assertTrue("Timeout waiting for delete request", latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + if (exc.get() != null) { + throw exc.get(); + } + assertNotNull("Received null response from delete request", resp.get()); + return resp.get(); + } } diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsTestUtils.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsTestUtils.java index 5928ed4a1e5cd..13051505f9c4d 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsTestUtils.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsTestUtils.java @@ -7,6 +7,8 @@ package org.elasticsearch.xpack.application.connector.secrets; +import org.elasticsearch.xpack.application.connector.secrets.action.DeleteConnectorSecretRequest; +import org.elasticsearch.xpack.application.connector.secrets.action.DeleteConnectorSecretResponse; import org.elasticsearch.xpack.application.connector.secrets.action.GetConnectorSecretRequest; import org.elasticsearch.xpack.application.connector.secrets.action.GetConnectorSecretResponse; import org.elasticsearch.xpack.application.connector.secrets.action.PostConnectorSecretRequest; @@ -14,6 +16,7 @@ import static org.elasticsearch.test.ESTestCase.randomAlphaOfLength; import static org.elasticsearch.test.ESTestCase.randomAlphaOfLengthBetween; +import static org.elasticsearch.test.ESTestCase.randomBoolean; public class ConnectorSecretsTestUtils { @@ -34,4 +37,12 @@ public static PostConnectorSecretRequest getRandomPostConnectorSecretRequest() { public static PostConnectorSecretResponse getRandomPostConnectorSecretResponse() { return new PostConnectorSecretResponse(randomAlphaOfLength(10)); } + + public static DeleteConnectorSecretRequest getRandomDeleteConnectorSecretRequest() { + return new DeleteConnectorSecretRequest(randomAlphaOfLengthBetween(1, 20)); + } + + public static DeleteConnectorSecretResponse getRandomDeleteConnectorSecretResponse() { + return new DeleteConnectorSecretResponse(randomBoolean()); + } } diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretActionTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretActionTests.java new file mode 100644 index 0000000000000..5d9127527fc3a --- /dev/null +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretActionTests.java @@ -0,0 +1,34 @@ +/* + * 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.application.connector.secrets.action; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.application.connector.secrets.ConnectorSecretsTestUtils; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +public class DeleteConnectorSecretActionTests extends ESTestCase { + + public void testValidate_WhenConnectorSecretIdIsPresent_ExpectNoValidationError() { + DeleteConnectorSecretRequest request = ConnectorSecretsTestUtils.getRandomDeleteConnectorSecretRequest(); + ActionRequestValidationException exception = request.validate(); + + assertThat(exception, nullValue()); + } + + public void testValidate_WhenConnectorSecretIdIsEmpty_ExpectValidationError() { + DeleteConnectorSecretRequest requestWithMissingConnectorId = new DeleteConnectorSecretRequest(""); + ActionRequestValidationException exception = requestWithMissingConnectorId.validate(); + + assertThat(exception, notNullValue()); + assertThat(exception.getMessage(), containsString("id missing")); + } +} diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretRequestBWCSerializingTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretRequestBWCSerializingTests.java new file mode 100644 index 0000000000000..bdbdb1982173e --- /dev/null +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretRequestBWCSerializingTests.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.application.connector.secrets.action; + +import org.elasticsearch.TransportVersion; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.xpack.core.ml.AbstractBWCWireSerializationTestCase; + +import java.io.IOException; + +public class DeleteConnectorSecretRequestBWCSerializingTests extends AbstractBWCWireSerializationTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return DeleteConnectorSecretRequest::new; + } + + @Override + protected DeleteConnectorSecretRequest createTestInstance() { + return new DeleteConnectorSecretRequest(randomAlphaOfLengthBetween(1, 10)); + } + + @Override + protected DeleteConnectorSecretRequest mutateInstance(DeleteConnectorSecretRequest instance) throws IOException { + return randomValueOtherThan(instance, this::createTestInstance); + } + + @Override + protected DeleteConnectorSecretRequest mutateInstanceForVersion(DeleteConnectorSecretRequest instance, TransportVersion version) { + return new DeleteConnectorSecretRequest(instance.id()); + } +} diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretResponseBWCSerializingTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretResponseBWCSerializingTests.java new file mode 100644 index 0000000000000..964c5e15d845d --- /dev/null +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/DeleteConnectorSecretResponseBWCSerializingTests.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.xpack.application.connector.secrets.action; + +import org.elasticsearch.TransportVersion; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.xpack.application.connector.Connector; +import org.elasticsearch.xpack.application.connector.secrets.ConnectorSecretsTestUtils; +import org.elasticsearch.xpack.core.ml.AbstractBWCWireSerializationTestCase; + +import java.io.IOException; +import java.util.List; + +public class DeleteConnectorSecretResponseBWCSerializingTests extends AbstractBWCWireSerializationTestCase { + + @Override + public NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(List.of(new NamedWriteableRegistry.Entry(Connector.class, Connector.NAME, Connector::new))); + } + + @Override + protected Writeable.Reader instanceReader() { + return DeleteConnectorSecretResponse::new; + } + + @Override + protected DeleteConnectorSecretResponse createTestInstance() { + return ConnectorSecretsTestUtils.getRandomDeleteConnectorSecretResponse(); + } + + @Override + protected DeleteConnectorSecretResponse mutateInstance(DeleteConnectorSecretResponse instance) throws IOException { + return randomValueOtherThan(instance, this::createTestInstance); + } + + @Override + protected DeleteConnectorSecretResponse mutateInstanceForVersion(DeleteConnectorSecretResponse instance, TransportVersion version) { + return instance; + } +} diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/TransportDeleteConnectorSecretActionTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/TransportDeleteConnectorSecretActionTests.java new file mode 100644 index 0000000000000..165cc560ada1a --- /dev/null +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/TransportDeleteConnectorSecretActionTests.java @@ -0,0 +1,72 @@ +/* + * 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.application.connector.secrets.action; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.application.connector.secrets.ConnectorSecretsTestUtils; +import org.junit.Before; + +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.mockito.Mockito.mock; + +public class TransportDeleteConnectorSecretActionTests extends ESSingleNodeTestCase { + + private static final Long TIMEOUT_SECONDS = 10L; + + private final ThreadPool threadPool = new TestThreadPool(getClass().getName()); + private TransportDeleteConnectorSecretAction action; + + @Before + public void setup() { + TransportService transportService = new TransportService( + Settings.EMPTY, + mock(Transport.class), + threadPool, + TransportService.NOOP_TRANSPORT_INTERCEPTOR, + x -> null, + null, + Collections.emptySet() + ); + + action = new TransportDeleteConnectorSecretAction(transportService, mock(ActionFilters.class), client()); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + ThreadPool.terminate(threadPool, TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + + public void testDeleteConnectorSecret_ExpectNoWarnings() throws InterruptedException { + DeleteConnectorSecretRequest request = ConnectorSecretsTestUtils.getRandomDeleteConnectorSecretRequest(); + + executeRequest(request); + + ensureNoWarnings(); + } + + private void executeRequest(DeleteConnectorSecretRequest request) throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + action.doExecute(mock(Task.class), request, ActionListener.wrap(response -> latch.countDown(), exception -> latch.countDown())); + + boolean requestTimedOut = latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + + assertTrue("Timeout waiting for delete request", requestTimedOut); + } +} diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexServiceTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexServiceTests.java index 170ed25c0b302..2bbcf6c74b6fd 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexServiceTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexServiceTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.application.connector.Connector; import org.elasticsearch.xpack.application.connector.ConnectorFiltering; import org.elasticsearch.xpack.application.connector.ConnectorIndexService; @@ -100,7 +101,6 @@ public void testCreateConnectorSyncJob() throws Exception { ConnectorSyncJob connectorSyncJob = awaitGetConnectorSyncJob(response.getId()); - assertThat(connectorSyncJob.getId(), notNullValue()); assertThat(connectorSyncJob.getJobType(), equalTo(requestJobType)); assertThat(connectorSyncJob.getTriggerMethod(), equalTo(requestTriggerMethod)); assertThat(connectorSyncJob.getStatus(), equalTo(ConnectorSyncJob.DEFAULT_INITIAL_STATUS)); @@ -283,11 +283,31 @@ public void testListConnectorSyncJobs() throws Exception { ConnectorSyncJobIndexService.ConnectorSyncJobsResult nextTwoSyncJobs = awaitListConnectorSyncJobs(2, 2, null, null); ConnectorSyncJobIndexService.ConnectorSyncJobsResult lastSyncJobs = awaitListConnectorSyncJobs(4, 100, null, null); - ConnectorSyncJob firstSyncJob = firstTwoSyncJobs.connectorSyncJobs().get(0); - ConnectorSyncJob secondSyncJob = firstTwoSyncJobs.connectorSyncJobs().get(1); - ConnectorSyncJob thirdSyncJob = nextTwoSyncJobs.connectorSyncJobs().get(0); - ConnectorSyncJob fourthSyncJob = nextTwoSyncJobs.connectorSyncJobs().get(1); - ConnectorSyncJob fifthSyncJob = lastSyncJobs.connectorSyncJobs().get(0); + ConnectorSyncJob firstSyncJob = ConnectorSyncJob.fromXContentBytes( + firstTwoSyncJobs.connectorSyncJobs().get(0).getSourceRef(), + firstTwoSyncJobs.connectorSyncJobs().get(0).getDocId(), + XContentType.JSON + ); + ConnectorSyncJob secondSyncJob = ConnectorSyncJob.fromXContentBytes( + firstTwoSyncJobs.connectorSyncJobs().get(1).getSourceRef(), + firstTwoSyncJobs.connectorSyncJobs().get(1).getDocId(), + XContentType.JSON + ); + ConnectorSyncJob thirdSyncJob = ConnectorSyncJob.fromXContentBytes( + nextTwoSyncJobs.connectorSyncJobs().get(0).getSourceRef(), + nextTwoSyncJobs.connectorSyncJobs().get(0).getDocId(), + XContentType.JSON + ); + ConnectorSyncJob fourthSyncJob = ConnectorSyncJob.fromXContentBytes( + nextTwoSyncJobs.connectorSyncJobs().get(1).getSourceRef(), + nextTwoSyncJobs.connectorSyncJobs().get(1).getDocId(), + XContentType.JSON + ); + ConnectorSyncJob fifthSyncJob = ConnectorSyncJob.fromXContentBytes( + lastSyncJobs.connectorSyncJobs().get(0).getSourceRef(), + lastSyncJobs.connectorSyncJobs().get(0).getDocId(), + XContentType.JSON + ); assertThat(firstTwoSyncJobs.connectorSyncJobs().size(), equalTo(2)); assertThat(firstTwoSyncJobs.totalResults(), equalTo(5L)); @@ -337,7 +357,7 @@ public void testListConnectorSyncJobs_WithStatusPending_GivenOnePendingTwoCancel ConnectorSyncStatus.PENDING ); long numberOfResults = connectorSyncJobsResult.totalResults(); - String idOfReturnedSyncJob = connectorSyncJobsResult.connectorSyncJobs().get(0).getId(); + String idOfReturnedSyncJob = connectorSyncJobsResult.connectorSyncJobs().get(0).getDocId(); assertThat(numberOfResults, equalTo(1L)); assertThat(idOfReturnedSyncJob, equalTo(syncJobOneId)); @@ -363,7 +383,11 @@ public void testListConnectorSyncJobs_WithConnectorOneId_GivenTwoOverallOneFromC ); long numberOfResults = connectorSyncJobsResult.totalResults(); - String connectorIdOfReturnedSyncJob = connectorSyncJobsResult.connectorSyncJobs().get(0).getConnector().getConnectorId(); + String connectorIdOfReturnedSyncJob = ConnectorSyncJob.fromXContentBytes( + connectorSyncJobsResult.connectorSyncJobs().get(0).getSourceRef(), + connectorSyncJobsResult.connectorSyncJobs().get(0).getDocId(), + XContentType.JSON + ).getConnector().getConnectorId(); assertThat(numberOfResults, equalTo(1L)); assertThat(connectorIdOfReturnedSyncJob, equalTo(connectorOneId)); @@ -699,9 +723,15 @@ private ConnectorSyncJob awaitGetConnectorSyncJob(String connectorSyncJobId) thr final AtomicReference resp = new AtomicReference<>(null); final AtomicReference exc = new AtomicReference<>(null); - connectorSyncJobIndexService.getConnectorSyncJob(connectorSyncJobId, new ActionListener() { + connectorSyncJobIndexService.getConnectorSyncJob(connectorSyncJobId, new ActionListener() { @Override - public void onResponse(ConnectorSyncJob connectorSyncJob) { + public void onResponse(ConnectorSyncJobSearchResult searchResult) { + // Serialize the sourceRef to ConnectorSyncJob class for unit tests + ConnectorSyncJob connectorSyncJob = ConnectorSyncJob.fromXContentBytes( + searchResult.getSourceRef(), + searchResult.getDocId(), + XContentType.JSON + ); resp.set(connectorSyncJob); latch.countDown(); } diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTestUtils.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTestUtils.java index 96a12c9efac51..c53231cd79219 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTestUtils.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTestUtils.java @@ -7,7 +7,10 @@ package org.elasticsearch.xpack.application.connector.syncjob; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.Tuple; +import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.application.connector.ConnectorTestUtils; import org.elasticsearch.xpack.application.connector.syncjob.action.CancelConnectorSyncJobAction; import org.elasticsearch.xpack.application.connector.syncjob.action.CheckInConnectorSyncJobAction; @@ -19,7 +22,9 @@ import org.elasticsearch.xpack.application.connector.syncjob.action.UpdateConnectorSyncJobIngestionStatsAction; import org.elasticsearch.xpack.application.search.SearchApplicationTestUtils; +import java.io.IOException; import java.time.Instant; +import java.util.Map; import static org.elasticsearch.test.ESTestCase.randomAlphaOfLength; import static org.elasticsearch.test.ESTestCase.randomAlphaOfLengthBetween; @@ -65,6 +70,30 @@ public static ConnectorSyncJob getRandomConnectorSyncJob() { .build(); } + private static BytesReference convertSyncJobToBytesReference(ConnectorSyncJob syncJob) { + try { + return XContentHelper.toXContent((builder, params) -> { + syncJob.toInnerXContent(builder, params); + return builder; + }, XContentType.JSON, null, false); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Map convertSyncJobToGenericMap(ConnectorSyncJob syncJob) { + return XContentHelper.convertToMap(convertSyncJobToBytesReference(syncJob), true, XContentType.JSON).v2(); + } + + public static ConnectorSyncJobSearchResult getRandomSyncJobSearchResult() { + ConnectorSyncJob syncJob = getRandomConnectorSyncJob(); + + return new ConnectorSyncJobSearchResult.Builder().setId(randomAlphaOfLength(10)) + .setResultMap(convertSyncJobToGenericMap(syncJob)) + .setResultBytes(convertSyncJobToBytesReference(syncJob)) + .build(); + } + public static ConnectorSyncJobTriggerMethod getRandomConnectorSyncJobTriggerMethod() { ConnectorSyncJobTriggerMethod[] values = ConnectorSyncJobTriggerMethod.values(); return values[randomInt(values.length - 1)]; @@ -146,7 +175,7 @@ public static GetConnectorSyncJobAction.Request getRandomGetConnectorSyncJobRequ } public static GetConnectorSyncJobAction.Response getRandomGetConnectorSyncJobResponse() { - return new GetConnectorSyncJobAction.Response(getRandomConnectorSyncJob()); + return new GetConnectorSyncJobAction.Response(getRandomSyncJobSearchResult()); } public static ListConnectorSyncJobsAction.Request getRandomListConnectorSyncJobsActionRequest() { diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTests.java index 64f11923ce164..7b1a0f7d8dcf7 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTests.java @@ -87,7 +87,6 @@ public void testFromXContent_WithAllFields_AllSet() throws IOException { "created_at": "2023-12-01T14:18:43.07693Z", "deleted_document_count": 10, "error": "some-error", - "id": "HIC-JYwB9RqKhB7x_hIE", "indexed_document_count": 10, "indexed_document_volume": 10, "job_type": "full", @@ -101,7 +100,7 @@ public void testFromXContent_WithAllFields_AllSet() throws IOException { } """); - ConnectorSyncJob syncJob = ConnectorSyncJob.fromXContentBytes(new BytesArray(content), XContentType.JSON); + ConnectorSyncJob syncJob = ConnectorSyncJob.fromXContentBytes(new BytesArray(content), "HIC-JYwB9RqKhB7x_hIE", XContentType.JSON); assertThat(syncJob.getCancelationRequestedAt(), equalTo(Instant.parse("2023-12-01T14:19:39.394194Z"))); assertThat(syncJob.getCanceledAt(), equalTo(Instant.parse("2023-12-01T14:19:39.394194Z"))); @@ -170,7 +169,6 @@ public void testFromXContent_WithOnlyNonNullableFieldsSet_DoesNotThrow() throws }, "created_at": "2023-12-01T14:18:43.07693Z", "deleted_document_count": 10, - "id": "HIC-JYwB9RqKhB7x_hIE", "indexed_document_count": 10, "indexed_document_volume": 10, "job_type": "full", @@ -182,7 +180,7 @@ public void testFromXContent_WithOnlyNonNullableFieldsSet_DoesNotThrow() throws } """); - ConnectorSyncJob.fromXContentBytes(new BytesArray(content), XContentType.JSON); + ConnectorSyncJob.fromXContentBytes(new BytesArray(content), "HIC-JYwB9RqKhB7x_hIE", XContentType.JSON); } public void testFromXContent_WithAllNullableFieldsSetToNull_DoesNotThrow() throws IOException { @@ -230,7 +228,6 @@ public void testFromXContent_WithAllNullableFieldsSetToNull_DoesNotThrow() throw "created_at": "2023-12-01T14:18:43.07693Z", "deleted_document_count": 10, "error": null, - "id": "HIC-JYwB9RqKhB7x_hIE", "indexed_document_count": 10, "indexed_document_volume": 10, "job_type": "full", @@ -244,7 +241,7 @@ public void testFromXContent_WithAllNullableFieldsSetToNull_DoesNotThrow() throw } """); - ConnectorSyncJob.fromXContentBytes(new BytesArray(content), XContentType.JSON); + ConnectorSyncJob.fromXContentBytes(new BytesArray(content), "HIC-JYwB9RqKhB7x_hIE", XContentType.JSON); } public void testSyncJobConnectorFromXContent_WithAllFieldsSet() throws IOException { diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/action/ListConnectorSyncJobsActionResponseBWCSerializingTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/action/ListConnectorSyncJobsActionResponseBWCSerializingTests.java index 48a358ad043cd..bc7b6320dddbe 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/action/ListConnectorSyncJobsActionResponseBWCSerializingTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/action/ListConnectorSyncJobsActionResponseBWCSerializingTests.java @@ -33,7 +33,7 @@ protected Writeable.Reader instanceReader( @Override protected ListConnectorSyncJobsAction.Response createTestInstance() { return new ListConnectorSyncJobsAction.Response( - randomList(10, ConnectorSyncJobTestUtils::getRandomConnectorSyncJob), + randomList(10, ConnectorSyncJobTestUtils::getRandomSyncJobSearchResult), randomLongBetween(0, 100) ); } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/search/RuntimeUtils.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/search/RuntimeUtils.java index 011b0d09fd8c5..a2309c48578a3 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/search/RuntimeUtils.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/execution/search/RuntimeUtils.java @@ -18,7 +18,7 @@ import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.search.SearchHit; -import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.xpack.eql.EqlClientException; import org.elasticsearch.xpack.eql.EqlIllegalArgumentException; @@ -95,7 +95,7 @@ public static ActionListener multiSearchLogListener(ActionL } private static void logSearchResponse(SearchResponse response, Logger logger) { - List aggs = Collections.emptyList(); + List aggs = Collections.emptyList(); if (response.getAggregations() != null) { aggs = response.getAggregations().asList(); } diff --git a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sample/CircuitBreakerTests.java b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sample/CircuitBreakerTests.java index b880ec4b06926..afb9b590914dd 100644 --- a/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sample/CircuitBreakerTests.java +++ b/x-pack/plugin/eql/src/test/java/org/elasticsearch/xpack/eql/execution/sample/CircuitBreakerTests.java @@ -30,7 +30,7 @@ import org.elasticsearch.indices.breaker.HierarchyCircuitBreakerService; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.composite.InternalComposite; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.ESTestCase; @@ -221,7 +221,7 @@ protected void @SuppressWarnings("unchecked") void handleSearchRequest(ActionListener listener, SearchRequest searchRequest) { - Aggregations aggs = new Aggregations(List.of(newInternalComposite())); + InternalAggregations aggs = InternalAggregations.from(List.of(newInternalComposite())); ActionListener.respondAndRelease( listener, (Response) new SearchResponse( diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSinkHandler.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSinkHandler.java index c8a6dd9128d16..6f8fd67d348d6 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSinkHandler.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSinkHandler.java @@ -108,7 +108,10 @@ public void addCompletionListener(ActionListener listener) { completionFuture.addListener(listener); } - boolean isFinished() { + /** + * Returns true if an exchange is finished + */ + public boolean isFinished() { return completionFuture.isDone(); } diff --git a/x-pack/plugin/esql/qa/server/build.gradle b/x-pack/plugin/esql/qa/server/build.gradle index ff7ace533fb3a..fe5e08cda32f7 100644 --- a/x-pack/plugin/esql/qa/server/build.gradle +++ b/x-pack/plugin/esql/qa/server/build.gradle @@ -7,5 +7,7 @@ dependencies { // Common utilities from QL api project(xpackModule('ql:test-fixtures')) + // Requirement for some ESQL-specific utilities + implementation project(':x-pack:plugin:esql') api project(xpackModule('esql:qa:testFixtures')) } diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java index 100895feade16..9009441945509 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java @@ -46,11 +46,14 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.IntFunction; import static java.util.Collections.emptySet; +import static org.elasticsearch.common.logging.LoggerMessageFormat.format; import static org.elasticsearch.test.ListMatcher.matchesList; import static org.elasticsearch.test.MapMatcher.assertMap; import static org.elasticsearch.test.MapMatcher.matchesMap; +import static org.elasticsearch.xpack.esql.EsqlTestUtils.as; import static org.elasticsearch.xpack.esql.qa.rest.RestEsqlTestCase.Mode.ASYNC; import static org.elasticsearch.xpack.esql.qa.rest.RestEsqlTestCase.Mode.SYNC; import static org.hamcrest.Matchers.containsString; @@ -71,6 +74,29 @@ public abstract class RestEsqlTestCase extends ESRestTestCase { private static final List NO_WARNINGS = List.of(); + private static final String MAPPING_ALL_TYPES; + + static { + try (InputStream mappingPropertiesStream = RestEsqlTestCase.class.getResourceAsStream("/mapping-all-types.json")) { + String properties = new String(mappingPropertiesStream.readAllBytes(), StandardCharsets.UTF_8); + MAPPING_ALL_TYPES = "{\"mappings\": " + properties + "}"; + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private static final String DOCUMENT_TEMPLATE = """ + {"index":{"_id":"{}"}} + {"boolean": {}, "byte": {}, "date": {}, "double": {}, "float": {}, "half_float": {}, "scaled_float": {}, "integer": {},""" + """ + "ip": {}, "keyword": {}, "long": {}, "unsigned_long": {}, "short": {}, "text": {},""" + """ + "version": {}, "wildcard": {}} + """; + + // larger than any (unsigned) long + private static final String HUMONGOUS_DOUBLE = "1E300"; + private static final String INFINITY = "1.0/0.0"; + private static final String NAN = "0.0/0.0"; + public static boolean shouldLog() { return false; } @@ -295,6 +321,81 @@ public void testCSVNoHeaderMode() throws IOException { assertEquals("keyword0,0\r\n", actual); } + public void testOutOfRangeComparisons() throws IOException { + final int NUM_SINGLE_VALUE_ROWS = 100; + bulkLoadTestData(NUM_SINGLE_VALUE_ROWS); + bulkLoadTestData(10, NUM_SINGLE_VALUE_ROWS, false, RestEsqlTestCase::createDocumentWithMVs); + bulkLoadTestData(5, NUM_SINGLE_VALUE_ROWS + 10, false, RestEsqlTestCase::createDocumentWithNulls); + + List dataTypes = List.of( + "alias_integer", + "byte", + "short", + "integer", + "long", + // TODO: https://github.com/elastic/elasticsearch/issues/102935 + // "unsigned_long", + // TODO: https://github.com/elastic/elasticsearch/issues/100130 + // "half_float", + // "float", + "double", + "scaled_float" + ); + + String lessOrLessEqual = randomFrom(" < ", " <= "); + String largerOrLargerEqual = randomFrom(" > ", " >= "); + String inEqualPlusMinus = randomFrom(" != ", " != -"); + String equalPlusMinus = randomFrom(" == ", " == -"); + // TODO: once we do not support infinity and NaN anymore, remove INFINITY/NAN cases. + // https://github.com/elastic/elasticsearch/issues/98698#issuecomment-1847423390 + String humongousPositiveLiteral = randomFrom(HUMONGOUS_DOUBLE, INFINITY); + String nanOrNull = randomFrom(NAN, "to_double(null)"); + + List trueForSingleValuesPredicates = List.of( + lessOrLessEqual + humongousPositiveLiteral, + largerOrLargerEqual + " -" + humongousPositiveLiteral, + inEqualPlusMinus + humongousPositiveLiteral, + inEqualPlusMinus + NAN + ); + List alwaysFalsePredicates = List.of( + lessOrLessEqual + " -" + humongousPositiveLiteral, + largerOrLargerEqual + humongousPositiveLiteral, + equalPlusMinus + humongousPositiveLiteral, + lessOrLessEqual + nanOrNull, + largerOrLargerEqual + nanOrNull, + equalPlusMinus + nanOrNull, + inEqualPlusMinus + "to_double(null)" + ); + + for (String fieldWithType : dataTypes) { + for (String truePredicate : trueForSingleValuesPredicates) { + String comparison = fieldWithType + truePredicate; + var query = builder().query(format(null, "from {} | where {}", testIndexName(), comparison)); + List expectedWarnings = List.of( + "Line 1:29: evaluation of [" + comparison + "] failed, treating result as null. Only first 20 failures recorded.", + "Line 1:29: java.lang.IllegalArgumentException: single-value function encountered multi-value" + ); + var result = runEsql(query, expectedWarnings, mode); + + var values = as(result.get("values"), ArrayList.class); + assertThat( + format(null, "Comparison [{}] should return all rows with single values.", comparison), + values.size(), + is(NUM_SINGLE_VALUE_ROWS) + ); + } + + for (String falsePredicate : alwaysFalsePredicates) { + String comparison = fieldWithType + falsePredicate; + var query = builder().query(format(null, "from {} | where {}", testIndexName(), comparison)); + var result = runEsql(query); + + var values = as(result.get("values"), ArrayList.class); + assertThat(format(null, "Comparison [{}] should return no rows.", comparison), values.size(), is(0)); + } + } + } + public void testWarningHeadersOnFailedConversions() throws IOException { int count = randomFrom(10, 40, 60); bulkLoadTestData(count); @@ -720,37 +821,90 @@ private static Set mutedWarnings() { } private static void bulkLoadTestData(int count) throws IOException { - Request request = new Request("PUT", "/" + testIndexName()); - request.setJsonEntity(""" - { - "mappings": { - "properties": { - "keyword": { - "type": "keyword" - }, - "integer": { - "type": "integer" - } - } - } - }"""); - assertEquals(200, client().performRequest(request).getStatusLine().getStatusCode()); + bulkLoadTestData(count, 0, true, RestEsqlTestCase::createDocument); + } + + private static void bulkLoadTestData(int count, int firstIndex, boolean createIndex, IntFunction createDocument) + throws IOException { + Request request; + if (createIndex) { + request = new Request("PUT", "/" + testIndexName()); + request.setJsonEntity(MAPPING_ALL_TYPES); + assertEquals(200, client().performRequest(request).getStatusLine().getStatusCode()); + } if (count > 0) { request = new Request("POST", "/" + testIndexName() + "/_bulk"); request.addParameter("refresh", "true"); + StringBuilder bulk = new StringBuilder(); for (int i = 0; i < count; i++) { - bulk.append(org.elasticsearch.core.Strings.format(""" - {"index":{"_id":"%s"}} - {"keyword":"keyword%s", "integer":%s} - """, i, i, i)); + bulk.append(createDocument.apply(i + firstIndex)); } request.setJsonEntity(bulk.toString()); assertEquals(200, client().performRequest(request).getStatusLine().getStatusCode()); } } + private static String createDocument(int i) { + return format( + null, + DOCUMENT_TEMPLATE, + i, + ((i & 1) == 0), + (i % 256), + i, + (i + 0.1), + (i + 0.1), + (i + 0.1), + (i + 0.1), + i, + "\"127.0.0." + (i % 256) + "\"", + "\"keyword" + i + "\"", + i, + i, + (i % Short.MAX_VALUE), + "\"text" + i + "\"", + "\"1.2." + i + "\"", + "\"wildcard" + i + "\"" + ); + } + + private static String createDocumentWithMVs(int i) { + return format( + null, + DOCUMENT_TEMPLATE, + i, + repeatValueAsMV((i & 1) == 0), + repeatValueAsMV(i % 256), + repeatValueAsMV(i), + repeatValueAsMV(i + 0.1), + repeatValueAsMV(i + 0.1), + repeatValueAsMV(i + 0.1), + repeatValueAsMV(i + 0.1), + repeatValueAsMV(i), + repeatValueAsMV("\"127.0.0." + (i % 256) + "\""), + repeatValueAsMV("\"keyword" + i + "\""), + repeatValueAsMV(i), + repeatValueAsMV(i), + repeatValueAsMV(i % Short.MAX_VALUE), + repeatValueAsMV("\"text" + i + "\""), + repeatValueAsMV("\"1.2." + i + "\""), + repeatValueAsMV("\"wildcard" + i + "\"") + ); + } + + private static String createDocumentWithNulls(int i) { + return format(null, """ + {"index":{"_id":"{}"}} + {} + """, i); + } + + private static String repeatValueAsMV(Object value) { + return "[" + value + ", " + value + "]"; + } + private static RequestObjectBuilder builder() throws IOException { return new RequestObjectBuilder(); } diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-all-types.json b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-all-types.json new file mode 100644 index 0000000000000..ee1ef56a63dfb --- /dev/null +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/mapping-all-types.json @@ -0,0 +1,61 @@ +{ + "properties" : { + "alias_integer": { + "type": "alias", + "path": "integer" + }, + "boolean": { + "type": "boolean" + }, + "byte" : { + "type" : "byte" + }, + "constant_keyword-foo": { + "type": "constant_keyword", + "value": "foo" + }, + "date": { + "type": "date" + }, + "double": { + "type": "double" + }, + "float": { + "type": "float" + }, + "half_float": { + "type": "half_float" + }, + "scaled_float": { + "type": "scaled_float", + "scaling_factor": 100 + }, + "integer" : { + "type" : "integer" + }, + "ip": { + "type": "ip" + }, + "keyword" : { + "type" : "keyword" + }, + "long": { + "type": "long" + }, + "unsigned_long": { + "type": "unsigned_long" + }, + "short": { + "type": "short" + }, + "text" : { + "type" : "text" + }, + "version": { + "type": "version" + }, + "wildcard": { + "type": "wildcard" + } + } +} diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/show.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/show.csv-spec index e1c1b276a90eb..16a4ebf8fb03e 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/show.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/show.csv-spec @@ -16,7 +16,7 @@ asin |"double asin(n:double|integer|long|unsigned_long)"|n atan |"double atan(n:double|integer|long|unsigned_long)" |n |"double|integer|long|unsigned_long" | "" |double | "Inverse tangent trigonometric function." | false | false | false atan2 |"double atan2(y:double|integer|long|unsigned_long, x:double|integer|long|unsigned_long)" |[y, x] |["double|integer|long|unsigned_long", "double|integer|long|unsigned_long"] |["", ""] |double | "The angle between the positive x-axis and the ray from the origin to the point (x , y) in the Cartesian plane." | [false, false] | false | false auto_bucket |"double|date auto_bucket(field:integer|long|double|date, buckets:integer, from:integer|long|double|date|string, to:integer|long|double|date|string)" |[field, buckets, from, to] |["integer|long|double|date", "integer", "integer|long|double|date|string", "integer|long|double|date|string"] |["", "", "", ""] | "double|date" | "Creates human-friendly buckets and returns a datetime value for each row that corresponds to the resulting bucket the row falls into." | [false, false, false, false] | false | false -avg |"double avg(field:double|integer|long|unsigned_long)" |field |"double|integer|long|unsigned_long" | "" |double | "The average of a numeric field." | false | false | true +avg |"double avg(field:double|integer|long)" |field |"double|integer|long" | "" |double | "The average of a numeric field." | false | false | true case |"boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version case(condition:boolean, rest...:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version)" |[condition, rest] |["boolean", "boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version"] |["", ""] |"boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version" | "Accepts pairs of conditions and values. The function returns the value that belongs to the first condition that evaluates to true." | [false, false] | true | false ceil |"double|integer|long|unsigned_long ceil(n:double|integer|long|unsigned_long)" |n |"double|integer|long|unsigned_long" | "" | "double|integer|long|unsigned_long" | "Round a number up to the nearest integer." | false | false | false cidr_match |boolean cidr_match(ip:ip, blockX...:keyword) |[ip, blockX] |[ip, keyword] |["", "CIDR block to test the IP against."] |boolean | "Returns true if the provided IP is contained in one of the provided CIDR blocks." | [false, false] | true | false @@ -25,7 +25,7 @@ concat |"keyword concat(first:keyword|text, rest...:keyword|te cos |"double cos(n:double|integer|long|unsigned_long)" |n |"double|integer|long|unsigned_long" | "An angle, in radians" |double | "Returns the trigonometric cosine of an angle" | false | false | false cosh |"double cosh(n:double|integer|long|unsigned_long)" |n |"double|integer|long|unsigned_long" | "The number who's hyperbolic cosine is to be returned" |double | "Returns the hyperbolic cosine of a number" | false | false | false count |"long count(?field:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version)" |field |"boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version" | "Column or literal for which to count the number of values." |long | "Returns the total number (count) of input values." | true | false | true -count_distinct |"long count_distinct(field:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version, ?precision:integer)" |[field, precision] |["boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version, integer"] |["Column or literal for which to count the number of distinct values.", ""] |long | "Returns the approximate number of distinct values." | [false, true] | false | true +count_distinct |"long count_distinct(field:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|version, ?precision:integer)" |[field, precision] |["boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|version, integer"] |["Column or literal for which to count the number of distinct values.", ""] |long | "Returns the approximate number of distinct values." | [false, true] | false | true date_diff |"integer date_diff(unit:keyword|text, startTimestamp:date, endTimestamp:date)"|[unit, startTimestamp, endTimestamp] |["keyword|text", "date", "date"] |["A valid date unit", "A string representing a start timestamp", "A string representing an end timestamp"] |integer | "Subtract 2 dates and return their difference in multiples of a unit specified in the 1st argument" | [false, false, false] | false | false date_extract |long date_extract(date_part:keyword, field:date) |[date_part, field] |[keyword, date] |["Part of the date to extract. Can be: aligned_day_of_week_in_month; aligned_day_of_week_in_year; aligned_week_of_month; aligned_week_of_year; ampm_of_day; clock_hour_of_ampm; clock_hour_of_day; day_of_month; day_of_week; day_of_year; epoch_day; era; hour_of_ampm; hour_of_day; instant_seconds; micro_of_day; micro_of_second; milli_of_day; milli_of_second; minute_of_day; minute_of_hour; month_of_year; nano_of_day; nano_of_second; offset_seconds; proleptic_month; second_of_day; second_of_minute; year; or year_of_era.", "Date expression"] |long | "Extracts parts of a date, like year, month, day, hour." | [false, false] | false | false date_format |keyword date_format(?format:keyword, date:date) |[format, date] |[keyword, date] |["A valid date pattern", "Date expression"] |keyword | "Returns a string representation of a date, in the provided format." | [true, false] | false | false @@ -40,10 +40,10 @@ left |"keyword left(str:keyword|text, length:integer)" length |"integer length(str:keyword|text)" |str |"keyword|text" | "" |integer | "Returns the character length of a string." | false | false | false log10 |"double log10(n:double|integer|long|unsigned_long)" |n |"double|integer|long|unsigned_long" | "" |double | "Returns the log base 10." | false | false | false ltrim |"keyword|text ltrim(str:keyword|text)" |str |"keyword|text" | "" |"keyword|text" |Removes leading whitespaces from a string.| false | false | false -max |"double|integer|long|unsigned_long max(field:double|integer|long|unsigned_long)" |field |"double|integer|long|unsigned_long" | "" |"double|integer|long|unsigned_long" | "The maximum value of a numeric field." | false | false | true -median |"double|integer|long|unsigned_long median(field:double|integer|long|unsigned_long)" |field |"double|integer|long|unsigned_long" | "" |"double|integer|long|unsigned_long" | "The value that is greater than half of all values and less than half of all values." | false | false | true -median_absolute_deviation|"double|integer|long|unsigned_long median_absolute_deviation(field:double|integer|long|unsigned_long)" |field |"double|integer|long|unsigned_long" | "" |"double|integer|long|unsigned_long" | "The median absolute deviation, a measure of variability." | false | false | true -min |"double|integer|long|unsigned_long min(field:double|integer|long|unsigned_long)" |field |"double|integer|long|unsigned_long" | "" |"double|integer|long|unsigned_long" | "The minimum value of a numeric field." | false | false | true +max |"double|integer|long max(field:double|integer|long)" |field |"double|integer|long" | "" |"double|integer|long" | "The maximum value of a numeric field." | false | false | true +median |"double|integer|long median(field:double|integer|long)" |field |"double|integer|long" | "" |"double|integer|long" | "The value that is greater than half of all values and less than half of all values." | false | false | true +median_absolute_deviation|"double|integer|long median_absolute_deviation(field:double|integer|long)" |field |"double|integer|long" | "" |"double|integer|long" | "The median absolute deviation, a measure of variability." | false | false | true +min |"double|integer|long min(field:double|integer|long)" |field |"double|integer|long" | "" |"double|integer|long" | "The minimum value of a numeric field." | false | false | true mv_avg |"double mv_avg(field:double|integer|long|unsigned_long)" |field |"double|integer|long|unsigned_long" | "" |double | "Converts a multivalued field into a single valued field containing the average of all of the values." | false | false | false mv_concat |"keyword mv_concat(v:text|keyword, delim:text|keyword)" |[v, delim] |["text|keyword", "text|keyword"] |["values to join", "delimiter"] |keyword | "Reduce a multivalued string field to a single valued field by concatenating all values." | [false, false] | false | false mv_count |"integer mv_count(v:boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|unsigned_long|version)" |v | "boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|unsigned_long|version" | "" | integer | "Reduce a multivalued field to a single valued field containing the count of values." | false | false | false @@ -55,7 +55,7 @@ mv_median |"double|integer|long|unsigned_long mv_median(v:double| mv_min |"boolean|date|double|integer|ip|keyword|long|text|unsigned_long|version mv_min(v:boolean|date|double|integer|ip|keyword|long|text|unsigned_long|version)" |v | "boolean|date|double|integer|ip|keyword|long|text|unsigned_long|version" | "" |"boolean|date|double|integer|ip|keyword|long|text|unsigned_long|version" | "Reduce a multivalued field to a single valued field containing the minimum value." | false | false | false mv_sum |"double|integer|long|unsigned_long mv_sum(v:double|integer|long|unsigned_long)" |v |"double|integer|long|unsigned_long" | "" |"double|integer|long|unsigned_long" | "Converts a multivalued field into a single valued field containing the sum of all of the values." | false | false | false now |date now() | null |null | null |date | "Returns current date and time." | null | false | false -percentile |"double|integer|long|unsigned_long percentile(field:double|integer|long|unsigned_long, percentile:double|integer|long)" |[field, percentile] |["double|integer|long|unsigned_long, double|integer|long"] |["", ""] |"double|integer|long|unsigned_long" | "The value at which a certain percentage of observed values occur." | [false, false] | false | true +percentile |"double|integer|long percentile(field:double|integer|long, percentile:double|integer|long)" |[field, percentile] |["double|integer|long, double|integer|long"] |["", ""] |"double|integer|long" | "The value at which a certain percentage of observed values occur." | [false, false] | false | true pi |double pi() | null | null | null |double | "The ratio of a circle’s circumference to its diameter." | null | false | false pow |"double pow(base:double|integer|long|unsigned_long, exponent:double|integer|long|unsigned_long)" |[base, exponent] |["double|integer|long|unsigned_long", "double|integer|long|unsigned_long"] |["", ""] |double | "Returns the value of a base raised to the power of an exponent." | [false, false] | false | false replace |"keyword replace(str:keyword|text, regex:keyword|text, newStr:keyword|text)" | [str, regex, newStr] | ["keyword|text", "keyword|text", "keyword|text"] |["", "", ""] |keyword | "The function substitutes in the string any match of the regular expression with the replacement string." | [false, false, false]| false | false @@ -69,7 +69,7 @@ sqrt |"double sqrt(n:double|integer|long|unsigned_long)" st_centroid |"geo_point|cartesian_point st_centroid(field:geo_point|cartesian_point)" |field |"geo_point|cartesian_point" | "" |"geo_point|cartesian_point" | "The centroid of a spatial field." | false | false | true starts_with |"boolean starts_with(str:keyword|text, prefix:keyword|text)" |[str, prefix] |["keyword|text", "keyword|text"] |["", ""] |boolean | "Returns a boolean that indicates whether a keyword string starts with another string" | [false, false] | false | false substring |"keyword substring(str:keyword|text, start:integer, ?length:integer)" |[str, start, length] |["keyword|text", "integer", "integer"] |["", "", ""] |keyword | "Returns a substring of a string, specified by a start position and an optional length" | [false, false, true]| false | false -sum |"long sum(field:double|integer|long|unsigned_long)" |field |"double|integer|long|unsigned_long" | "" |long | "The sum of a numeric field." | false | false | true +sum |"long sum(field:double|integer|long)" |field |"double|integer|long" | "" |long | "The sum of a numeric field." | false | false | true tan |"double tan(n:double|integer|long|unsigned_long)" |n |"double|integer|long|unsigned_long" | "An angle, in radians" |double | "Returns the trigonometric tangent of an angle" | false | false | false tanh |"double tanh(n:double|integer|long|unsigned_long)" |n |"double|integer|long|unsigned_long" | "The number to return the hyperbolic tangent of" |double | "Returns the hyperbolic tangent of a number" | false | false | false tau |double tau() | null | null | null |double | "The ratio of a circle’s circumference to its radius." | null | false | false @@ -112,7 +112,7 @@ synopsis:keyword "double atan(n:double|integer|long|unsigned_long)" "double atan2(y:double|integer|long|unsigned_long, x:double|integer|long|unsigned_long)" "double|date auto_bucket(field:integer|long|double|date, buckets:integer, from:integer|long|double|date|string, to:integer|long|double|date|string)" -"double avg(field:double|integer|long|unsigned_long)" +"double avg(field:double|integer|long)" "boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version case(condition:boolean, rest...:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version)" "double|integer|long|unsigned_long ceil(n:double|integer|long|unsigned_long)" boolean cidr_match(ip:ip, blockX...:keyword) @@ -121,7 +121,7 @@ boolean cidr_match(ip:ip, blockX...:keyword) "double cos(n:double|integer|long|unsigned_long)" "double cosh(n:double|integer|long|unsigned_long)" "long count(?field:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version)" -"long count_distinct(field:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|unsigned_long|version, ?precision:integer)" +"long count_distinct(field:boolean|cartesian_point|date|double|geo_point|integer|ip|keyword|long|text|version, ?precision:integer)" "integer date_diff(unit:keyword|text, startTimestamp:date, endTimestamp:date)" long date_extract(date_part:keyword, field:date) keyword date_format(?format:keyword, date:date) @@ -136,10 +136,10 @@ double e() "integer length(str:keyword|text)" "double log10(n:double|integer|long|unsigned_long)" "keyword|text ltrim(str:keyword|text)" -"double|integer|long|unsigned_long max(field:double|integer|long|unsigned_long)" -"double|integer|long|unsigned_long median(field:double|integer|long|unsigned_long)" -"double|integer|long|unsigned_long median_absolute_deviation(field:double|integer|long|unsigned_long)" -"double|integer|long|unsigned_long min(field:double|integer|long|unsigned_long)" +"double|integer|long max(field:double|integer|long)" +"double|integer|long median(field:double|integer|long)" +"double|integer|long median_absolute_deviation(field:double|integer|long)" +"double|integer|long min(field:double|integer|long)" "double mv_avg(field:double|integer|long|unsigned_long)" "keyword mv_concat(v:text|keyword, delim:text|keyword)" "integer mv_count(v:boolean|cartesian_point|cartesian_shape|date|double|geo_point|geo_shape|integer|ip|keyword|long|text|unsigned_long|version)" @@ -151,7 +151,7 @@ double e() "boolean|date|double|integer|ip|keyword|long|text|unsigned_long|version mv_min(v:boolean|date|double|integer|ip|keyword|long|text|unsigned_long|version)" "double|integer|long|unsigned_long mv_sum(v:double|integer|long|unsigned_long)" date now() -"double|integer|long|unsigned_long percentile(field:double|integer|long|unsigned_long, percentile:double|integer|long)" +"double|integer|long percentile(field:double|integer|long, percentile:double|integer|long)" double pi() "double pow(base:double|integer|long|unsigned_long, exponent:double|integer|long|unsigned_long)" "keyword replace(str:keyword|text, regex:keyword|text, newStr:keyword|text)" @@ -165,7 +165,7 @@ double pi() "geo_point|cartesian_point st_centroid(field:geo_point|cartesian_point)" "boolean starts_with(str:keyword|text, prefix:keyword|text)" "keyword substring(str:keyword|text, start:integer, ?length:integer)" -"long sum(field:double|integer|long|unsigned_long)" +"long sum(field:double|integer|long)" "double tan(n:double|integer|long|unsigned_long)" "double tanh(n:double|integer|long|unsigned_long)" double tau() diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec index 0dd2f4f937421..65b01aae461e5 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec @@ -831,8 +831,10 @@ FROM employees // end::statsCalcMultipleValues[] ; +// tag::statsCalcMultipleValues-result[] avg_lang:double | max_lang:integer 3.1222222222222222|5 +// end::statsCalcMultipleValues-result[] ; docsStatsGroupByMultipleValues @@ -983,3 +985,130 @@ ROW a = 1, c = null COUNT(c):long | a:integer 0 | 1 ; + + +countVersion#[skip:-8.12.99,reason:bug fixed in 8.13+] +from apps | stats c = count(version), cd = count_distinct(version); + +c:long | cd:long +12 | 9 +; + + +docsStatsAvgNestedExpression#[skip:-8.12.99,reason:supported in 8.13+] +// tag::docsStatsAvgNestedExpression[] +FROM employees +| STATS avg_salary_change = AVG(MV_AVG(salary_change)) +// end::docsStatsAvgNestedExpression[] +; + +// tag::docsStatsAvgNestedExpression-result[] +avg_salary_change:double +1.3904535864978902 +// end::docsStatsAvgNestedExpression-result[] +; + +docsStatsByExpression#[skip:-8.12.99,reason:supported in 8.13+] +// tag::docsStatsByExpression[] +FROM employees +| STATS my_count = COUNT() BY LEFT(last_name, 1) +| SORT `LEFT(last_name, 1)` +// end::docsStatsByExpression[] +; + +// tag::docsStatsByExpression-result[] +my_count:long |LEFT(last_name, 1):keyword +2 |A +11 |B +5 |C +5 |D +2 |E +4 |F +4 |G +6 |H +2 |J +3 |K +5 |L +12 |M +4 |N +1 |O +7 |P +5 |R +13 |S +4 |T +2 |W +3 |Z +// end::docsStatsByExpression-result[] +; + +docsStatsMaxNestedExpression#[skip:-8.12.99,reason:supported in 8.13+] +// tag::docsStatsMaxNestedExpression[] +FROM employees +| STATS max_avg_salary_change = MAX(MV_AVG(salary_change)) +// end::docsStatsMaxNestedExpression[] +; + +// tag::docsStatsMaxNestedExpression-result[] +max_avg_salary_change:double +13.75 +// end::docsStatsMaxNestedExpression-result[] +; + +docsStatsMinNestedExpression#[skip:-8.12.99,reason:supported in 8.13+] +// tag::docsStatsMinNestedExpression[] +FROM employees +| STATS min_avg_salary_change = MIN(MV_AVG(salary_change)) +// end::docsStatsMinNestedExpression[] +; + +// tag::docsStatsMinNestedExpression-result[] +min_avg_salary_change:double +-8.46 +// end::docsStatsMinNestedExpression-result[] +; + +docsStatsSumNestedExpression#[skip:-8.12.99,reason:supported in 8.13+] +// tag::docsStatsSumNestedExpression[] +FROM employees +| STATS total_salary_changes = SUM(MV_MAX(salary_change)) +// end::docsStatsSumNestedExpression[] +; + +// tag::docsStatsSumNestedExpression-result[] +total_salary_changes:double +446.75 +// end::docsStatsSumNestedExpression-result[] +; + +docsCountWithExpression#[skip:-8.12.99,reason:supported in 8.13+] +// tag::docsCountWithExpression[] +ROW words="foo;bar;baz;qux;quux;foo" +| STATS word_count = COUNT(SPLIT(words, ";")) +// end::docsCountWithExpression[] +; + +// tag::docsCountWithExpression-result[] +word_count:long +6 +// end::docsCountWithExpression-result[] +; + +countMultiValuesRow +ROW keyword_field = ["foo", "bar"], int_field = [1, 2, 3] | STATS ck = COUNT(keyword_field), ci = COUNT(int_field), c = COUNT(*); + +ck:l | ci:l | c:l +2 | 3 | 1 +; + +countSource +FROM employees | +STATS ck = COUNT(job_positions), + cb = COUNT(is_rehired), + cd = COUNT(salary_change), + ci = COUNT(salary_change.int), + c = COUNT(*), + csv = COUNT(emp_no); + +ck:l | cb:l | cd:l | ci:l | c:l | csv:l +221 | 204 | 183 | 183 | 100 | 100 +; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_count_distinct.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_count_distinct.csv-spec index 8f926fd8f6ed7..b4f6a701ec272 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_count_distinct.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_count_distinct.csv-spec @@ -153,6 +153,18 @@ m:long | languages:i 10 | null ; +docsCountDistinctWithExpression#[skip:-8.12.99,reason:supported in 8.13+] +// tag::docsCountDistinctWithExpression[] +ROW words="foo;bar;baz;qux;quux;foo" +| STATS distinct_word_count = COUNT_DISTINCT(SPLIT(words, ";")) +// end::docsCountDistinctWithExpression[] +; + +// tag::docsCountDistinctWithExpression-result[] +distinct_word_count:long +5 +// end::docsCountDistinctWithExpression-result[] +; countDistinctWithGroupPrecisionAndNestedExpression#[skip:-8.12.99,reason:supported in 8.13+] from employees | stats m = count_distinct(height + 5, 9876) by languages | sort languages; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_percentile.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_percentile.csv-spec index 091a625c7e10d..8ac93dc5455bd 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_percentile.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats_percentile.csv-spec @@ -156,3 +156,42 @@ from employees | stats p50 = percentile(salary_change, -(50-1)+99); p50:double 0.75 ; + +docsStatsMedianNestedExpression#[skip:-8.12.99,reason:supported in 8.13+] +// tag::docsStatsMedianNestedExpression[] +FROM employees +| STATS median_max_salary_change = MEDIAN(MV_MAX(salary_change)) +// end::docsStatsMedianNestedExpression[] +; + +// tag::docsStatsMedianNestedExpression-result[] +median_max_salary_change:double +7.69 +// end::docsStatsMedianNestedExpression-result[] +; + +docsStatsMADNestedExpression#[skip:-8.12.99,reason:supported in 8.13+] +// tag::docsStatsMADNestedExpression[] +FROM employees +| STATS m_a_d_max_salary_change = MEDIAN_ABSOLUTE_DEVIATION(MV_MAX(salary_change)) +// end::docsStatsMADNestedExpression[] +; + +// tag::docsStatsMADNestedExpression-result[] +m_a_d_max_salary_change:double +5.69 +// end::docsStatsMADNestedExpression-result[] +; + +docsStatsPercentileNestedExpression#[skip:-8.12.99,reason:supported in 8.13+] +// tag::docsStatsPercentileNestedExpression[] +FROM employees +| STATS p80_max_salary_change = PERCENTILE(MV_MAX(salary_change), 80) +// end::docsStatsPercentileNestedExpression[] +; + +// tag::docsStatsPercentileNestedExpression-result[] +p80_max_salary_change:double +12.132 +// end::docsStatsPercentileNestedExpression-result[] +; diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractEsqlIntegTestCase.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractEsqlIntegTestCase.java index 0590caf2019b4..5ba9c622d85da 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractEsqlIntegTestCase.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/AbstractEsqlIntegTestCase.java @@ -185,6 +185,9 @@ protected static QueryPragmas randomPragmas() { }; settings.put("page_size", pageSize); } + if (randomBoolean()) { + settings.put("max_concurrent_shards_per_node", randomIntBetween(1, 10)); + } } return new QueryPragmas(settings.build()); } diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/ManyShardsIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/ManyShardsIT.java index a39439d33bfba..fb598cb855013 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/ManyShardsIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/ManyShardsIT.java @@ -13,11 +13,20 @@ import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.search.MockSearchService; +import org.elasticsearch.search.SearchService; import org.elasticsearch.xpack.esql.plugin.QueryPragmas; +import org.hamcrest.Matchers; +import org.junit.Before; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; /** * Make sures that we can run many concurrent requests with large number of shards with any data_partitioning. @@ -25,7 +34,15 @@ @LuceneTestCase.SuppressFileSystems(value = "HandleLimitFS") public class ManyShardsIT extends AbstractEsqlIntegTestCase { - public void testConcurrentQueries() throws Exception { + @Override + protected Collection> getMockPlugins() { + var plugins = new ArrayList<>(super.getMockPlugins()); + plugins.add(MockSearchService.TestPlugin.class); + return plugins; + } + + @Before + public void setupIndices() { int numIndices = between(10, 20); for (int i = 0; i < numIndices; i++) { String index = "test-" + i; @@ -49,6 +66,9 @@ public void testConcurrentQueries() throws Exception { } bulk.get(); } + } + + public void testConcurrentQueries() throws Exception { int numQueries = between(10, 20); Thread[] threads = new Thread[numQueries]; CountDownLatch latch = new CountDownLatch(1); @@ -76,4 +96,57 @@ public void testConcurrentQueries() throws Exception { thread.join(); } } + + static class SearchContextCounter { + private final int maxAllowed; + private final AtomicInteger current = new AtomicInteger(); + + SearchContextCounter(int maxAllowed) { + this.maxAllowed = maxAllowed; + } + + void onNewContext() { + int total = current.incrementAndGet(); + assertThat("opening more shards than the limit", total, Matchers.lessThanOrEqualTo(maxAllowed)); + } + + void onContextReleased() { + int total = current.decrementAndGet(); + assertThat(total, Matchers.greaterThanOrEqualTo(0)); + } + } + + public void testLimitConcurrentShards() { + Iterable searchServices = internalCluster().getInstances(SearchService.class); + try { + var queries = List.of( + "from test-* | stats count(user) by tags", + "from test-* | stats count(user) by tags | LIMIT 0", + "from test-* | stats count(user) by tags | LIMIT 1", + "from test-* | stats count(user) by tags | LIMIT 1000", + "from test-* | LIMIT 0", + "from test-* | LIMIT 1", + "from test-* | LIMIT 1000", + "from test-* | SORT tags | LIMIT 0", + "from test-* | SORT tags | LIMIT 1", + "from test-* | SORT tags | LIMIT 1000" + ); + for (String q : queries) { + QueryPragmas pragmas = randomPragmas(); + for (SearchService searchService : searchServices) { + SearchContextCounter counter = new SearchContextCounter(pragmas.maxConcurrentShardsPerNode()); + var mockSearchService = (MockSearchService) searchService; + mockSearchService.setOnPutContext(r -> counter.onNewContext()); + mockSearchService.setOnRemoveContext(r -> counter.onContextReleased()); + } + run(q, pragmas).close(); + } + } finally { + for (SearchService searchService : searchServices) { + var mockSearchService = (MockSearchService) searchService; + mockSearchService.setOnPutContext(r -> {}); + mockSearchService.setOnRemoveContext(r -> {}); + } + } + } } diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/WarningsIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/WarningsIT.java index fb6d23695f837..0f05add15da53 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/WarningsIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/WarningsIT.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.esql.action; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.transport.TransportService; @@ -38,7 +39,11 @@ public void testCollectWarnings() throws Exception { client().admin() .indices() .prepareCreate("index-1") - .setSettings(Settings.builder().put("index.routing.allocation.require._name", node1)) + .setSettings( + Settings.builder() + .put("index.routing.allocation.require._name", node1) + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, between(1, 5)) + ) .setMapping("host", "type=keyword") ); for (int i = 0; i < numDocs1; i++) { @@ -49,7 +54,11 @@ public void testCollectWarnings() throws Exception { client().admin() .indices() .prepareCreate("index-2") - .setSettings(Settings.builder().put("index.routing.allocation.require._name", node2)) + .setSettings( + Settings.builder() + .put("index.routing.allocation.require._name", node2) + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, between(1, 5)) + ) .setMapping("host", "type=keyword") ); for (int i = 0; i < numDocs2; i++) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Avg.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Avg.java index 0ba834d1d8954..784d97f820428 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Avg.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Avg.java @@ -21,18 +21,24 @@ import java.util.List; import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.DEFAULT; -import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isNumeric; +import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isType; public class Avg extends AggregateFunction implements SurrogateExpression { @FunctionInfo(returnType = "double", description = "The average of a numeric field.", isAggregation = true) - public Avg(Source source, @Param(name = "field", type = { "double", "integer", "long", "unsigned_long" }) Expression field) { + public Avg(Source source, @Param(name = "field", type = { "double", "integer", "long" }) Expression field) { super(source, field); } @Override protected Expression.TypeResolution resolveType() { - return isNumeric(field(), sourceText(), DEFAULT); + return isType( + field(), + dt -> dt.isNumeric() && dt != DataTypes.UNSIGNED_LONG, + sourceText(), + DEFAULT, + "numeric except unsigned_long" + ); } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/CountDistinct.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/CountDistinct.java index 62dd3bc6b6254..4e52eecc5e80a 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/CountDistinct.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/CountDistinct.java @@ -32,6 +32,7 @@ import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isFoldable; import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isInteger; +import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isType; public class CountDistinct extends AggregateFunction implements OptionalArgument, ToAggregator { private static final int DEFAULT_PRECISION = 3000; @@ -42,19 +43,7 @@ public CountDistinct( Source source, @Param( name = "field", - type = { - "boolean", - "cartesian_point", - "date", - "double", - "geo_point", - "integer", - "ip", - "keyword", - "long", - "text", - "unsigned_long", - "version" }, + type = { "boolean", "cartesian_point", "date", "double", "geo_point", "integer", "ip", "keyword", "long", "text", "version" }, description = "Column or literal for which to count the number of distinct values." ) Expression field, @Param(optional = true, name = "precision", type = { "integer" }) Expression precision @@ -85,10 +74,21 @@ protected TypeResolution resolveType() { } TypeResolution resolution = EsqlTypeResolutions.isExact(field(), sourceText(), DEFAULT); - if (resolution.unresolved() || precision == null) { + if (resolution.unresolved()) { return resolution; } + boolean resolved = resolution.resolved(); + resolution = isType( + field(), + dt -> resolved && dt != DataTypes.UNSIGNED_LONG, + sourceText(), + DEFAULT, + "any exact type except unsigned_long" + ); + if (resolution.unresolved() || precision == null) { + return resolution; + } return isInteger(precision, sourceText(), SECOND).and(isFoldable(precision, sourceText(), SECOND)); } @@ -109,7 +109,7 @@ public AggregatorFunctionSupplier supplier(List inputChannels) { if (type == DataTypes.DOUBLE) { return new CountDistinctDoubleAggregatorFunctionSupplier(inputChannels, precision); } - if (type == DataTypes.KEYWORD || type == DataTypes.IP || type == DataTypes.TEXT) { + if (type == DataTypes.KEYWORD || type == DataTypes.IP || type == DataTypes.VERSION || type == DataTypes.TEXT) { return new CountDistinctBytesRefAggregatorFunctionSupplier(inputChannels, precision); } throw EsqlIllegalArgumentException.illegalDataType(type); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Max.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Max.java index cdcfe20c968a8..d8ec5300c061f 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Max.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Max.java @@ -22,12 +22,8 @@ public class Max extends NumericAggregate { - @FunctionInfo( - returnType = { "double", "integer", "long", "unsigned_long" }, - description = "The maximum value of a numeric field.", - isAggregation = true - ) - public Max(Source source, @Param(name = "field", type = { "double", "integer", "long", "unsigned_long" }) Expression field) { + @FunctionInfo(returnType = { "double", "integer", "long" }, description = "The maximum value of a numeric field.", isAggregation = true) + public Max(Source source, @Param(name = "field", type = { "double", "integer", "long" }) Expression field) { super(source, field); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Median.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Median.java index 7f5bce981db51..a6f4e30a62459 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Median.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Median.java @@ -22,22 +22,28 @@ import java.util.List; import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.DEFAULT; -import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isNumeric; +import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isType; public class Median extends AggregateFunction implements SurrogateExpression { // TODO: Add the compression parameter @FunctionInfo( - returnType = { "double", "integer", "long", "unsigned_long" }, + returnType = { "double", "integer", "long" }, description = "The value that is greater than half of all values and less than half of all values.", isAggregation = true ) - public Median(Source source, @Param(name = "field", type = { "double", "integer", "long", "unsigned_long" }) Expression field) { + public Median(Source source, @Param(name = "field", type = { "double", "integer", "long" }) Expression field) { super(source, field); } @Override protected Expression.TypeResolution resolveType() { - return isNumeric(field(), sourceText(), DEFAULT); + return isType( + field(), + dt -> dt.isNumeric() && dt != DataTypes.UNSIGNED_LONG, + sourceText(), + DEFAULT, + "numeric except unsigned_long" + ); } @Override diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MedianAbsoluteDeviation.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MedianAbsoluteDeviation.java index ddf0fd15fe2d0..ecf1a47ee9eb3 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MedianAbsoluteDeviation.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/MedianAbsoluteDeviation.java @@ -23,14 +23,11 @@ public class MedianAbsoluteDeviation extends NumericAggregate { // TODO: Add parameter @FunctionInfo( - returnType = { "double", "integer", "long", "unsigned_long" }, + returnType = { "double", "integer", "long" }, description = "The median absolute deviation, a measure of variability.", isAggregation = true ) - public MedianAbsoluteDeviation( - Source source, - @Param(name = "field", type = { "double", "integer", "long", "unsigned_long" }) Expression field - ) { + public MedianAbsoluteDeviation(Source source, @Param(name = "field", type = { "double", "integer", "long" }) Expression field) { super(source, field); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Min.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Min.java index 22da614675f9e..8fdce6d959b98 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Min.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Min.java @@ -22,12 +22,8 @@ public class Min extends NumericAggregate { - @FunctionInfo( - returnType = { "double", "integer", "long", "unsigned_long" }, - description = "The minimum value of a numeric field.", - isAggregation = true - ) - public Min(Source source, @Param(name = "field", type = { "double", "integer", "long", "unsigned_long" }) Expression field) { + @FunctionInfo(returnType = { "double", "integer", "long" }, description = "The minimum value of a numeric field.", isAggregation = true) + public Min(Source source, @Param(name = "field", type = { "double", "integer", "long" }) Expression field) { super(source, field); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/NumericAggregate.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/NumericAggregate.java index 297aeb7fc0e29..8e1e38441e9a6 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/NumericAggregate.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/NumericAggregate.java @@ -19,7 +19,7 @@ import java.util.List; import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.DEFAULT; -import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isNumeric; +import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isType; public abstract class NumericAggregate extends AggregateFunction implements ToAggregator { @@ -36,14 +36,20 @@ protected TypeResolution resolveType() { if (supportsDates()) { return TypeResolutions.isType( this, - e -> e.isNumeric() || e == DataTypes.DATETIME, + e -> e == DataTypes.DATETIME || e.isNumeric() && e != DataTypes.UNSIGNED_LONG, sourceText(), DEFAULT, - "numeric", - "datetime" + "datetime", + "numeric except unsigned_long" ); } - return isNumeric(field(), sourceText(), DEFAULT); + return isType( + field(), + dt -> dt.isNumeric() && dt != DataTypes.UNSIGNED_LONG, + sourceText(), + DEFAULT, + "numeric except unsigned_long" + ); } protected boolean supportsDates() { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Percentile.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Percentile.java index c34783f7352c3..96385d534edcd 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Percentile.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Percentile.java @@ -16,6 +16,7 @@ import org.elasticsearch.xpack.ql.expression.Expression; import org.elasticsearch.xpack.ql.tree.NodeInfo; import org.elasticsearch.xpack.ql.tree.Source; +import org.elasticsearch.xpack.ql.type.DataTypes; import java.util.List; @@ -23,18 +24,19 @@ import static org.elasticsearch.xpack.ql.expression.TypeResolutions.ParamOrdinal.SECOND; import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isFoldable; import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isNumeric; +import static org.elasticsearch.xpack.ql.expression.TypeResolutions.isType; public class Percentile extends NumericAggregate { private final Expression percentile; @FunctionInfo( - returnType = { "double", "integer", "long", "unsigned_long" }, + returnType = { "double", "integer", "long" }, description = "The value at which a certain percentage of observed values occur.", isAggregation = true ) public Percentile( Source source, - @Param(name = "field", type = { "double", "integer", "long", "unsigned_long" }) Expression field, + @Param(name = "field", type = { "double", "integer", "long" }) Expression field, @Param(name = "percentile", type = { "double", "integer", "long" }) Expression percentile ) { super(source, field, List.of(percentile)); @@ -61,7 +63,13 @@ protected TypeResolution resolveType() { return new TypeResolution("Unresolved children"); } - TypeResolution resolution = isNumeric(field(), sourceText(), FIRST); + TypeResolution resolution = isType( + field(), + dt -> dt.isNumeric() && dt != DataTypes.UNSIGNED_LONG, + sourceText(), + FIRST, + "numeric except unsigned_long" + ); if (resolution.unresolved()) { return resolution; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Sum.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Sum.java index 0acf18981a83d..d09762947a597 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Sum.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/aggregate/Sum.java @@ -29,7 +29,7 @@ public class Sum extends NumericAggregate { @FunctionInfo(returnType = "long", description = "The sum of a numeric field.", isAggregation = true) - public Sum(Source source, @Param(name = "field", type = { "double", "integer", "long", "unsigned_long" }) Expression field) { + public Sum(Source source, @Param(name = "field", type = { "double", "integer", "long" }) Expression field) { super(source, field); } 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 c375ef24da829..f5cee225b1b13 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 @@ -254,15 +254,18 @@ private static String dataTypeToString(DataType type, Class aggClass) { return "Long"; } else if (type.equals(DataTypes.DOUBLE)) { return "Double"; - } else if (type.equals(DataTypes.KEYWORD) || type.equals(DataTypes.IP) || type.equals(DataTypes.TEXT)) { - return "BytesRef"; - } else if (type.equals(GEO_POINT)) { - return "GeoPoint"; - } else if (type.equals(CARTESIAN_POINT)) { - return "CartesianPoint"; - } else { - throw new EsqlIllegalArgumentException("illegal agg type: " + type.typeName()); - } + } else if (type.equals(DataTypes.KEYWORD) + || type.equals(DataTypes.IP) + || type.equals(DataTypes.VERSION) + || type.equals(DataTypes.TEXT)) { + return "BytesRef"; + } else if (type.equals(GEO_POINT)) { + return "GeoPoint"; + } else if (type.equals(CARTESIAN_POINT)) { + return "CartesianPoint"; + } else { + throw new EsqlIllegalArgumentException("illegal agg type: " + type.typeName()); + } } private static Expression unwrapAlias(Expression expression) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlTranslatorHandler.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlTranslatorHandler.java index 98b1037c704f6..4dd61def0b2c3 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlTranslatorHandler.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/EsqlTranslatorHandler.java @@ -22,23 +22,39 @@ import org.elasticsearch.xpack.ql.expression.function.scalar.ScalarFunction; import org.elasticsearch.xpack.ql.expression.predicate.nulls.IsNotNull; import org.elasticsearch.xpack.ql.expression.predicate.nulls.IsNull; +import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.BinaryComparison; +import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.Equals; +import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.GreaterThan; +import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.GreaterThanOrEqual; +import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.LessThan; +import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.LessThanOrEqual; +import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NotEquals; +import org.elasticsearch.xpack.ql.expression.predicate.operator.comparison.NullEquals; import org.elasticsearch.xpack.ql.planner.ExpressionTranslator; import org.elasticsearch.xpack.ql.planner.ExpressionTranslators; import org.elasticsearch.xpack.ql.planner.QlTranslatorHandler; import org.elasticsearch.xpack.ql.planner.TranslatorHandler; +import org.elasticsearch.xpack.ql.querydsl.query.MatchAll; import org.elasticsearch.xpack.ql.querydsl.query.Query; import org.elasticsearch.xpack.ql.querydsl.query.TermQuery; import org.elasticsearch.xpack.ql.tree.Source; import org.elasticsearch.xpack.ql.type.DataType; +import org.elasticsearch.xpack.ql.type.DataTypes; import org.elasticsearch.xpack.ql.util.Check; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.List; import java.util.function.Supplier; +import static org.elasticsearch.xpack.ql.type.DataTypes.UNSIGNED_LONG; +import static org.elasticsearch.xpack.ql.util.NumericUtils.unsignedLongAsNumber; + public final class EsqlTranslatorHandler extends QlTranslatorHandler { public static final List> QUERY_TRANSLATORS = List.of( new EqualsIgnoreCaseTranslator(), + new TrivialBinaryComparisons(), new ExpressionTranslators.BinaryComparisons(), new ExpressionTranslators.Ranges(), new ExpressionTranslators.BinaryLogic(), @@ -124,4 +140,109 @@ static Query translate(InsensitiveEquals bc) { return new TermQuery(source, name, value.utf8ToString(), true); } } + + public static class TrivialBinaryComparisons extends ExpressionTranslator { + @Override + protected Query asQuery(BinaryComparison bc, TranslatorHandler handler) { + ExpressionTranslators.BinaryComparisons.checkBinaryComparison(bc); + Query translated = translate(bc); + return translated == null ? null : handler.wrapFunctionQuery(bc, bc.left(), () -> translated); + } + + private static Query translate(BinaryComparison bc) { + if ((bc.left() instanceof FieldAttribute) == false + || bc.left().dataType().isNumeric() == false + || bc.right().foldable() == false) { + return null; + } + Source source = bc.source(); + Object value = ExpressionTranslators.valueOf(bc.right()); + + // Comparisons with multi-values always return null in ESQL. + if (value instanceof List) { + return new MatchAll(source).negate(source); + } + + DataType valueType = bc.right().dataType(); + DataType attributeDataType = bc.left().dataType(); + if (valueType == UNSIGNED_LONG && value instanceof Long ul) { + value = unsignedLongAsNumber(ul); + } + Number num = (Number) value; + if (isInRange(attributeDataType, valueType, num)) { + return null; + } + + if (Double.isNaN(((Number) value).doubleValue())) { + return new MatchAll(source).negate(source); + } + + boolean matchAllOrNone; + if (bc instanceof GreaterThan || bc instanceof GreaterThanOrEqual) { + matchAllOrNone = (num.doubleValue() > 0) == false; + } else if (bc instanceof LessThan || bc instanceof LessThanOrEqual) { + matchAllOrNone = (num.doubleValue() > 0); + } else if (bc instanceof Equals || bc instanceof NullEquals) { + matchAllOrNone = false; + } else if (bc instanceof NotEquals) { + matchAllOrNone = true; + } else { + throw new QlIllegalArgumentException("Unknown binary comparison [{}]", bc); + } + + return matchAllOrNone ? new MatchAll(source) : new MatchAll(source).negate(source); + } + + private static final BigDecimal HALF_FLOAT_MAX = BigDecimal.valueOf(65504); + private static final BigDecimal UNSIGNED_LONG_MAX = BigDecimal.valueOf(2).pow(64).subtract(BigDecimal.ONE); + + private static boolean isInRange(DataType numericFieldDataType, DataType valueDataType, Number value) { + double doubleValue = value.doubleValue(); + if (Double.isNaN(doubleValue) || Double.isInfinite(doubleValue)) { + return false; + } + + BigDecimal decimalValue; + if (value instanceof BigInteger bigIntValue) { + // Unsigned longs may be represented as BigInteger. + decimalValue = new BigDecimal(bigIntValue); + } else { + decimalValue = valueDataType.isRational() ? BigDecimal.valueOf(doubleValue) : BigDecimal.valueOf(value.longValue()); + } + + // Determine min/max for dataType. Use BigDecimals as doubles will have rounding errors for long/ulong. + BigDecimal minValue; + BigDecimal maxValue; + if (numericFieldDataType == DataTypes.BYTE) { + minValue = BigDecimal.valueOf(Byte.MIN_VALUE); + maxValue = BigDecimal.valueOf(Byte.MAX_VALUE); + } else if (numericFieldDataType == DataTypes.SHORT) { + minValue = BigDecimal.valueOf(Short.MIN_VALUE); + maxValue = BigDecimal.valueOf(Short.MAX_VALUE); + } else if (numericFieldDataType == DataTypes.INTEGER) { + minValue = BigDecimal.valueOf(Integer.MIN_VALUE); + maxValue = BigDecimal.valueOf(Integer.MAX_VALUE); + } else if (numericFieldDataType == DataTypes.LONG) { + minValue = BigDecimal.valueOf(Long.MIN_VALUE); + maxValue = BigDecimal.valueOf(Long.MAX_VALUE); + } else if (numericFieldDataType == DataTypes.UNSIGNED_LONG) { + minValue = BigDecimal.ZERO; + maxValue = UNSIGNED_LONG_MAX; + } else if (numericFieldDataType == DataTypes.HALF_FLOAT) { + minValue = HALF_FLOAT_MAX.negate(); + maxValue = HALF_FLOAT_MAX; + } else if (numericFieldDataType == DataTypes.FLOAT) { + minValue = BigDecimal.valueOf(-Float.MAX_VALUE); + maxValue = BigDecimal.valueOf(Float.MAX_VALUE); + } else if (numericFieldDataType == DataTypes.DOUBLE || numericFieldDataType == DataTypes.SCALED_FLOAT) { + // Scaled floats are represented as doubles in ESQL. + minValue = BigDecimal.valueOf(-Double.MAX_VALUE); + maxValue = BigDecimal.valueOf(Double.MAX_VALUE); + } else { + throw new QlIllegalArgumentException("Data type [{}] unsupported for numeric range check", numericFieldDataType); + } + + return minValue.compareTo(decimalValue) <= 0 && maxValue.compareTo(decimalValue) >= 0; + } + } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java index ef9bd6a9103af..1e988e392590f 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java @@ -31,6 +31,7 @@ import org.elasticsearch.compute.operator.DriverTaskRunner; import org.elasticsearch.compute.operator.ResponseHeadersCollector; import org.elasticsearch.compute.operator.exchange.ExchangeService; +import org.elasticsearch.compute.operator.exchange.ExchangeSink; import org.elasticsearch.compute.operator.exchange.ExchangeSinkHandler; import org.elasticsearch.compute.operator.exchange.ExchangeSourceHandler; import org.elasticsearch.core.IOUtils; @@ -369,7 +370,7 @@ private ActionListener cancelOnFailure(CancellableTask task, AtomicBoolean } void runCompute(CancellableTask task, ComputeContext context, PhysicalPlan plan, ActionListener> listener) { - listener = ActionListener.runAfter(listener, () -> Releasables.close(context.searchContexts)); + listener = ActionListener.runBefore(listener, () -> Releasables.close(context.searchContexts)); List contexts = new ArrayList<>(context.searchContexts.size()); for (int i = 0; i < context.searchContexts.size(); i++) { SearchContext searchContext = context.searchContexts.get(i); @@ -457,6 +458,8 @@ private void acquireSearchContexts( aliasFilter, clusterAlias ); + // TODO: `searchService.createSearchContext` allows opening search contexts without limits, + // we need to limit the number of active search contexts here or in SearchService SearchContext context = searchService.createSearchContext(shardRequest, SearchService.NO_TIMEOUT); searchContexts.add(context); } @@ -576,46 +579,94 @@ void lookupDataNodes( // TODO: Use an internal action here public static final String DATA_ACTION_NAME = EsqlQueryAction.NAME + "/data"; - private class DataNodeRequestHandler implements TransportRequestHandler { - @Override - public void messageReceived(DataNodeRequest request, TransportChannel channel, Task task) { - final var parentTask = (CancellableTask) task; - final var sessionId = request.sessionId(); - final var exchangeSink = exchangeService.getSinkHandler(sessionId); + private class DataNodeRequestExecutor { + private final DataNodeRequest request; + private final CancellableTask parentTask; + private final ExchangeSinkHandler exchangeSink; + private final ActionListener listener; + private final List driverProfiles; + private final int maxConcurrentShards; + private final ExchangeSink blockingSink; // block until we have completed on all shards or the coordinator has enough data + + DataNodeRequestExecutor( + DataNodeRequest request, + CancellableTask parentTask, + ExchangeSinkHandler exchangeSink, + int maxConcurrentShards, + ActionListener listener + ) { + this.request = request; + this.parentTask = parentTask; + this.exchangeSink = exchangeSink; + this.listener = listener; + this.driverProfiles = request.configuration().profile() ? Collections.synchronizedList(new ArrayList<>()) : List.of(); + this.maxConcurrentShards = maxConcurrentShards; + this.blockingSink = exchangeSink.createExchangeSink(); + } + + void start() { parentTask.addListener( - () -> exchangeService.finishSinkHandler(sessionId, new TaskCancelledException(parentTask.getReasonCancelled())) + () -> exchangeService.finishSinkHandler(request.sessionId(), new TaskCancelledException(parentTask.getReasonCancelled())) ); - final ActionListener listener = new ChannelActionListener<>(channel); + runBatch(0); + } + + private void runBatch(int startBatchIndex) { final EsqlConfiguration configuration = request.configuration(); - String clusterAlias = request.clusterAlias(); - acquireSearchContexts( - clusterAlias, - request.shardIds(), - configuration, - request.aliasFilters(), - ActionListener.wrap(searchContexts -> { - assert ThreadPool.assertCurrentThreadPool(ESQL_THREAD_POOL_NAME); - var computeContext = new ComputeContext(sessionId, clusterAlias, searchContexts, configuration, null, exchangeSink); - runCompute(parentTask, computeContext, request.plan(), ActionListener.wrap(driverProfiles -> { - // don't return until all pages are fetched - exchangeSink.addCompletionListener( - ContextPreservingActionListener.wrapPreservingContext( - ActionListener.releaseAfter( - listener.map(nullValue -> new ComputeResponse(driverProfiles)), - () -> exchangeService.finishSinkHandler(sessionId, null) - ), - transportService.getThreadPool().getThreadContext() - ) - ); - }, e -> { - exchangeService.finishSinkHandler(sessionId, e); - listener.onFailure(e); - })); - }, e -> { - exchangeService.finishSinkHandler(sessionId, e); - listener.onFailure(e); - }) + final String clusterAlias = request.clusterAlias(); + final var sessionId = request.sessionId(); + final int endBatchIndex = Math.min(startBatchIndex + maxConcurrentShards, request.shardIds().size()); + List shardIds = request.shardIds().subList(startBatchIndex, endBatchIndex); + acquireSearchContexts(clusterAlias, shardIds, configuration, request.aliasFilters(), ActionListener.wrap(searchContexts -> { + assert ThreadPool.assertCurrentThreadPool(ESQL_THREAD_POOL_NAME, ESQL_WORKER_THREAD_POOL_NAME); + var computeContext = new ComputeContext(sessionId, clusterAlias, searchContexts, configuration, null, exchangeSink); + runCompute( + parentTask, + computeContext, + request.plan(), + ActionListener.wrap(profiles -> onBatchCompleted(endBatchIndex, profiles), this::onFailure) + ); + }, this::onFailure)); + } + + private void onBatchCompleted(int lastBatchIndex, List batchProfiles) { + if (request.configuration().profile()) { + driverProfiles.addAll(batchProfiles); + } + if (lastBatchIndex < request.shardIds().size() && exchangeSink.isFinished() == false) { + runBatch(lastBatchIndex); + } else { + blockingSink.finish(); + // don't return until all pages are fetched + exchangeSink.addCompletionListener( + ContextPreservingActionListener.wrapPreservingContext( + ActionListener.runBefore( + listener.map(nullValue -> new ComputeResponse(driverProfiles)), + () -> exchangeService.finishSinkHandler(request.sessionId(), null) + ), + transportService.getThreadPool().getThreadContext() + ) + ); + } + } + + private void onFailure(Exception e) { + exchangeService.finishSinkHandler(request.sessionId(), e); + listener.onFailure(e); + } + } + + private class DataNodeRequestHandler implements TransportRequestHandler { + @Override + public void messageReceived(DataNodeRequest request, TransportChannel channel, Task task) { + DataNodeRequestExecutor executor = new DataNodeRequestExecutor( + request, + (CancellableTask) task, + exchangeService.getSinkHandler(request.sessionId()), + request.configuration().pragmas().maxConcurrentShardsPerNode(), + new ChannelActionListener<>(channel) ); + executor.start(); } } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/QueryPragmas.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/QueryPragmas.java index 65a07c98af29a..2ceee9de9001e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/QueryPragmas.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/QueryPragmas.java @@ -53,6 +53,8 @@ public final class QueryPragmas implements Writeable { */ public static final Setting STATUS_INTERVAL = Setting.timeSetting("status_interval", Driver.DEFAULT_STATUS_INTERVAL); + public static final Setting MAX_CONCURRENT_SHARDS_PER_NODE = Setting.intSetting("max_concurrent_shards_per_node", 10, 1, 100); + public static final QueryPragmas EMPTY = new QueryPragmas(Settings.EMPTY); private final Settings settings; @@ -114,6 +116,14 @@ public int enrichMaxWorkers() { return ENRICH_MAX_WORKERS.get(settings); } + /** + * The maximum number of shards can be executed concurrently on a single node by this query. This is a safeguard to avoid + * opening and holding many shards (equivalent to many file descriptors) or having too many field infos created by a single query. + */ + public int maxConcurrentShardsPerNode() { + return MAX_CONCURRENT_SHARDS_PER_NODE.get(settings); + } + public boolean isEmpty() { return settings.isEmpty(); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/SearchStats.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/SearchStats.java index 1106ecc344db7..d57b42a7a511f 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/SearchStats.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/stats/SearchStats.java @@ -323,12 +323,12 @@ private static int countEntries(IndexReader indexReader, String field) { if (fieldInfo.getPointIndexDimensionCount() > 0) { PointValues points = reader.getPointValues(field); if (points != null) { - count += points.getDocCount(); + count += points.size(); } } else if (fieldInfo.getIndexOptions() != IndexOptions.NONE) { Terms terms = reader.terms(field); if (terms != null) { - count += terms.getDocCount(); + count += terms.getSumTotalTermFreq(); } } else { return -1; // no shortcut possible for fields that are not indexed 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 a1d5374773eb4..ee77ff93b7687 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 @@ -1554,6 +1554,48 @@ public void testUnresolvedMvExpand() { assertThat(e.getMessage(), containsString("Unknown column [bar]")); } + public void testUnsupportedTypesInStats() { + verifyUnsupported( + """ + row x = to_unsigned_long(\"10\") + | stats avg(x), count_distinct(x), max(x), median(x), median_absolute_deviation(x), min(x), percentile(x, 10), sum(x) + """, + "Found 8 problems\n" + + "line 2:12: argument of [avg(x)] must be [numeric except unsigned_long], found value [x] type [unsigned_long]\n" + + "line 2:20: argument of [count_distinct(x)] must be [any exact type except unsigned_long], " + + "found value [x] type [unsigned_long]\n" + + "line 2:39: argument of [max(x)] must be [datetime or numeric except unsigned_long], " + + "found value [max(x)] type [unsigned_long]\n" + + "line 2:47: argument of [median(x)] must be [numeric except unsigned_long], found value [x] type [unsigned_long]\n" + + "line 2:58: argument of [median_absolute_deviation(x)] must be [numeric except unsigned_long], " + + "found value [x] type [unsigned_long]\n" + + "line 2:88: argument of [min(x)] must be [datetime or numeric except unsigned_long], " + + "found value [min(x)] type [unsigned_long]\n" + + "line 2:96: first argument of [percentile(x, 10)] must be [numeric except unsigned_long], " + + "found value [x] type [unsigned_long]\n" + + "line 2:115: argument of [sum(x)] must be [numeric except unsigned_long], found value [x] type [unsigned_long]" + ); + + verifyUnsupported( + """ + row x = to_version("1.2") + | stats avg(x), max(x), median(x), median_absolute_deviation(x), min(x), percentile(x, 10), sum(x) + """, + "Found 7 problems\n" + + "line 2:10: argument of [avg(x)] must be [numeric except unsigned_long], found value [x] type [version]\n" + + "line 2:18: argument of [max(x)] must be [datetime or numeric except unsigned_long], " + + "found value [max(x)] type [version]\n" + + "line 2:26: argument of [median(x)] must be [numeric except unsigned_long], found value [x] type [version]\n" + + "line 2:37: argument of [median_absolute_deviation(x)] must be [numeric except unsigned_long], " + + "found value [x] type [version]\n" + + "line 2:67: argument of [min(x)] must be [datetime or numeric except unsigned_long], " + + "found value [min(x)] type [version]\n" + + "line 2:75: first argument of [percentile(x, 10)] must be [numeric except unsigned_long], " + + "found value [x] type [version]\n" + + "line 2:94: argument of [sum(x)] must be [numeric except unsigned_long], found value [x] type [version]" + ); + } + private void verifyUnsupported(String query, String errorMessage) { verifyUnsupported(query, errorMessage, "mapping-multi-field-variation.json"); } 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 4c8e58fceffde..632c6087cf880 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 @@ -74,7 +74,7 @@ public void testAggsExpressionsInStatsAggs() { error("from test | stats max(max(salary)) by first_name") ); assertEquals( - "1:25: argument of [avg(first_name)] must be [numeric], found value [first_name] type [keyword]", + "1:25: argument of [avg(first_name)] must be [numeric except unsigned_long], found value [first_name] type [keyword]", error("from test | stats count(avg(first_name)) by first_name") ); assertEquals( @@ -244,7 +244,7 @@ public void testUnsignedLongNegation() { public void testSumOnDate() { assertEquals( - "1:19: argument of [sum(hire_date)] must be [numeric], found value [hire_date] type [datetime]", + "1:19: argument of [sum(hire_date)] must be [numeric except unsigned_long], found value [hire_date] type [datetime]", error("from test | stats sum(hire_date)") ); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index 9a558daea6de6..7321799efd705 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -44,11 +44,9 @@ import org.elasticsearch.xpack.esql.session.EsqlConfiguration; import org.elasticsearch.xpack.esql.stats.Metrics; import org.elasticsearch.xpack.esql.stats.SearchStats; -import org.elasticsearch.xpack.esql.type.EsqlDataTypes; import org.elasticsearch.xpack.ql.expression.Alias; import org.elasticsearch.xpack.ql.expression.Expressions; import org.elasticsearch.xpack.ql.expression.ReferenceAttribute; -import org.elasticsearch.xpack.ql.expression.function.FunctionRegistry; import org.elasticsearch.xpack.ql.index.EsIndex; import org.elasticsearch.xpack.ql.index.IndexResolution; import org.elasticsearch.xpack.ql.tree.Source; @@ -67,6 +65,7 @@ import static org.elasticsearch.xpack.esql.plan.physical.AggregateExec.Mode.FINAL; import static org.elasticsearch.xpack.esql.plan.physical.EsStatsQueryExec.StatsType; 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; @@ -86,9 +85,8 @@ public class LocalPhysicalPlanOptimizerTests extends ESTestCase { private Analyzer analyzer; private LogicalPlanOptimizer logicalOptimizer; private PhysicalPlanOptimizer physicalPlanOptimizer; + private EsqlFunctionRegistry functionRegistry; private Mapper mapper; - private Map mapping; - private int allFieldRowSize; private final EsqlConfiguration config; private final SearchStats IS_SV_STATS = new TestSearchStats() { @@ -117,24 +115,9 @@ public LocalPhysicalPlanOptimizerTests(String name, EsqlConfiguration config) { @Before public void init() { parser = new EsqlParser(); - - mapping = loadMapping("mapping-basic.json"); - allFieldRowSize = mapping.values() - .stream() - .mapToInt( - f -> (EstimatesRowSize.estimateSize(EsqlDataTypes.widenSmallNumericTypes(f.getDataType())) + f.getProperties() - .values() - .stream() - // check one more level since the mapping contains TEXT fields with KEYWORD multi-fields - .mapToInt(x -> EstimatesRowSize.estimateSize(EsqlDataTypes.widenSmallNumericTypes(x.getDataType()))) - .sum()) - ) - .sum(); - EsIndex test = new EsIndex("test", mapping); - IndexResolution getIndexResult = IndexResolution.valid(test); logicalOptimizer = new LogicalPlanOptimizer(new LogicalOptimizerContext(EsqlTestUtils.TEST_CFG)); physicalPlanOptimizer = new PhysicalPlanOptimizer(new PhysicalOptimizerContext(config)); - FunctionRegistry functionRegistry = new EsqlFunctionRegistry(); + functionRegistry = new EsqlFunctionRegistry(); mapper = new Mapper(functionRegistry); EnrichResolution enrichResolution = new EnrichResolution(); enrichResolution.addResolvedPolicy( @@ -151,10 +134,15 @@ public void init() { ) ) ); - analyzer = new Analyzer( - new AnalyzerContext(config, functionRegistry, getIndexResult, enrichResolution), - new Verifier(new Metrics()) - ); + analyzer = makeAnalyzer("mapping-basic.json", enrichResolution); + } + + private Analyzer makeAnalyzer(String mappingFileName, EnrichResolution enrichResolution) { + var mapping = loadMapping(mappingFileName); + EsIndex test = new EsIndex("test", mapping); + IndexResolution getIndexResult = IndexResolution.valid(test); + + return new Analyzer(new AnalyzerContext(config, functionRegistry, getIndexResult, enrichResolution), new Verifier(new Metrics())); } /** @@ -427,6 +415,115 @@ public void testIsNullPushdownFilter() { assertThat(query.query().toString(), is(expected.toString())); } + private record OutOfRangeTestCase(String fieldName, String tooLow, String tooHigh) {}; + + public void testOutOfRangeFilterPushdown() { + var allTypeMappingAnalyzer = makeAnalyzer("mapping-all-types.json", new EnrichResolution()); + + String largerThanInteger = String.valueOf(randomLongBetween(Integer.MAX_VALUE + 1L, Long.MAX_VALUE)); + String smallerThanInteger = String.valueOf(randomLongBetween(Long.MIN_VALUE, Integer.MIN_VALUE - 1L)); + + // These values are already out of bounds for longs due to rounding errors. + double longLowerBoundExclusive = (double) Long.MIN_VALUE; + double longUpperBoundExclusive = (double) Long.MAX_VALUE; + String largerThanLong = String.valueOf(randomDoubleBetween(longUpperBoundExclusive, Double.MAX_VALUE, true)); + String smallerThanLong = String.valueOf(randomDoubleBetween(-Double.MAX_VALUE, longLowerBoundExclusive, true)); + + List cases = List.of( + new OutOfRangeTestCase("byte", smallerThanInteger, largerThanInteger), + new OutOfRangeTestCase("short", smallerThanInteger, largerThanInteger), + new OutOfRangeTestCase("integer", smallerThanInteger, largerThanInteger), + new OutOfRangeTestCase("long", smallerThanLong, largerThanLong), + // TODO: add unsigned_long https://github.com/elastic/elasticsearch/issues/102935 + // TODO: add half_float, float https://github.com/elastic/elasticsearch/issues/100130 + new OutOfRangeTestCase("double", "-1.0/0.0", "1.0/0.0"), + new OutOfRangeTestCase("scaled_float", "-1.0/0.0", "1.0/0.0") + ); + + final String LT = "<"; + final String LTE = "<="; + final String GT = ">"; + final String GTE = ">="; + final String EQ = "=="; + final String NEQ = "!="; + + for (OutOfRangeTestCase testCase : cases) { + List trueForSingleValuesPredicates = List.of( + LT + testCase.tooHigh, + LTE + testCase.tooHigh, + GT + testCase.tooLow, + GTE + testCase.tooLow, + NEQ + testCase.tooHigh, + NEQ + testCase.tooLow, + NEQ + "0.0/0.0" + ); + List alwaysFalsePredicates = List.of( + LT + testCase.tooLow, + LTE + testCase.tooLow, + GT + testCase.tooHigh, + GTE + testCase.tooHigh, + EQ + testCase.tooHigh, + EQ + testCase.tooLow, + LT + "0.0/0.0", + LTE + "0.0/0.0", + GT + "0.0/0.0", + GTE + "0.0/0.0", + EQ + "0.0/0.0" + ); + + for (String truePredicate : trueForSingleValuesPredicates) { + String comparison = testCase.fieldName + truePredicate; + var query = "from test | where " + comparison; + Source expectedSource = new Source(1, 18, comparison); + + EsQueryExec actualQueryExec = doTestOutOfRangeFilterPushdown(query, allTypeMappingAnalyzer); + + assertThat(actualQueryExec.query(), is(instanceOf(SingleValueQuery.Builder.class))); + var actualLuceneQuery = (SingleValueQuery.Builder) actualQueryExec.query(); + assertThat(actualLuceneQuery.field(), equalTo(testCase.fieldName)); + assertThat(actualLuceneQuery.source(), equalTo(expectedSource)); + + assertThat(actualLuceneQuery.next(), equalTo(QueryBuilders.matchAllQuery())); + } + + for (String falsePredicate : alwaysFalsePredicates) { + String comparison = testCase.fieldName + falsePredicate; + var query = "from test | where " + comparison; + Source expectedSource = new Source(1, 18, comparison); + + EsQueryExec actualQueryExec = doTestOutOfRangeFilterPushdown(query, allTypeMappingAnalyzer); + + assertThat(actualQueryExec.query(), is(instanceOf(SingleValueQuery.Builder.class))); + var actualLuceneQuery = (SingleValueQuery.Builder) actualQueryExec.query(); + assertThat(actualLuceneQuery.field(), equalTo(testCase.fieldName)); + assertThat(actualLuceneQuery.source(), equalTo(expectedSource)); + + var expectedInnerQuery = QueryBuilders.boolQuery().mustNot(QueryBuilders.matchAllQuery()); + assertThat(actualLuceneQuery.next(), equalTo(expectedInnerQuery)); + } + } + } + + /** + * Expects e.g. + * LimitExec[500[INTEGER]] + * \_ExchangeExec[[],false] + * \_ProjectExec[[!alias_integer, boolean{f}#190, byte{f}#191, constant_keyword-foo{f}#192, date{f}#193, double{f}#194, ...]] + * \_FieldExtractExec[!alias_integer, boolean{f}#190, byte{f}#191, consta..][] + * \_EsQueryExec[test], query[{"esql_single_value":{"field":"byte","next":{"match_all":{"boost":1.0}},...}}] + */ + private EsQueryExec doTestOutOfRangeFilterPushdown(String query, Analyzer analyzer) { + var plan = plan(query, EsqlTestUtils.TEST_SEARCH_STATS, analyzer); + + var limit = as(plan, LimitExec.class); + var exchange = as(limit.child(), ExchangeExec.class); + var project = as(exchange.child(), ProjectExec.class); + var fieldExtract = as(project.child(), FieldExtractExec.class); + var luceneQuery = as(fieldExtract.child(), EsQueryExec.class); + + return luceneQuery; + } + /** * Expects * LimitExec[500[INTEGER]] @@ -486,7 +583,11 @@ private PhysicalPlan plan(String query) { } private PhysicalPlan plan(String query, SearchStats stats) { - var physical = optimizedPlan(physicalPlan(query), stats); + return plan(query, stats, analyzer); + } + + private PhysicalPlan plan(String query, SearchStats stats, Analyzer analyzer) { + var physical = optimizedPlan(physicalPlan(query, analyzer), stats); return physical; } @@ -509,7 +610,7 @@ private PhysicalPlan optimizedPlan(PhysicalPlan plan, SearchStats searchStats) { return l; } - private PhysicalPlan physicalPlan(String query) { + private PhysicalPlan physicalPlan(String query, Analyzer analyzer) { var logical = logicalOptimizer.optimize(analyzer.analyze(parser.createStatement(query))); // System.out.println("Logical\n" + logical); var physical = mapper.map(logical); diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServiceExtension.java index eee6f68c20ff7..5ffb4b5df08cc 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServiceExtension.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServiceExtension.java @@ -16,6 +16,7 @@ import org.elasticsearch.inference.InferenceService; import org.elasticsearch.inference.InferenceServiceExtension; import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelConfigurations; import org.elasticsearch.inference.ModelSecrets; @@ -123,11 +124,11 @@ public void infer( Model model, List input, Map taskSettings, + InputType inputType, ActionListener listener ) { switch (model.getConfigurations().getTaskType()) { - case ANY -> listener.onResponse(makeResults(input)); - case SPARSE_EMBEDDING -> listener.onResponse(makeResults(input)); + case ANY, SPARSE_EMBEDDING -> listener.onResponse(makeResults(input)); default -> listener.onFailure( new ElasticsearchStatusException( TaskType.unsupportedTaskTypeErrorMsg(model.getConfigurations().getTaskType(), name()), diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/action/TransportInferenceAction.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/action/TransportInferenceAction.java index b9cc14977b87e..fb3974fc12e8b 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/action/TransportInferenceAction.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/action/TransportInferenceAction.java @@ -92,6 +92,7 @@ private void inferOnService( model, request.getInput(), request.getTaskSettings(), + request.getInputType(), listener.delegateFailureAndWrap((l, inferenceResults) -> l.onResponse(new InferenceAction.Response(inferenceResults))) ); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereActionCreator.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereActionCreator.java index 8c9d70f0a7323..0fb5ca9283fae 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereActionCreator.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereActionCreator.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.inference.external.action.cohere; +import org.elasticsearch.inference.InputType; import org.elasticsearch.xpack.inference.external.action.ExecutableAction; import org.elasticsearch.xpack.inference.external.http.sender.Sender; import org.elasticsearch.xpack.inference.services.ServiceComponents; @@ -28,8 +29,8 @@ public CohereActionCreator(Sender sender, ServiceComponents serviceComponents) { } @Override - public ExecutableAction create(CohereEmbeddingsModel model, Map taskSettings) { - var overriddenModel = model.overrideWith(taskSettings); + public ExecutableAction create(CohereEmbeddingsModel model, Map taskSettings, InputType inputType) { + var overriddenModel = CohereEmbeddingsModel.of(model, taskSettings, inputType); return new CohereEmbeddingsAction(sender, overriddenModel, serviceComponents); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereActionVisitor.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereActionVisitor.java index 1500d48e3c201..cc732e7ab8dc5 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereActionVisitor.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereActionVisitor.java @@ -7,11 +7,12 @@ package org.elasticsearch.xpack.inference.external.action.cohere; +import org.elasticsearch.inference.InputType; import org.elasticsearch.xpack.inference.external.action.ExecutableAction; import org.elasticsearch.xpack.inference.services.cohere.embeddings.CohereEmbeddingsModel; import java.util.Map; public interface CohereActionVisitor { - ExecutableAction create(CohereEmbeddingsModel model, Map taskSettings); + ExecutableAction create(CohereEmbeddingsModel model, Map taskSettings, InputType inputType); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/action/openai/OpenAiActionCreator.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/action/openai/OpenAiActionCreator.java index 6c423760d0b35..94583c634fb26 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/action/openai/OpenAiActionCreator.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/action/openai/OpenAiActionCreator.java @@ -29,7 +29,7 @@ public OpenAiActionCreator(Sender sender, ServiceComponents serviceComponents) { @Override public ExecutableAction create(OpenAiEmbeddingsModel model, Map taskSettings) { - var overriddenModel = model.overrideWith(taskSettings); + var overriddenModel = OpenAiEmbeddingsModel.of(model, taskSettings); return new OpenAiEmbeddingsAction(sender, overriddenModel, serviceComponents); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequest.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequest.java index 8cacbd0f16aaf..30427aaa35869 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequest.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequest.java @@ -62,6 +62,7 @@ public HttpRequest createHttpRequest() { httpPost.setHeader(HttpHeaders.CONTENT_TYPE, XContentType.JSON.mediaType()); httpPost.setHeader(createAuthBearerHeader(account.apiKey())); + httpPost.setHeader(CohereUtils.createRequestSourceHeader()); return new HttpRequest(httpPost, getInferenceEntityId()); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestEntity.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestEntity.java index a0b5444ee45e4..9e34af5ed6385 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestEntity.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestEntity.java @@ -20,6 +20,8 @@ import java.util.List; import java.util.Objects; +import static org.elasticsearch.xpack.inference.services.cohere.embeddings.CohereEmbeddingsTaskSettings.invalidInputTypeMessage; + public record CohereEmbeddingsRequestEntity( List input, CohereEmbeddingsTaskSettings taskSettings, @@ -29,14 +31,6 @@ public record CohereEmbeddingsRequestEntity( private static final String SEARCH_DOCUMENT = "search_document"; private static final String SEARCH_QUERY = "search_query"; - /** - * Maps the {@link InputType} to the expected value for cohere for the input_type field in the request using the enum's ordinal. - * The order of these entries is important and needs to match the order in the enum - */ - private static final String[] INPUT_TYPE_MAPPING = { SEARCH_DOCUMENT, SEARCH_QUERY }; - static { - assert INPUT_TYPE_MAPPING.length == InputType.values().length : "input type mapping was incorrectly defined"; - } private static final String TEXTS_FIELD = "texts"; @@ -56,23 +50,31 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(CohereServiceSettings.MODEL, model); } - if (taskSettings.inputType() != null) { - builder.field(INPUT_TYPE_FIELD, covertToString(taskSettings.inputType())); + if (taskSettings.getInputType() != null) { + builder.field(INPUT_TYPE_FIELD, covertToString(taskSettings.getInputType())); } if (embeddingType != null) { builder.field(EMBEDDING_TYPES_FIELD, List.of(embeddingType)); } - if (taskSettings.truncation() != null) { - builder.field(CohereServiceFields.TRUNCATE, taskSettings.truncation()); + if (taskSettings.getTruncation() != null) { + builder.field(CohereServiceFields.TRUNCATE, taskSettings.getTruncation()); } builder.endObject(); return builder; } - private static String covertToString(InputType inputType) { - return INPUT_TYPE_MAPPING[inputType.ordinal()]; + // default for testing + static String covertToString(InputType inputType) { + return switch (inputType) { + case INGEST -> SEARCH_DOCUMENT; + case SEARCH -> SEARCH_QUERY; + default -> { + assert false : invalidInputTypeMessage(inputType); + yield null; + } + }; } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereUtils.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereUtils.java index f8ccd91d4e3d2..e54328df1dbf7 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereUtils.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereUtils.java @@ -7,10 +7,19 @@ package org.elasticsearch.xpack.inference.external.request.cohere; +import org.apache.http.Header; +import org.apache.http.message.BasicHeader; + public class CohereUtils { public static final String HOST = "api.cohere.ai"; public static final String VERSION_1 = "v1"; public static final String EMBEDDINGS_PATH = "embed"; + public static final String REQUEST_SOURCE_HEADER = "Request-Source"; + public static final String ELASTIC_REQUEST_SOURCE = "unspecified:elasticsearch"; + + public static Header createRequestSourceHeader() { + return new BasicHeader(REQUEST_SOURCE_HEADER, ELASTIC_REQUEST_SOURCE); + } private CohereUtils() {} } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/SenderService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/SenderService.java index bb45e8fd684a6..0c40863b37db2 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/SenderService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/SenderService.java @@ -12,6 +12,7 @@ import org.elasticsearch.core.IOUtils; import org.elasticsearch.inference.InferenceService; import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.xpack.inference.external.http.sender.HttpRequestSenderFactory; import org.elasticsearch.xpack.inference.external.http.sender.Sender; @@ -41,16 +42,23 @@ protected ServiceComponents getServiceComponents() { } @Override - public void infer(Model model, List input, Map taskSettings, ActionListener listener) { + public void infer( + Model model, + List input, + Map taskSettings, + InputType inputType, + ActionListener listener + ) { init(); - doInfer(model, input, taskSettings, listener); + doInfer(model, input, taskSettings, inputType, listener); } protected abstract void doInfer( Model model, List input, Map taskSettings, + InputType inputType, ActionListener listener ); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/ServiceUtils.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/ServiceUtils.java index c218a0ff12c22..7637bd9740670 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/ServiceUtils.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/ServiceUtils.java @@ -11,10 +11,10 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.ValidationException; import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.core.CheckedFunction; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Strings; import org.elasticsearch.inference.InferenceService; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.TaskType; import org.elasticsearch.rest.RestStatus; @@ -24,7 +24,7 @@ import java.net.URI; import java.net.URISyntaxException; -import java.util.Arrays; +import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -110,7 +110,7 @@ public static String mustBeNonEmptyString(String settingName, String scope) { return Strings.format("[%s] Invalid value empty string. [%s] must be a non-empty string", scope, settingName); } - public static String invalidValue(String settingName, String scope, String invalidType, String... requiredTypes) { + public static String invalidValue(String settingName, String scope, String invalidType, String[] requiredTypes) { return Strings.format( "[%s] Invalid value [%s] received. [%s] must be one of [%s]", scope, @@ -221,12 +221,12 @@ public static String extractOptionalString( return optionalField; } - public static T extractOptionalEnum( + public static > E extractOptionalEnum( Map map, String settingName, String scope, - CheckedFunction converter, - T[] validTypes, + EnumConstructor constructor, + EnumSet validValues, ValidationException validationException ) { var enumString = extractOptionalString(map, settingName, scope, validationException); @@ -234,16 +234,34 @@ public static T extractOptionalEnum( return null; } - var validTypesAsStrings = Arrays.stream(validTypes).map(type -> type.toString().toLowerCase(Locale.ROOT)).toArray(String[]::new); + var validValuesAsStrings = validValues.stream().map(value -> value.toString().toLowerCase(Locale.ROOT)).toArray(String[]::new); try { - return converter.apply(enumString); + var createdEnum = constructor.apply(enumString); + validateEnumValue(createdEnum, validValues); + + return createdEnum; } catch (IllegalArgumentException e) { - validationException.addValidationError(invalidValue(settingName, scope, enumString, validTypesAsStrings)); + validationException.addValidationError(invalidValue(settingName, scope, enumString, validValuesAsStrings)); } return null; } + private static > void validateEnumValue(E enumValue, EnumSet validValues) { + if (validValues.contains(enumValue) == false) { + throw new IllegalArgumentException(Strings.format("Enum value [%s] is not one of the acceptable values", enumValue.toString())); + } + } + + /** + * Functional interface for creating an enum from a string. + * @param + */ + @FunctionalInterface + public interface EnumConstructor> { + E apply(String name) throws IllegalArgumentException; + } + public static String parsePersistedConfigErrorMsg(String inferenceEntityId, String serviceName) { return format( "Failed to parse stored model [%s] for [%s] service, please delete and add the service again", @@ -272,7 +290,7 @@ public static ElasticsearchStatusException createInvalidModelException(Model mod public static void getEmbeddingSize(Model model, InferenceService service, ActionListener listener) { assert model.getTaskType() == TaskType.TEXT_EMBEDDING; - service.infer(model, List.of(TEST_EMBEDDING_INPUT), Map.of(), listener.delegateFailureAndWrap((delegate, r) -> { + service.infer(model, List.of(TEST_EMBEDDING_INPUT), Map.of(), InputType.INGEST, listener.delegateFailureAndWrap((delegate, r) -> { if (r instanceof TextEmbedding embeddingResults) { try { delegate.onResponse(embeddingResults.getFirstEmbeddingSize()); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereModel.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereModel.java index 1b4843e441248..81a27e1e536f3 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereModel.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereModel.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.inference.services.cohere; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelConfigurations; import org.elasticsearch.inference.ModelSecrets; @@ -30,5 +31,5 @@ protected CohereModel(CohereModel model, ServiceSettings serviceSettings) { super(model, serviceSettings); } - public abstract ExecutableAction accept(CohereActionVisitor creator, Map taskSettings); + public abstract ExecutableAction accept(CohereActionVisitor creator, Map taskSettings, InputType inputType); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java index 8783f12852ec8..3f608c977f686 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java @@ -14,6 +14,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.core.Nullable; import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelConfigurations; import org.elasticsearch.inference.ModelSecrets; @@ -123,6 +124,7 @@ public void doInfer( Model model, List input, Map taskSettings, + InputType inputType, ActionListener listener ) { if (model instanceof CohereModel == false) { @@ -133,7 +135,7 @@ public void doInfer( CohereModel cohereModel = (CohereModel) model; var actionCreator = new CohereActionCreator(getSender(), getServiceComponents()); - var action = cohereModel.accept(actionCreator, taskSettings); + var action = cohereModel.accept(actionCreator, taskSettings, inputType); action.execute(input, listener); } @@ -174,6 +176,6 @@ private CohereEmbeddingsModel updateModelWithEmbeddingDetails(CohereEmbeddingsMo @Override public TransportVersion getMinimalSupportedVersion() { - return TransportVersions.ML_INFERENCE_COHERE_EMBEDDINGS_ADDED; + return TransportVersions.ML_INFERENCE_REQUEST_INPUT_TYPE_UNSPECIFIED_ADDED; } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsModel.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsModel.java index c92700e87cd96..a3afdc306b217 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsModel.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsModel.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.inference.services.cohere.embeddings; import org.elasticsearch.core.Nullable; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.ModelConfigurations; import org.elasticsearch.inference.ModelSecrets; import org.elasticsearch.inference.TaskType; @@ -19,6 +20,11 @@ import java.util.Map; public class CohereEmbeddingsModel extends CohereModel { + public static CohereEmbeddingsModel of(CohereEmbeddingsModel model, Map taskSettings, InputType inputType) { + var requestTaskSettings = CohereEmbeddingsTaskSettings.fromMap(taskSettings); + return new CohereEmbeddingsModel(model, CohereEmbeddingsTaskSettings.of(model.getTaskSettings(), requestTaskSettings, inputType)); + } + public CohereEmbeddingsModel( String modelId, TaskType taskType, @@ -73,16 +79,7 @@ public DefaultSecretSettings getSecretSettings() { } @Override - public ExecutableAction accept(CohereActionVisitor visitor, Map taskSettings) { - return visitor.create(this, taskSettings); - } - - public CohereEmbeddingsModel overrideWith(Map taskSettings) { - if (taskSettings == null || taskSettings.isEmpty()) { - return this; - } - - var requestTaskSettings = CohereEmbeddingsTaskSettings.fromMap(taskSettings); - return new CohereEmbeddingsModel(this, getTaskSettings().overrideWith(requestTaskSettings)); + public ExecutableAction accept(CohereActionVisitor visitor, Map taskSettings, InputType inputType) { + return visitor.create(this, taskSettings, inputType); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsServiceSettings.java index 5327bcbcf22dd..916e7fadcc8fb 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsServiceSettings.java @@ -19,6 +19,7 @@ import org.elasticsearch.xpack.inference.services.cohere.CohereServiceSettings; import java.io.IOException; +import java.util.EnumSet; import java.util.Map; import java.util.Objects; @@ -37,7 +38,7 @@ public static CohereEmbeddingsServiceSettings fromMap(Map map) { EMBEDDING_TYPE, ModelConfigurations.SERVICE_SETTINGS, CohereEmbeddingType::fromString, - CohereEmbeddingType.values(), + EnumSet.allOf(CohereEmbeddingType.class), validationException ); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsTaskSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsTaskSettings.java index 858efdb0d1ace..b294350580a2e 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsTaskSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsTaskSettings.java @@ -9,6 +9,7 @@ import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.ValidationException; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -20,7 +21,9 @@ import org.elasticsearch.xpack.inference.services.cohere.CohereTruncation; import java.io.IOException; +import java.util.EnumSet; import java.util.Map; +import java.util.Objects; import static org.elasticsearch.xpack.inference.services.ServiceUtils.extractOptionalEnum; import static org.elasticsearch.xpack.inference.services.cohere.CohereServiceFields.TRUNCATE; @@ -31,18 +34,16 @@ *

* See api docs for details. *

- * - * @param inputType Specifies the type of input you're giving to the model - * @param truncation Specifies how the API will handle inputs longer than the maximum token length */ -public record CohereEmbeddingsTaskSettings(@Nullable InputType inputType, @Nullable CohereTruncation truncation) implements TaskSettings { +public class CohereEmbeddingsTaskSettings implements TaskSettings { public static final String NAME = "cohere_embeddings_task_settings"; public static final CohereEmbeddingsTaskSettings EMPTY_SETTINGS = new CohereEmbeddingsTaskSettings(null, null); static final String INPUT_TYPE = "input_type"; + private static final EnumSet VALID_REQUEST_VALUES2 = EnumSet.of(InputType.INGEST, InputType.SEARCH); public static CohereEmbeddingsTaskSettings fromMap(Map map) { - if (map.isEmpty()) { + if (map == null || map.isEmpty()) { return EMPTY_SETTINGS; } @@ -53,7 +54,7 @@ public static CohereEmbeddingsTaskSettings fromMap(Map map) { INPUT_TYPE, ModelConfigurations.TASK_SETTINGS, InputType::fromString, - InputType.values(), + VALID_REQUEST_VALUES2, validationException ); CohereTruncation truncation = extractOptionalEnum( @@ -61,7 +62,7 @@ public static CohereEmbeddingsTaskSettings fromMap(Map map) { TRUNCATE, ModelConfigurations.TASK_SETTINGS, CohereTruncation::fromString, - CohereTruncation.values(), + EnumSet.allOf(CohereTruncation.class), validationException ); @@ -72,10 +73,73 @@ public static CohereEmbeddingsTaskSettings fromMap(Map map) { return new CohereEmbeddingsTaskSettings(inputType, truncation); } + /** + * Creates a new {@link CohereEmbeddingsTaskSettings} by preferring non-null fields from the provided parameters. + * For the input type, preference is given to requestInputType if it is not null and not UNSPECIFIED. + * Then preference is given to the requestTaskSettings and finally to originalSettings even if the value is null. + * + * Similarly, for the truncation field preference is given to requestTaskSettings if it is not null and then to + * originalSettings. + * @param originalSettings the settings stored as part of the inference entity configuration + * @param requestTaskSettings the settings passed in within the task_settings field of the request + * @param requestInputType the input type passed in the request parameters + * @return a constructed {@link CohereEmbeddingsTaskSettings} + */ + public static CohereEmbeddingsTaskSettings of( + CohereEmbeddingsTaskSettings originalSettings, + CohereEmbeddingsTaskSettings requestTaskSettings, + InputType requestInputType + ) { + var inputTypeToUse = getValidInputType(originalSettings, requestTaskSettings, requestInputType); + var truncationToUse = getValidTruncation(originalSettings, requestTaskSettings); + + return new CohereEmbeddingsTaskSettings(inputTypeToUse, truncationToUse); + } + + private static InputType getValidInputType( + CohereEmbeddingsTaskSettings originalSettings, + CohereEmbeddingsTaskSettings requestTaskSettings, + InputType requestInputType + ) { + InputType inputTypeToUse = originalSettings.inputType; + + if (VALID_REQUEST_VALUES2.contains(requestInputType)) { + inputTypeToUse = requestInputType; + } else if (requestTaskSettings.inputType != null) { + inputTypeToUse = requestTaskSettings.inputType; + } + + return inputTypeToUse; + } + + private static CohereTruncation getValidTruncation( + CohereEmbeddingsTaskSettings originalSettings, + CohereEmbeddingsTaskSettings requestTaskSettings + ) { + return requestTaskSettings.getTruncation() == null ? originalSettings.truncation : requestTaskSettings.getTruncation(); + } + + private final InputType inputType; + private final CohereTruncation truncation; + public CohereEmbeddingsTaskSettings(StreamInput in) throws IOException { this(in.readOptionalEnum(InputType.class), in.readOptionalEnum(CohereTruncation.class)); } + public CohereEmbeddingsTaskSettings(@Nullable InputType inputType, @Nullable CohereTruncation truncation) { + validateInputType(inputType); + this.inputType = inputType; + this.truncation = truncation; + } + + private static void validateInputType(InputType inputType) { + if (inputType == null) { + return; + } + + assert VALID_REQUEST_VALUES2.contains(inputType) : invalidInputTypeMessage(inputType); + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -90,6 +154,14 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws return builder; } + public InputType getInputType() { + return inputType; + } + + public CohereTruncation getTruncation() { + return truncation; + } + @Override public String getWriteableName() { return NAME; @@ -106,10 +178,20 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalEnum(truncation); } - public CohereEmbeddingsTaskSettings overrideWith(CohereEmbeddingsTaskSettings requestTaskSettings) { - var inputTypeToUse = requestTaskSettings.inputType() == null ? inputType : requestTaskSettings.inputType(); - var truncationToUse = requestTaskSettings.truncation() == null ? truncation : requestTaskSettings.truncation(); + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CohereEmbeddingsTaskSettings that = (CohereEmbeddingsTaskSettings) o; + return Objects.equals(inputType, that.inputType) && Objects.equals(truncation, that.truncation); + } - return new CohereEmbeddingsTaskSettings(inputTypeToUse, truncationToUse); + @Override + public int hashCode() { + return Objects.hash(inputType, truncation); + } + + public static String invalidInputTypeMessage(InputType inputType) { + return Strings.format("received invalid input type value [%s]", inputType.toString()); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elser/ElserMlNodeService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elser/ElserMlNodeService.java index 12bdcd3f20614..1d0bd123c69f3 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elser/ElserMlNodeService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elser/ElserMlNodeService.java @@ -19,6 +19,7 @@ import org.elasticsearch.inference.InferenceService; import org.elasticsearch.inference.InferenceServiceExtension; import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelConfigurations; import org.elasticsearch.inference.TaskType; @@ -210,7 +211,13 @@ public void stop(String inferenceEntityId, ActionListener listener) { } @Override - public void infer(Model model, List input, Map taskSettings, ActionListener listener) { + public void infer( + Model model, + List input, + Map taskSettings, + InputType inputType, + ActionListener listener + ) { // No task settings to override with requestTaskSettings if (TaskType.SPARSE_EMBEDDING.isAnyOrSame(model.getConfigurations().getTaskType()) == false) { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseService.java index ef93cdd57b756..dcaa760868c49 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseService.java @@ -10,6 +10,7 @@ import org.apache.lucene.util.SetOnce; import org.elasticsearch.action.ActionListener; import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelConfigurations; import org.elasticsearch.inference.ModelSecrets; @@ -96,6 +97,7 @@ public void doInfer( Model model, List input, Map taskSettings, + InputType inputType, ActionListener listener ) { if (model instanceof HuggingFaceModel == false) { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/OpenAiService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/OpenAiService.java index 9b5283ef4f803..594d7cf2cf31c 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/OpenAiService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/OpenAiService.java @@ -14,6 +14,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.core.Nullable; import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelConfigurations; import org.elasticsearch.inference.ModelSecrets; @@ -136,6 +137,7 @@ public void doInfer( Model model, List input, Map taskSettings, + InputType inputType, ActionListener listener ) { if (model instanceof OpenAiModel == false) { diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsModel.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsModel.java index 98b0161665d8e..74d97099bbb76 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsModel.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsModel.java @@ -21,6 +21,15 @@ public class OpenAiEmbeddingsModel extends OpenAiModel { + public static OpenAiEmbeddingsModel of(OpenAiEmbeddingsModel model, Map taskSettings) { + if (taskSettings == null || taskSettings.isEmpty()) { + return model; + } + + var requestTaskSettings = OpenAiEmbeddingsRequestTaskSettings.fromMap(taskSettings); + return new OpenAiEmbeddingsModel(model, OpenAiEmbeddingsTaskSettings.of(model.getTaskSettings(), requestTaskSettings)); + } + public OpenAiEmbeddingsModel( String inferenceEntityId, TaskType taskType, @@ -78,13 +87,4 @@ public DefaultSecretSettings getSecretSettings() { public ExecutableAction accept(OpenAiActionVisitor creator, Map taskSettings) { return creator.create(this, taskSettings); } - - public OpenAiEmbeddingsModel overrideWith(Map taskSettings) { - if (taskSettings == null || taskSettings.isEmpty()) { - return this; - } - - var requestTaskSettings = OpenAiEmbeddingsRequestTaskSettings.fromMap(taskSettings); - return new OpenAiEmbeddingsModel(this, getTaskSettings().overrideWith(requestTaskSettings)); - } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsTaskSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsTaskSettings.java index 45a9ce1cabbc3..c6f3179a4f088 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsTaskSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsTaskSettings.java @@ -50,6 +50,23 @@ public static OpenAiEmbeddingsTaskSettings fromMap(Map map) { return new OpenAiEmbeddingsTaskSettings(model, user); } + /** + * Creates a new {@link OpenAiEmbeddingsTaskSettings} object by overriding the values in originalSettings with the ones + * passed in via requestSettings if the fields are not null. + * @param originalSettings the original task settings from the inference entity configuration from storage + * @param requestSettings the task settings from the request + * @return a new {@link OpenAiEmbeddingsTaskSettings} + */ + public static OpenAiEmbeddingsTaskSettings of( + OpenAiEmbeddingsTaskSettings originalSettings, + OpenAiEmbeddingsRequestTaskSettings requestSettings + ) { + var modelToUse = requestSettings.model() == null ? originalSettings.model : requestSettings.model(); + var userToUse = requestSettings.user() == null ? originalSettings.user : requestSettings.user(); + + return new OpenAiEmbeddingsTaskSettings(modelToUse, userToUse); + } + public OpenAiEmbeddingsTaskSettings { Objects.requireNonNull(model); } @@ -84,11 +101,4 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(model); out.writeOptionalString(user); } - - public OpenAiEmbeddingsTaskSettings overrideWith(OpenAiEmbeddingsRequestTaskSettings requestSettings) { - var modelToUse = requestSettings.model() == null ? model : requestSettings.model(); - var userToUse = requestSettings.user() == null ? user : requestSettings.user(); - - return new OpenAiEmbeddingsTaskSettings(modelToUse, userToUse); - } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InputTypeTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InputTypeTests.java new file mode 100644 index 0000000000000..088f93507d35f --- /dev/null +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/InputTypeTests.java @@ -0,0 +1,21 @@ +/* + * 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.inference.InputType; +import org.elasticsearch.test.ESTestCase; + +public class InputTypeTests extends ESTestCase { + public static InputType randomWithoutUnspecified() { + return randomFrom(InputType.INGEST, InputType.SEARCH); + } + + public static InputType[] valuesWithoutUnspecified() { + return new InputType[] { InputType.INGEST, InputType.SEARCH }; + } +} diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/action/InferenceActionRequestTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/action/InferenceActionRequestTests.java index 4f7ae9436418f..396af55ce5616 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/action/InferenceActionRequestTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/action/InferenceActionRequestTests.java @@ -7,22 +7,26 @@ package org.elasticsearch.xpack.inference.action; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.core.Tuple; import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.TaskType; -import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xcontent.json.JsonXContent; import org.elasticsearch.xpack.core.inference.action.InferenceAction; +import org.elasticsearch.xpack.core.ml.AbstractBWCWireSerializationTestCase; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.hamcrest.Matchers.is; import static org.hamcrest.collection.IsIterableContainingInOrder.contains; -public class InferenceActionRequestTests extends AbstractWireSerializingTestCase { +public class InferenceActionRequestTests extends AbstractBWCWireSerializationTestCase { @Override protected Writeable.Reader instanceReader() { @@ -70,7 +74,7 @@ public void testParseRequest_DefaultsInputTypeToIngest() throws IOException { """; try (var parser = createParser(JsonXContent.jsonXContent, singleInputRequest)) { var request = InferenceAction.Request.parseRequest("model_id", "sparse_embedding", parser); - assertThat(request.getInputType(), is(InputType.INGEST)); + assertThat(request.getInputType(), is(InputType.UNSPECIFIED)); } } @@ -135,4 +139,76 @@ protected InferenceAction.Request mutateInstance(InferenceAction.Request instanc default -> throw new UnsupportedOperationException(); }; } + + @Override + protected InferenceAction.Request mutateInstanceForVersion(InferenceAction.Request instance, TransportVersion version) { + if (version.before(TransportVersions.INFERENCE_MULTIPLE_INPUTS)) { + return new InferenceAction.Request( + instance.getTaskType(), + instance.getInferenceEntityId(), + instance.getInput().subList(0, 1), + instance.getTaskSettings(), + InputType.UNSPECIFIED + ); + } else if (version.before(TransportVersions.ML_INFERENCE_REQUEST_INPUT_TYPE_ADDED)) { + return new InferenceAction.Request( + instance.getTaskType(), + instance.getInferenceEntityId(), + instance.getInput(), + instance.getTaskSettings(), + InputType.UNSPECIFIED + ); + } else if (version.before(TransportVersions.ML_INFERENCE_REQUEST_INPUT_TYPE_UNSPECIFIED_ADDED) + && instance.getInputType() == InputType.UNSPECIFIED) { + return new InferenceAction.Request( + instance.getTaskType(), + instance.getInferenceEntityId(), + instance.getInput(), + instance.getTaskSettings(), + InputType.INGEST + ); + } + + return instance; + } + + public void testWriteTo_WhenVersionIsOnAfterUnspecifiedAdded() throws IOException { + assertBwcSerialization( + new InferenceAction.Request(TaskType.TEXT_EMBEDDING, "model", List.of(), Map.of(), InputType.UNSPECIFIED), + TransportVersions.ML_INFERENCE_REQUEST_INPUT_TYPE_UNSPECIFIED_ADDED + ); + } + + public void testWriteTo_WhenVersionIsBeforeUnspecifiedAdded_ButAfterInputTypeAdded_ShouldSetToIngest() throws IOException { + assertBwcSerialization( + new InferenceAction.Request(TaskType.TEXT_EMBEDDING, "model", List.of(), Map.of(), InputType.UNSPECIFIED), + TransportVersions.ML_INFERENCE_REQUEST_INPUT_TYPE_ADDED + ); + } + + public void testWriteTo_WhenVersionIsBeforeUnspecifiedAdded_ButAfterInputTypeAdded_ShouldSetToIngest_ManualCheck() throws IOException { + var instance = new InferenceAction.Request(TaskType.TEXT_EMBEDDING, "model", List.of(), Map.of(), InputType.UNSPECIFIED); + + InferenceAction.Request deserializedInstance = copyWriteable( + instance, + getNamedWriteableRegistry(), + instanceReader(), + TransportVersions.ML_INFERENCE_REQUEST_INPUT_TYPE_ADDED + ); + + assertThat(deserializedInstance.getInputType(), is(InputType.INGEST)); + } + + public void testWriteTo_WhenVersionIsBeforeInputTypeAdded_ShouldSetInputTypeToUnspecified() throws IOException { + var instance = new InferenceAction.Request(TaskType.TEXT_EMBEDDING, "model", List.of(), Map.of(), InputType.INGEST); + + InferenceAction.Request deserializedInstance = copyWriteable( + instance, + getNamedWriteableRegistry(), + instanceReader(), + TransportVersions.HOT_THREADS_AS_BYTES + ); + + assertThat(deserializedInstance.getInputType(), is(InputType.UNSPECIFIED)); + } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereActionCreatorTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereActionCreatorTests.java index 67a95265f093d..e7cfc784db117 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereActionCreatorTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereActionCreatorTests.java @@ -110,7 +110,7 @@ public void testCreate_CohereEmbeddingsModel() throws IOException { ); var actionCreator = new CohereActionCreator(sender, createWithEmptySettings(threadPool)); var overriddenTaskSettings = CohereEmbeddingsTaskSettingsTests.getTaskSettingsMap(InputType.SEARCH, CohereTruncation.END); - var action = actionCreator.create(model, overriddenTaskSettings); + var action = actionCreator.create(model, overriddenTaskSettings, InputType.UNSPECIFIED); PlainActionFuture listener = new PlainActionFuture<>(); action.execute(List.of("abc"), listener); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereEmbeddingsActionTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereEmbeddingsActionTests.java index 501d5a5e42bfe..7fd33f7bba58f 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereEmbeddingsActionTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/action/cohere/CohereEmbeddingsActionTests.java @@ -25,6 +25,7 @@ import org.elasticsearch.xpack.inference.external.http.HttpResult; import org.elasticsearch.xpack.inference.external.http.sender.HttpRequestSenderFactory; import org.elasticsearch.xpack.inference.external.http.sender.Sender; +import org.elasticsearch.xpack.inference.external.request.cohere.CohereUtils; import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import org.elasticsearch.xpack.inference.results.TextEmbeddingByteResultsTests; import org.elasticsearch.xpack.inference.services.cohere.CohereTruncation; @@ -130,6 +131,10 @@ public void testExecute_ReturnsSuccessfulResponse() throws IOException { equalTo(XContentType.JSON.mediaType()) ); MatcherAssert.assertThat(webServer.requests().get(0).getHeader(HttpHeaders.AUTHORIZATION), equalTo("Bearer secret")); + MatcherAssert.assertThat( + webServer.requests().get(0).getHeader(CohereUtils.REQUEST_SOURCE_HEADER), + equalTo(CohereUtils.ELASTIC_REQUEST_SOURCE) + ); var requestMap = entityAsMap(webServer.requests().get(0).getBody()); MatcherAssert.assertThat( @@ -210,6 +215,10 @@ public void testExecute_ReturnsSuccessfulResponse_ForInt8ResponseType() throws I equalTo(XContentType.JSON.mediaType()) ); MatcherAssert.assertThat(webServer.requests().get(0).getHeader(HttpHeaders.AUTHORIZATION), equalTo("Bearer secret")); + MatcherAssert.assertThat( + webServer.requests().get(0).getHeader(CohereUtils.REQUEST_SOURCE_HEADER), + equalTo(CohereUtils.ELASTIC_REQUEST_SOURCE) + ); var requestMap = entityAsMap(webServer.requests().get(0).getBody()); MatcherAssert.assertThat( diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestEntityTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestEntityTests.java index 8ef9ea4b0316b..2d3ff25222ab9 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestEntityTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestEntityTests.java @@ -66,4 +66,9 @@ public void testXContent_WritesNoOptionalFields_WhenTheyAreNotDefined() throws I MatcherAssert.assertThat(xContentResult, is(""" {"texts":["abc"]}""")); } + + public void testConvertToString_ThrowsAssertionFailure_WhenInputTypeIsUnspecified() { + var thrownException = expectThrows(AssertionError.class, () -> CohereEmbeddingsRequestEntity.covertToString(InputType.UNSPECIFIED)); + MatcherAssert.assertThat(thrownException.getMessage(), is("received invalid input type value [unspecified]")); + } } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestTests.java index df61417ffff9c..d3783f6fed76b 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/request/cohere/CohereEmbeddingsRequestTests.java @@ -44,6 +44,10 @@ public void testCreateRequest_UrlDefined() throws URISyntaxException, IOExceptio MatcherAssert.assertThat(httpPost.getURI().toString(), is("url")); MatcherAssert.assertThat(httpPost.getLastHeader(HttpHeaders.CONTENT_TYPE).getValue(), is(XContentType.JSON.mediaType())); MatcherAssert.assertThat(httpPost.getLastHeader(HttpHeaders.AUTHORIZATION).getValue(), is("Bearer secret")); + MatcherAssert.assertThat( + httpPost.getLastHeader(CohereUtils.REQUEST_SOURCE_HEADER).getValue(), + is(CohereUtils.ELASTIC_REQUEST_SOURCE) + ); var requestMap = entityAsMap(httpPost.getEntity().getContent()); MatcherAssert.assertThat(requestMap, is(Map.of("texts", List.of("abc")))); @@ -71,6 +75,10 @@ public void testCreateRequest_AllOptionsDefined() throws URISyntaxException, IOE MatcherAssert.assertThat(httpPost.getURI().toString(), is("url")); MatcherAssert.assertThat(httpPost.getLastHeader(HttpHeaders.CONTENT_TYPE).getValue(), is(XContentType.JSON.mediaType())); MatcherAssert.assertThat(httpPost.getLastHeader(HttpHeaders.AUTHORIZATION).getValue(), is("Bearer secret")); + MatcherAssert.assertThat( + httpPost.getLastHeader(CohereUtils.REQUEST_SOURCE_HEADER).getValue(), + is(CohereUtils.ELASTIC_REQUEST_SOURCE) + ); var requestMap = entityAsMap(httpPost.getEntity().getContent()); MatcherAssert.assertThat( @@ -114,6 +122,10 @@ public void testCreateRequest_InputTypeSearch_EmbeddingTypeInt8_TruncateEnd() th MatcherAssert.assertThat(httpPost.getURI().toString(), is("url")); MatcherAssert.assertThat(httpPost.getLastHeader(HttpHeaders.CONTENT_TYPE).getValue(), is(XContentType.JSON.mediaType())); MatcherAssert.assertThat(httpPost.getLastHeader(HttpHeaders.AUTHORIZATION).getValue(), is("Bearer secret")); + MatcherAssert.assertThat( + httpPost.getLastHeader(CohereUtils.REQUEST_SOURCE_HEADER).getValue(), + is(CohereUtils.ELASTIC_REQUEST_SOURCE) + ); var requestMap = entityAsMap(httpPost.getEntity().getContent()); MatcherAssert.assertThat( @@ -157,6 +169,10 @@ public void testCreateRequest_TruncateNone() throws URISyntaxException, IOExcept MatcherAssert.assertThat(httpPost.getURI().toString(), is("url")); MatcherAssert.assertThat(httpPost.getLastHeader(HttpHeaders.CONTENT_TYPE).getValue(), is(XContentType.JSON.mediaType())); MatcherAssert.assertThat(httpPost.getLastHeader(HttpHeaders.AUTHORIZATION).getValue(), is("Bearer secret")); + MatcherAssert.assertThat( + httpPost.getLastHeader(CohereUtils.REQUEST_SOURCE_HEADER).getValue(), + is(CohereUtils.ELASTIC_REQUEST_SOURCE) + ); var requestMap = entityAsMap(httpPost.getEntity().getContent()); MatcherAssert.assertThat(requestMap, is(Map.of("texts", List.of("abc"), "truncate", "none"))); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/SenderServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/SenderServiceTests.java index 31d7667fa6665..8b596aa5cf0c8 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/SenderServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/SenderServiceTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.core.TimeValue; import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.TaskType; import org.elasticsearch.test.ESTestCase; @@ -105,6 +106,7 @@ protected void doInfer( Model model, List input, Map taskSettings, + InputType inputType, ActionListener listener ) { diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/ServiceUtilsTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/ServiceUtilsTests.java index b935c5a8c64b3..689c9f9b08a2b 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/ServiceUtilsTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/ServiceUtilsTests.java @@ -23,6 +23,7 @@ import org.elasticsearch.xpack.inference.results.TextEmbeddingByteResultsTests; import org.elasticsearch.xpack.inference.results.TextEmbeddingResultsTests; +import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -261,7 +262,7 @@ public void testExtractOptionalString_AddsException_WhenFieldIsEmpty() { public void testExtractOptionalEnum_ReturnsNull_WhenFieldDoesNotExist() { var validation = new ValidationException(); Map map = modifiableMap(Map.of("key", "value")); - var createdEnum = extractOptionalEnum(map, "abc", "scope", InputType::fromString, InputType.values(), validation); + var createdEnum = extractOptionalEnum(map, "abc", "scope", InputType::fromString, EnumSet.allOf(InputType.class), validation); assertNull(createdEnum); assertTrue(validation.validationErrors().isEmpty()); @@ -271,7 +272,14 @@ public void testExtractOptionalEnum_ReturnsNull_WhenFieldDoesNotExist() { public void testExtractOptionalEnum_ReturnsNullAndAddsException_WhenAnInvalidValueExists() { var validation = new ValidationException(); Map map = modifiableMap(Map.of("key", "invalid_value")); - var createdEnum = extractOptionalEnum(map, "key", "scope", InputType::fromString, InputType.values(), validation); + var createdEnum = extractOptionalEnum( + map, + "key", + "scope", + InputType::fromString, + EnumSet.of(InputType.INGEST, InputType.SEARCH), + validation + ); assertNull(createdEnum); assertFalse(validation.validationErrors().isEmpty()); @@ -282,6 +290,27 @@ public void testExtractOptionalEnum_ReturnsNullAndAddsException_WhenAnInvalidVal ); } + public void testExtractOptionalEnum_ReturnsNullAndAddsException_WhenValueIsNotPartOfTheAcceptableValues() { + var validation = new ValidationException(); + Map map = modifiableMap(Map.of("key", InputType.UNSPECIFIED.toString())); + var createdEnum = extractOptionalEnum(map, "key", "scope", InputType::fromString, EnumSet.of(InputType.INGEST), validation); + + assertNull(createdEnum); + assertFalse(validation.validationErrors().isEmpty()); + assertTrue(map.isEmpty()); + assertThat(validation.validationErrors().get(0), is("[scope] Invalid value [unspecified] received. [key] must be one of [ingest]")); + } + + public void testExtractOptionalEnum_ReturnsIngest_WhenValueIsAcceptable() { + var validation = new ValidationException(); + Map map = modifiableMap(Map.of("key", InputType.INGEST.toString())); + var createdEnum = extractOptionalEnum(map, "key", "scope", InputType::fromString, EnumSet.of(InputType.INGEST), validation); + + assertThat(createdEnum, is(InputType.INGEST)); + assertTrue(validation.validationErrors().isEmpty()); + assertTrue(map.isEmpty()); + } + public void testGetEmbeddingSize_ReturnsError_WhenTextEmbeddingResults_IsEmpty() { var service = mock(InferenceService.class); @@ -290,11 +319,11 @@ public void testGetEmbeddingSize_ReturnsError_WhenTextEmbeddingResults_IsEmpty() doAnswer(invocation -> { @SuppressWarnings("unchecked") - ActionListener listener = (ActionListener) invocation.getArguments()[3]; + ActionListener listener = (ActionListener) invocation.getArguments()[4]; listener.onResponse(new TextEmbeddingResults(List.of())); return Void.TYPE; - }).when(service).infer(any(), any(), any(), any()); + }).when(service).infer(any(), any(), any(), any(), any()); PlainActionFuture listener = new PlainActionFuture<>(); getEmbeddingSize(model, service, listener); @@ -313,11 +342,11 @@ public void testGetEmbeddingSize_ReturnsError_WhenTextEmbeddingByteResults_IsEmp doAnswer(invocation -> { @SuppressWarnings("unchecked") - ActionListener listener = (ActionListener) invocation.getArguments()[3]; + ActionListener listener = (ActionListener) invocation.getArguments()[4]; listener.onResponse(new TextEmbeddingByteResults(List.of())); return Void.TYPE; - }).when(service).infer(any(), any(), any(), any()); + }).when(service).infer(any(), any(), any(), any(), any()); PlainActionFuture listener = new PlainActionFuture<>(); getEmbeddingSize(model, service, listener); @@ -338,11 +367,11 @@ public void testGetEmbeddingSize_ReturnsSize_ForTextEmbeddingResults() { doAnswer(invocation -> { @SuppressWarnings("unchecked") - ActionListener listener = (ActionListener) invocation.getArguments()[3]; + ActionListener listener = (ActionListener) invocation.getArguments()[4]; listener.onResponse(textEmbedding); return Void.TYPE; - }).when(service).infer(any(), any(), any(), any()); + }).when(service).infer(any(), any(), any(), any(), any()); PlainActionFuture listener = new PlainActionFuture<>(); getEmbeddingSize(model, service, listener); @@ -362,11 +391,11 @@ public void testGetEmbeddingSize_ReturnsSize_ForTextEmbeddingByteResults() { doAnswer(invocation -> { @SuppressWarnings("unchecked") - ActionListener listener = (ActionListener) invocation.getArguments()[3]; + ActionListener listener = (ActionListener) invocation.getArguments()[4]; listener.onResponse(textEmbedding); return Void.TYPE; - }).when(service).infer(any(), any(), any(), any()); + }).when(service).infer(any(), any(), any(), any(), any()); PlainActionFuture listener = new PlainActionFuture<>(); getEmbeddingSize(model, service, listener); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceTests.java index 0250e08a48452..7daad207f9068 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceTests.java @@ -34,6 +34,7 @@ import org.elasticsearch.xpack.inference.services.cohere.embeddings.CohereEmbeddingsModelTests; import org.elasticsearch.xpack.inference.services.cohere.embeddings.CohereEmbeddingsServiceSettingsTests; import org.elasticsearch.xpack.inference.services.cohere.embeddings.CohereEmbeddingsTaskSettings; +import org.elasticsearch.xpack.inference.services.cohere.embeddings.CohereEmbeddingsTaskSettingsTests; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.After; @@ -686,7 +687,7 @@ public void testInfer_ThrowsErrorWhenModelIsNotCohereModel() throws IOException try (var service = new CohereService(new SetOnce<>(factory), new SetOnce<>(createWithEmptySettings(threadPool)))) { PlainActionFuture listener = new PlainActionFuture<>(); - service.infer(mockModel, List.of(""), new HashMap<>(), listener); + service.infer(mockModel, List.of(""), new HashMap<>(), InputType.INGEST, listener); var thrownException = expectThrows(ElasticsearchStatusException.class, () -> listener.actionGet(TIMEOUT)); MatcherAssert.assertThat( @@ -745,7 +746,7 @@ public void testInfer_SendsRequest() throws IOException { null ); PlainActionFuture listener = new PlainActionFuture<>(); - service.infer(model, List.of("abc"), new HashMap<>(), listener); + service.infer(model, List.of("abc"), new HashMap<>(), InputType.INGEST, listener); var result = listener.actionGet(TIMEOUT); @@ -848,7 +849,7 @@ public void testInfer_UnauthorisedResponse() throws IOException { null ); PlainActionFuture listener = new PlainActionFuture<>(); - service.infer(model, List.of("abc"), new HashMap<>(), listener); + service.infer(model, List.of("abc"), new HashMap<>(), InputType.INGEST, listener); var error = expectThrows(ElasticsearchException.class, () -> listener.actionGet(TIMEOUT)); MatcherAssert.assertThat(error.getMessage(), containsString("Received an authentication error status code for request")); @@ -857,6 +858,193 @@ public void testInfer_UnauthorisedResponse() throws IOException { } } + public void testInfer_SetsInputTypeToIngest_FromInferParameter_WhenTaskSettingsAreEmpty() throws IOException { + var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); + + try (var service = new CohereService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + + String responseJson = """ + { + "id": "de37399c-5df6-47cb-bc57-e3c5680c977b", + "texts": [ + "hello" + ], + "embeddings": { + "float": [ + [ + 0.123, + -0.123 + ] + ] + }, + "meta": { + "api_version": { + "version": "1" + }, + "billed_units": { + "input_tokens": 1 + } + }, + "response_type": "embeddings_by_type" + } + """; + webServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseJson)); + + var model = CohereEmbeddingsModelTests.createModel( + getUrl(webServer), + "secret", + CohereEmbeddingsTaskSettings.EMPTY_SETTINGS, + 1024, + 1024, + "model", + null + ); + PlainActionFuture listener = new PlainActionFuture<>(); + service.infer(model, List.of("abc"), new HashMap<>(), InputType.INGEST, listener); + + var result = listener.actionGet(TIMEOUT); + + MatcherAssert.assertThat(result.asMap(), Matchers.is(buildExpectation(List.of(List.of(0.123F, -0.123F))))); + MatcherAssert.assertThat(webServer.requests(), hasSize(1)); + assertNull(webServer.requests().get(0).getUri().getQuery()); + MatcherAssert.assertThat( + webServer.requests().get(0).getHeader(HttpHeaders.CONTENT_TYPE), + equalTo(XContentType.JSON.mediaType()) + ); + MatcherAssert.assertThat(webServer.requests().get(0).getHeader(HttpHeaders.AUTHORIZATION), equalTo("Bearer secret")); + + var requestMap = entityAsMap(webServer.requests().get(0).getBody()); + MatcherAssert.assertThat(requestMap, is(Map.of("texts", List.of("abc"), "model", "model", "input_type", "search_document"))); + } + } + + public void testInfer_SetsInputTypeToIngestFromInferParameter_WhenModelSettingIsNull_AndRequestTaskSettingsIsSearch() + throws IOException { + var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); + + try (var service = new CohereService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + + String responseJson = """ + { + "id": "de37399c-5df6-47cb-bc57-e3c5680c977b", + "texts": [ + "hello" + ], + "embeddings": { + "float": [ + [ + 0.123, + -0.123 + ] + ] + }, + "meta": { + "api_version": { + "version": "1" + }, + "billed_units": { + "input_tokens": 1 + } + }, + "response_type": "embeddings_by_type" + } + """; + webServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseJson)); + + var model = CohereEmbeddingsModelTests.createModel( + getUrl(webServer), + "secret", + new CohereEmbeddingsTaskSettings(null, null), + 1024, + 1024, + "model", + null + ); + PlainActionFuture listener = new PlainActionFuture<>(); + service.infer( + model, + List.of("abc"), + CohereEmbeddingsTaskSettingsTests.getTaskSettingsMap(InputType.SEARCH, null), + InputType.INGEST, + listener + ); + + var result = listener.actionGet(TIMEOUT); + + MatcherAssert.assertThat(result.asMap(), Matchers.is(buildExpectation(List.of(List.of(0.123F, -0.123F))))); + MatcherAssert.assertThat(webServer.requests(), hasSize(1)); + assertNull(webServer.requests().get(0).getUri().getQuery()); + MatcherAssert.assertThat( + webServer.requests().get(0).getHeader(HttpHeaders.CONTENT_TYPE), + equalTo(XContentType.JSON.mediaType()) + ); + MatcherAssert.assertThat(webServer.requests().get(0).getHeader(HttpHeaders.AUTHORIZATION), equalTo("Bearer secret")); + + var requestMap = entityAsMap(webServer.requests().get(0).getBody()); + MatcherAssert.assertThat(requestMap, is(Map.of("texts", List.of("abc"), "model", "model", "input_type", "search_document"))); + } + } + + public void testInfer_DoesNotSetInputType_WhenNotPresentInTaskSettings_AndUnspecifiedIsPassedInRequest() throws IOException { + var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); + + try (var service = new CohereService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + + String responseJson = """ + { + "id": "de37399c-5df6-47cb-bc57-e3c5680c977b", + "texts": [ + "hello" + ], + "embeddings": { + "float": [ + [ + 0.123, + -0.123 + ] + ] + }, + "meta": { + "api_version": { + "version": "1" + }, + "billed_units": { + "input_tokens": 1 + } + }, + "response_type": "embeddings_by_type" + } + """; + webServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseJson)); + + var model = CohereEmbeddingsModelTests.createModel( + getUrl(webServer), + "secret", + new CohereEmbeddingsTaskSettings(null, null), + 1024, + 1024, + "model", + null + ); + PlainActionFuture listener = new PlainActionFuture<>(); + service.infer(model, List.of("abc"), new HashMap<>(), InputType.UNSPECIFIED, listener); + + var result = listener.actionGet(TIMEOUT); + + MatcherAssert.assertThat(result.asMap(), Matchers.is(buildExpectation(List.of(List.of(0.123F, -0.123F))))); + MatcherAssert.assertThat(webServer.requests(), hasSize(1)); + assertNull(webServer.requests().get(0).getUri().getQuery()); + MatcherAssert.assertThat( + webServer.requests().get(0).getHeader(HttpHeaders.CONTENT_TYPE), + equalTo(XContentType.JSON.mediaType()) + ); + MatcherAssert.assertThat(webServer.requests().get(0).getHeader(HttpHeaders.AUTHORIZATION), equalTo("Bearer secret")); + + var requestMap = entityAsMap(webServer.requests().get(0).getBody()); + MatcherAssert.assertThat(requestMap, is(Map.of("texts", List.of("abc"), "model", "model"))); + } + } + private Map getRequestConfigMap( Map serviceSettings, Map taskSettings, diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsModelTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsModelTests.java index 1961d6b168d54..5570731dbe8d9 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsModelTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsModelTests.java @@ -21,12 +21,36 @@ import static org.elasticsearch.xpack.inference.services.cohere.embeddings.CohereEmbeddingsTaskSettingsTests.getTaskSettingsMap; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.sameInstance; public class CohereEmbeddingsModelTests extends ESTestCase { - public void testOverrideWith_OverridesInputType_WithSearch() { + public void testOverrideWith_DoesNotOverrideAndModelRemainsEqual_WhenSettingsAreEmpty_AndInputTypeIsInvalid() { + var model = createModel("url", "api_key", null, null, null); + + var overriddenModel = CohereEmbeddingsModel.of(model, Map.of(), InputType.UNSPECIFIED); + MatcherAssert.assertThat(overriddenModel, is(model)); + } + + public void testOverrideWith_DoesNotOverrideAndModelRemainsEqual_WhenSettingsAreNull_AndInputTypeIsInvalid() { + var model = createModel("url", "api_key", null, null, null); + + var overriddenModel = CohereEmbeddingsModel.of(model, null, InputType.UNSPECIFIED); + MatcherAssert.assertThat(overriddenModel, is(model)); + } + + public void testOverrideWith_SetsInputTypeToIngest_WhenTheFieldIsNullInModelTaskSettings_AndNullInRequestTaskSettings() { var model = createModel( + "url", + "api_key", + new CohereEmbeddingsTaskSettings(null, null), + null, + null, + "model", + CohereEmbeddingType.FLOAT + ); + + var overriddenModel = CohereEmbeddingsModel.of(model, getTaskSettingsMap(null, null), InputType.INGEST); + var expectedModel = createModel( "url", "api_key", new CohereEmbeddingsTaskSettings(InputType.INGEST, null), @@ -35,8 +59,21 @@ public void testOverrideWith_OverridesInputType_WithSearch() { "model", CohereEmbeddingType.FLOAT ); + MatcherAssert.assertThat(overriddenModel, is(expectedModel)); + } - var overriddenModel = model.overrideWith(getTaskSettingsMap(InputType.SEARCH, null)); + public void testOverrideWith_SetsInputType_FromRequest_IfValid_OverridingStoredTaskSettings() { + var model = createModel( + "url", + "api_key", + new CohereEmbeddingsTaskSettings(InputType.INGEST, null), + null, + null, + "model", + CohereEmbeddingType.FLOAT + ); + + var overriddenModel = CohereEmbeddingsModel.of(model, getTaskSettingsMap(null, null), InputType.SEARCH); var expectedModel = createModel( "url", "api_key", @@ -49,18 +86,100 @@ public void testOverrideWith_OverridesInputType_WithSearch() { MatcherAssert.assertThat(overriddenModel, is(expectedModel)); } - public void testOverrideWith_DoesNotOverride_WhenSettingsAreEmpty() { - var model = createModel("url", "api_key", null, null, null); + public void testOverrideWith_SetsInputType_FromRequest_IfValid_OverridingRequestTaskSettings() { + var model = createModel( + "url", + "api_key", + new CohereEmbeddingsTaskSettings(null, null), + null, + null, + "model", + CohereEmbeddingType.FLOAT + ); - var overriddenModel = model.overrideWith(Map.of()); - MatcherAssert.assertThat(overriddenModel, sameInstance(model)); + var overriddenModel = CohereEmbeddingsModel.of(model, getTaskSettingsMap(InputType.INGEST, null), InputType.SEARCH); + var expectedModel = createModel( + "url", + "api_key", + new CohereEmbeddingsTaskSettings(InputType.SEARCH, null), + null, + null, + "model", + CohereEmbeddingType.FLOAT + ); + MatcherAssert.assertThat(overriddenModel, is(expectedModel)); } - public void testOverrideWith_DoesNotOverride_WhenSettingsAreNull() { - var model = createModel("url", "api_key", null, null, null); + public void testOverrideWith_OverridesInputType_WithRequestTaskSettingsSearch_WhenRequestInputTypeIsInvalid() { + var model = createModel( + "url", + "api_key", + new CohereEmbeddingsTaskSettings(InputType.INGEST, null), + null, + null, + "model", + CohereEmbeddingType.FLOAT + ); + + var overriddenModel = CohereEmbeddingsModel.of(model, getTaskSettingsMap(InputType.SEARCH, null), InputType.UNSPECIFIED); + var expectedModel = createModel( + "url", + "api_key", + new CohereEmbeddingsTaskSettings(InputType.SEARCH, null), + null, + null, + "model", + CohereEmbeddingType.FLOAT + ); + MatcherAssert.assertThat(overriddenModel, is(expectedModel)); + } + + public void testOverrideWith_DoesNotSetInputType_FromRequest_IfInputTypeIsInvalid() { + var model = createModel( + "url", + "api_key", + new CohereEmbeddingsTaskSettings(null, null), + null, + null, + "model", + CohereEmbeddingType.FLOAT + ); - var overriddenModel = model.overrideWith(null); - MatcherAssert.assertThat(overriddenModel, sameInstance(model)); + var overriddenModel = CohereEmbeddingsModel.of(model, getTaskSettingsMap(null, null), InputType.UNSPECIFIED); + var expectedModel = createModel( + "url", + "api_key", + new CohereEmbeddingsTaskSettings(null, null), + null, + null, + "model", + CohereEmbeddingType.FLOAT + ); + MatcherAssert.assertThat(overriddenModel, is(expectedModel)); + } + + public void testOverrideWith_DoesNotSetInputType_WhenRequestTaskSettingsIsNull_AndRequestInputTypeIsInvalid() { + var model = createModel( + "url", + "api_key", + new CohereEmbeddingsTaskSettings(InputType.INGEST, null), + null, + null, + "model", + CohereEmbeddingType.FLOAT + ); + + var overriddenModel = CohereEmbeddingsModel.of(model, getTaskSettingsMap(null, null), InputType.UNSPECIFIED); + var expectedModel = createModel( + "url", + "api_key", + new CohereEmbeddingsTaskSettings(InputType.INGEST, null), + null, + null, + "model", + CohereEmbeddingType.FLOAT + ); + MatcherAssert.assertThat(overriddenModel, is(expectedModel)); } public static CohereEmbeddingsModel createModel( diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsTaskSettingsTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsTaskSettingsTests.java index 164d3998f138f..77e3280d18f93 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsTaskSettingsTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/embeddings/CohereEmbeddingsTaskSettingsTests.java @@ -13,20 +13,21 @@ import org.elasticsearch.inference.InputType; import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.xpack.inference.services.cohere.CohereServiceFields; -import org.elasticsearch.xpack.inference.services.cohere.CohereServiceSettings; import org.elasticsearch.xpack.inference.services.cohere.CohereTruncation; +import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import static org.elasticsearch.xpack.inference.InputTypeTests.randomWithoutUnspecified; import static org.hamcrest.Matchers.is; public class CohereEmbeddingsTaskSettingsTests extends AbstractWireSerializingTestCase { public static CohereEmbeddingsTaskSettings createRandom() { - var inputType = randomBoolean() ? randomFrom(InputType.values()) : null; + var inputType = randomBoolean() ? randomWithoutUnspecified() : null; var truncation = randomBoolean() ? randomFrom(CohereTruncation.values()) : null; return new CohereEmbeddingsTaskSettings(inputType, truncation); @@ -39,6 +40,10 @@ public void testFromMap_CreatesEmptySettings_WhenAllFieldsAreNull() { ); } + public void testFromMap_CreatesEmptySettings_WhenMapIsNull() { + MatcherAssert.assertThat(CohereEmbeddingsTaskSettings.fromMap(null), is(new CohereEmbeddingsTaskSettings(null, null))); + } + public void testFromMap_CreatesSettings_WhenAllFieldsOfSettingsArePresent() { MatcherAssert.assertThat( CohereEmbeddingsTaskSettings.fromMap( @@ -67,26 +72,55 @@ public void testFromMap_ReturnsFailure_WhenInputTypeIsInvalid() { ); } - public void testOverrideWith_KeepsOriginalValuesWhenOverridesAreNull() { - var taskSettings = CohereEmbeddingsTaskSettings.fromMap( - new HashMap<>(Map.of(CohereServiceSettings.MODEL, "model", CohereServiceFields.TRUNCATE, CohereTruncation.END.toString())) + public void testFromMap_ReturnsFailure_WhenInputTypeIsUnspecified() { + var exception = expectThrows( + ValidationException.class, + () -> CohereEmbeddingsTaskSettings.fromMap( + new HashMap<>(Map.of(CohereEmbeddingsTaskSettings.INPUT_TYPE, InputType.UNSPECIFIED.toString())) + ) + ); + + MatcherAssert.assertThat( + exception.getMessage(), + is("Validation Failed: 1: [task_settings] Invalid value [unspecified] received. [input_type] must be one of [ingest, search];") ); + } + + public void testXContent_ThrowsAssertionFailure_WhenInputTypeIsUnspecified() { + var thrownException = expectThrows(AssertionError.class, () -> new CohereEmbeddingsTaskSettings(InputType.UNSPECIFIED, null)); + MatcherAssert.assertThat(thrownException.getMessage(), CoreMatchers.is("received invalid input type value [unspecified]")); + } - var overriddenTaskSettings = taskSettings.overrideWith(CohereEmbeddingsTaskSettings.EMPTY_SETTINGS); + public void testOf_KeepsOriginalValuesWhenRequestSettingsAreNull_AndRequestInputTypeIsInvalid() { + var taskSettings = new CohereEmbeddingsTaskSettings(InputType.INGEST, CohereTruncation.NONE); + var overriddenTaskSettings = CohereEmbeddingsTaskSettings.of( + taskSettings, + CohereEmbeddingsTaskSettings.EMPTY_SETTINGS, + InputType.UNSPECIFIED + ); MatcherAssert.assertThat(overriddenTaskSettings, is(taskSettings)); } - public void testOverrideWith_UsesOverriddenSettings() { - var taskSettings = CohereEmbeddingsTaskSettings.fromMap( - new HashMap<>(Map.of(CohereServiceFields.TRUNCATE, CohereTruncation.END.toString())) + public void testOf_UsesRequestTaskSettings() { + var taskSettings = new CohereEmbeddingsTaskSettings(null, CohereTruncation.NONE); + var overriddenTaskSettings = CohereEmbeddingsTaskSettings.of( + taskSettings, + new CohereEmbeddingsTaskSettings(InputType.INGEST, CohereTruncation.END), + InputType.UNSPECIFIED ); - var requestTaskSettings = CohereEmbeddingsTaskSettings.fromMap( - new HashMap<>(Map.of(CohereServiceFields.TRUNCATE, CohereTruncation.START.toString())) + MatcherAssert.assertThat(overriddenTaskSettings, is(new CohereEmbeddingsTaskSettings(InputType.INGEST, CohereTruncation.END))); + } + + public void testOf_UsesRequestTaskSettings_AndRequestInputType() { + var taskSettings = new CohereEmbeddingsTaskSettings(InputType.SEARCH, CohereTruncation.NONE); + var overriddenTaskSettings = CohereEmbeddingsTaskSettings.of( + taskSettings, + new CohereEmbeddingsTaskSettings(null, CohereTruncation.END), + InputType.INGEST ); - var overriddenTaskSettings = taskSettings.overrideWith(requestTaskSettings); - MatcherAssert.assertThat(overriddenTaskSettings, is(new CohereEmbeddingsTaskSettings(null, CohereTruncation.START))); + MatcherAssert.assertThat(overriddenTaskSettings, is(new CohereEmbeddingsTaskSettings(InputType.INGEST, CohereTruncation.END))); } @Override diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseServiceTests.java index e9fb835016b4f..dcf8b3a900a22 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseServiceTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.core.TimeValue; import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.TaskType; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; @@ -64,7 +65,7 @@ public void testInfer_ThrowsErrorWhenModelIsNotHuggingFaceModel() throws IOExcep try (var service = new TestService(new SetOnce<>(factory), new SetOnce<>(createWithEmptySettings(threadPool)))) { PlainActionFuture listener = new PlainActionFuture<>(); - service.infer(mockModel, List.of(""), new HashMap<>(), listener); + service.infer(mockModel, List.of(""), new HashMap<>(), InputType.INGEST, listener); var thrownException = expectThrows(ElasticsearchStatusException.class, () -> listener.actionGet(TIMEOUT)); assertThat( diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceServiceTests.java index a76cce41b4fe4..36a4d144d8c5c 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceServiceTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelConfigurations; import org.elasticsearch.inference.ModelSecrets; @@ -492,7 +493,7 @@ public void testInfer_SendsEmbeddingsRequest() throws IOException { var model = HuggingFaceEmbeddingsModelTests.createModel(getUrl(webServer), "secret"); PlainActionFuture listener = new PlainActionFuture<>(); - service.infer(model, List.of("abc"), new HashMap<>(), listener); + service.infer(model, List.of("abc"), new HashMap<>(), InputType.INGEST, listener); var result = listener.actionGet(TIMEOUT); @@ -527,7 +528,7 @@ public void testInfer_SendsElserRequest() throws IOException { var model = HuggingFaceElserModelTests.createModel(getUrl(webServer), "secret"); PlainActionFuture listener = new PlainActionFuture<>(); - service.infer(model, List.of("abc"), new HashMap<>(), listener); + service.infer(model, List.of("abc"), new HashMap<>(), InputType.INGEST, listener); var result = listener.actionGet(TIMEOUT); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/OpenAiServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/OpenAiServiceTests.java index 394286ee5287b..2659715771686 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/OpenAiServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/OpenAiServiceTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelConfigurations; import org.elasticsearch.inference.ModelSecrets; @@ -667,7 +668,7 @@ public void testInfer_ThrowsErrorWhenModelIsNotOpenAiModel() throws IOException try (var service = new OpenAiService(new SetOnce<>(factory), new SetOnce<>(createWithEmptySettings(threadPool)))) { PlainActionFuture listener = new PlainActionFuture<>(); - service.infer(mockModel, List.of(""), new HashMap<>(), listener); + service.infer(mockModel, List.of(""), new HashMap<>(), InputType.INGEST, listener); var thrownException = expectThrows(ElasticsearchStatusException.class, () -> listener.actionGet(TIMEOUT)); assertThat( @@ -713,7 +714,7 @@ public void testInfer_SendsRequest() throws IOException { var model = OpenAiEmbeddingsModelTests.createModel(getUrl(webServer), "org", "secret", "model", "user"); PlainActionFuture listener = new PlainActionFuture<>(); - service.infer(model, List.of("abc"), new HashMap<>(), listener); + service.infer(model, List.of("abc"), new HashMap<>(), InputType.INGEST, listener); var result = listener.actionGet(TIMEOUT); @@ -787,7 +788,7 @@ public void testInfer_UnauthorisedResponse() throws IOException { var model = OpenAiEmbeddingsModelTests.createModel(getUrl(webServer), "org", "secret", "model", "user"); PlainActionFuture listener = new PlainActionFuture<>(); - service.infer(model, List.of("abc"), new HashMap<>(), listener); + service.infer(model, List.of("abc"), new HashMap<>(), InputType.INGEST, listener); var error = expectThrows(ElasticsearchException.class, () -> listener.actionGet(TIMEOUT)); assertThat(error.getMessage(), containsString("Received an authentication error status code for request")); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsModelTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsModelTests.java index 10e856ec8a27e..e2144132af6c1 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsModelTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsModelTests.java @@ -27,7 +27,7 @@ public void testOverrideWith_OverridesUser() { var model = createModel("url", "org", "api_key", "model_name", null); var requestTaskSettingsMap = getRequestTaskSettingsMap(null, "user_override"); - var overriddenModel = model.overrideWith(requestTaskSettingsMap); + var overriddenModel = OpenAiEmbeddingsModel.of(model, requestTaskSettingsMap); assertThat(overriddenModel, is(createModel("url", "org", "api_key", "model_name", "user_override"))); } @@ -37,14 +37,14 @@ public void testOverrideWith_EmptyMap() { var requestTaskSettingsMap = Map.of(); - var overriddenModel = model.overrideWith(requestTaskSettingsMap); + var overriddenModel = OpenAiEmbeddingsModel.of(model, requestTaskSettingsMap); assertThat(overriddenModel, sameInstance(model)); } public void testOverrideWith_NullMap() { var model = createModel("url", "org", "api_key", "model_name", null); - var overriddenModel = model.overrideWith(null); + var overriddenModel = OpenAiEmbeddingsModel.of(model, null); assertThat(overriddenModel, sameInstance(model)); } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsTaskSettingsTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsTaskSettingsTests.java index f297eb622c421..103fab071098e 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsTaskSettingsTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/embeddings/OpenAiEmbeddingsTaskSettingsTests.java @@ -72,7 +72,7 @@ public void testOverrideWith_KeepsOriginalValuesWithOverridesAreNull() { new HashMap<>(Map.of(OpenAiEmbeddingsTaskSettings.MODEL, "model", OpenAiEmbeddingsTaskSettings.USER, "user")) ); - var overriddenTaskSettings = taskSettings.overrideWith(OpenAiEmbeddingsRequestTaskSettings.EMPTY_SETTINGS); + var overriddenTaskSettings = OpenAiEmbeddingsTaskSettings.of(taskSettings, OpenAiEmbeddingsRequestTaskSettings.EMPTY_SETTINGS); MatcherAssert.assertThat(overriddenTaskSettings, is(taskSettings)); } @@ -85,7 +85,7 @@ public void testOverrideWith_UsesOverriddenSettings() { new HashMap<>(Map.of(OpenAiEmbeddingsTaskSettings.MODEL, "model2", OpenAiEmbeddingsTaskSettings.USER, "user2")) ); - var overriddenTaskSettings = taskSettings.overrideWith(requestTaskSettings); + var overriddenTaskSettings = OpenAiEmbeddingsTaskSettings.of(taskSettings, requestTaskSettings); MatcherAssert.assertThat(overriddenTaskSettings, is(new OpenAiEmbeddingsTaskSettings("model2", "user2"))); } @@ -98,7 +98,7 @@ public void testOverrideWith_UsesOnlyNonNullModelSetting() { new HashMap<>(Map.of(OpenAiEmbeddingsTaskSettings.MODEL, "model2")) ); - var overriddenTaskSettings = taskSettings.overrideWith(requestTaskSettings); + var overriddenTaskSettings = OpenAiEmbeddingsTaskSettings.of(taskSettings, requestTaskSettings); MatcherAssert.assertThat(overriddenTaskSettings, is(new OpenAiEmbeddingsTaskSettings("model2", "user"))); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetOverallBucketsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetOverallBucketsAction.java index 38c7f85b189f2..b37f82e45ec49 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetOverallBucketsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportGetOverallBucketsAction.java @@ -19,7 +19,7 @@ import org.elasticsearch.core.TimeValue; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; import org.elasticsearch.search.aggregations.metrics.Max; @@ -191,7 +191,7 @@ private void initChunkedBucketSearcher( ActionListener.wrap(searchResponse -> { long totalHits = searchResponse.getHits().getTotalHits().value; if (totalHits > 0) { - Aggregations aggregations = searchResponse.getAggregations(); + InternalAggregations aggregations = searchResponse.getAggregations(); Min min = aggregations.get(EARLIEST_TIME); long earliestTime = Intervals.alignToFloor((long) min.value(), maxBucketSpanMillis); Max max = aggregations.get(LATEST_TIME); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/MlAggsHelper.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/MlAggsHelper.java index 780841880a6c1..88c19fc670794 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/MlAggsHelper.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/MlAggsHelper.java @@ -8,7 +8,7 @@ package org.elasticsearch.xpack.ml.aggs; import org.elasticsearch.search.aggregations.Aggregation; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; import org.elasticsearch.search.aggregations.InvalidAggregationPathException; import org.elasticsearch.search.aggregations.pipeline.BucketHelpers; @@ -34,7 +34,7 @@ public static InvalidAggregationPathException invalidPathException(List * @param aggregations The aggregations * @return The double values and doc_counts extracted from the path if the bucket path exists and the value is a valid number */ - public static Optional extractDoubleBucketedValues(String bucketPath, Aggregations aggregations) { + public static Optional extractDoubleBucketedValues(String bucketPath, InternalAggregations aggregations) { return extractDoubleBucketedValues(bucketPath, aggregations, BucketHelpers.GapPolicy.INSERT_ZEROS, false); } @@ -50,7 +50,7 @@ public static Optional extractDoubleBucketedValues(String bu */ public static Optional extractDoubleBucketedValues( String bucketPath, - Aggregations aggregations, + InternalAggregations aggregations, BucketHelpers.GapPolicy gapPolicy, boolean excludeLastBucket ) { @@ -101,7 +101,7 @@ public static Optional extractDoubleBucketedValues( public static Optional extractBucket( String bucketPath, - Aggregations aggregations, + InternalAggregations aggregations, int bucket ) { List parsedPath = AggregationPath.parse(bucketPath).getPathElementsAsStringList(); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/changepoint/ChangePointAggregator.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/changepoint/ChangePointAggregator.java index 48c0f645b6fbc..650c02af00837 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/changepoint/ChangePointAggregator.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/changepoint/ChangePointAggregator.java @@ -17,8 +17,8 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.search.aggregations.AggregationReduceContext; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.pipeline.BucketHelpers; import org.elasticsearch.search.aggregations.pipeline.SiblingPipelineAggregator; import org.elasticsearch.xpack.ml.aggs.MlAggsHelper; @@ -92,7 +92,7 @@ public ChangePointAggregator(String name, String bucketsPath, Map maybeBucketsValue = extractDoubleBucketedValues( bucketsPaths()[0], aggregations, diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/correlation/BucketCorrelationAggregator.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/correlation/BucketCorrelationAggregator.java index 02386acbd6134..97e803b5961a7 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/correlation/BucketCorrelationAggregator.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/correlation/BucketCorrelationAggregator.java @@ -9,8 +9,8 @@ import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.AggregationReduceContext; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.pipeline.InternalSimpleValue; import org.elasticsearch.search.aggregations.pipeline.SiblingPipelineAggregator; import org.elasticsearch.xpack.ml.aggs.MlAggsHelper; @@ -33,7 +33,7 @@ public BucketCorrelationAggregator( } @Override - public InternalAggregation doReduce(Aggregations aggregations, AggregationReduceContext context) { + public InternalAggregation doReduce(InternalAggregations aggregations, AggregationReduceContext context) { CountCorrelationIndicator bucketPathValue = MlAggsHelper.extractDoubleBucketedValues(bucketsPaths()[0], aggregations) .map( doubleBucketValues -> new CountCorrelationIndicator( diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/inference/InferencePipelineAggregator.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/inference/InferencePipelineAggregator.java index ea01f07146ea6..fd5c66399c72d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/inference/InferencePipelineAggregator.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/inference/InferencePipelineAggregator.java @@ -26,7 +26,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; public class InferencePipelineAggregator extends PipelineAggregator { @@ -102,12 +101,7 @@ public InternalAggregation reduce(InternalAggregation aggregation, AggregationRe inference = new WarningInferenceResults(e.getMessage()); } - final List aggs = bucket.getAggregations() - .asList() - .stream() - .map((p) -> (InternalAggregation) p) - .collect(Collectors.toList()); - + final List aggs = new ArrayList<>(bucket.getAggregations().asList()); InternalInferenceAggregation aggResult = new InternalInferenceAggregation(name(), metadata(), inference); aggs.add(aggResult); InternalMultiBucketAggregation.InternalBucket newBucket = originalAgg.createBucket(InternalAggregations.from(aggs), bucket); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/kstest/BucketCountKSTestAggregator.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/kstest/BucketCountKSTestAggregator.java index 518b76aae3732..f26dadf5ece22 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/kstest/BucketCountKSTestAggregator.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/aggs/kstest/BucketCountKSTestAggregator.java @@ -13,8 +13,8 @@ import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.search.aggregations.AggregationExecutionException; import org.elasticsearch.search.aggregations.AggregationReduceContext; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.pipeline.SiblingPipelineAggregator; import org.elasticsearch.xpack.ml.aggs.DoubleArray; import org.elasticsearch.xpack.ml.aggs.MlAggsHelper; @@ -224,7 +224,7 @@ private static double sidedKSStat(double a, double b, Alternative alternative) { } @Override - public InternalAggregation doReduce(Aggregations aggregations, AggregationReduceContext context) { + public InternalAggregation doReduce(InternalAggregations aggregations, AggregationReduceContext context) { Optional maybeBucketsValue = extractDoubleBucketedValues(bucketsPaths()[0], aggregations).map( bucketValue -> { double[] values = new double[bucketValue.getValues().length + 1]; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AbstractAggregationDataExtractor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AbstractAggregationDataExtractor.java index 468eecc9e56e5..4cd5379d8fe3b 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AbstractAggregationDataExtractor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AbstractAggregationDataExtractor.java @@ -14,7 +14,8 @@ import org.elasticsearch.client.internal.Client; import org.elasticsearch.core.Nullable; import org.elasticsearch.search.aggregations.Aggregation; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfigUtils; @@ -96,7 +97,7 @@ public Result next() throws IOException { SearchInterval searchInterval = new SearchInterval(context.start, context.end); if (aggregationToJsonProcessor == null) { - Aggregations aggs = search(); + InternalAggregations aggs = search(); if (aggs == null) { hasNext = false; return new Result(searchInterval, Optional.empty()); @@ -118,7 +119,7 @@ public Result next() throws IOException { ); } - private Aggregations search() { + private InternalAggregations search() { LOGGER.debug("[{}] Executing aggregated search", context.jobId); T searchRequest = buildSearchRequest(buildBaseSearchSource()); assert searchRequest.request().allowPartialSearchResults() == false; @@ -133,7 +134,7 @@ private Aggregations search() { } } - private void initAggregationProcessor(Aggregations aggs) throws IOException { + private void initAggregationProcessor(InternalAggregations aggs) throws IOException { aggregationToJsonProcessor = new AggregationToJsonProcessor( context.timeField, context.fields, @@ -167,11 +168,11 @@ private SearchSourceBuilder buildBaseSearchSource() { protected abstract T buildSearchRequest(SearchSourceBuilder searchRequestBuilder); - private static Aggregations validateAggs(@Nullable Aggregations aggs) { + private static InternalAggregations validateAggs(@Nullable InternalAggregations aggs) { if (aggs == null) { return null; } - List aggsAsList = aggs.asList(); + List aggsAsList = aggs.asList(); if (aggsAsList.isEmpty()) { return null; } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationToJsonProcessor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationToJsonProcessor.java index 612860efee549..5c9711a6e5d8b 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationToJsonProcessor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AggregationToJsonProcessor.java @@ -11,7 +11,8 @@ import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.Nullable; import org.elasticsearch.search.aggregations.Aggregation; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation; import org.elasticsearch.search.aggregations.bucket.SingleBucketAggregation; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation; @@ -89,7 +90,7 @@ class AggregationToJsonProcessor { this.compositeAggDateValueSourceName = compositeAggDateValueSourceName; } - public void process(Aggregations aggs) throws IOException { + public void process(InternalAggregations aggs) throws IOException { processAggs(0, aggs.asList()); } @@ -102,7 +103,7 @@ public void process(Aggregations aggs) throws IOException { *
  • {@link Percentiles}
  • * */ - private void processAggs(long docCount, List aggregations) throws IOException { + private void processAggs(long docCount, List aggregations) throws IOException { if (aggregations.isEmpty()) { // This means we reached a bucket aggregation without sub-aggs. Thus, we can flush the path written so far. queueDocToWrite(keyValuePairs, docCount); @@ -230,7 +231,7 @@ private void processDateHistogram(Histogram agg) throws IOException { } } - List childAggs = bucket.getAggregations().asList(); + List childAggs = bucket.getAggregations().asList(); processAggs(bucket.getDocCount(), childAggs); keyValuePairs.remove(timeField); } @@ -269,7 +270,7 @@ private void processCompositeAgg(CompositeAggregation agg) throws IOException { } Collection addedFields = processCompositeAggBucketKeys(bucket.getKey()); - List childAggs = bucket.getAggregations().asList(); + List childAggs = bucket.getAggregations().asList(); processAggs(bucket.getDocCount(), childAggs); keyValuePairs.remove(timeField); for (String fieldName : addedFields) { @@ -335,7 +336,7 @@ boolean bucketAggContainsRequiredAgg(MultiBucketsAggregation aggregation) { } boolean foundRequiredAgg = false; - List aggs = asList(aggregation.getBuckets().get(0).getAggregations()); + List aggs = asList(aggregation.getBuckets().get(0).getAggregations()); for (Aggregation agg : aggs) { if (fields.contains(agg.getName())) { foundRequiredAgg = true; @@ -484,7 +485,7 @@ public long getKeyValueCount() { return keyValueWrittenCount; } - private static List asList(@Nullable Aggregations aggs) { + private static List asList(@Nullable InternalAggregations aggs) { return aggs == null ? Collections.emptyList() : aggs.asList(); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/CompositeAggregationDataExtractor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/CompositeAggregationDataExtractor.java index 0d2608cd2752e..0dfdd9897737e 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/CompositeAggregationDataExtractor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/CompositeAggregationDataExtractor.java @@ -12,7 +12,7 @@ import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.internal.Client; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; @@ -108,7 +108,7 @@ public Result next() throws IOException { } SearchInterval searchInterval = new SearchInterval(context.start, context.end); - Aggregations aggs = search(); + InternalAggregations aggs = search(); if (aggs == null) { LOGGER.trace(() -> "[" + context.jobId + "] extraction finished"); hasNext = false; @@ -118,7 +118,7 @@ public Result next() throws IOException { return new Result(searchInterval, Optional.of(processAggs(aggs))); } - private Aggregations search() { + private InternalAggregations search() { // Compare to the normal aggregation implementation, this search does not search for the previous bucket's data. // For composite aggs, since it is scrolling, it is not really possible to know the previous pages results in the current page. // Aggregations like derivative cannot work within composite aggs, for now. @@ -142,7 +142,7 @@ private Aggregations search() { try { LOGGER.trace(() -> "[" + context.jobId + "] Search composite response was obtained"); timingStatsReporter.reportSearchDuration(searchResponse.getTook()); - Aggregations aggregations = searchResponse.getAggregations(); + InternalAggregations aggregations = searchResponse.getAggregations(); if (aggregations == null) { return null; } @@ -175,7 +175,7 @@ protected SearchResponse executeSearchRequest(ActionRequestBuilder 0) { - Aggregations aggregations = searchResponse.getAggregations(); + InternalAggregations aggregations = searchResponse.getAggregations(); Min min = aggregations.get(EARLIEST_TIME); earliestTime = (long) min.value(); Max max = aggregations.get(LATEST_TIME); @@ -285,7 +285,7 @@ private DataSummary newAggregatedDataSummary() { LOGGER.debug("[{}] Aggregating Data summary response was obtained", context.jobId); timingStatsReporter.reportSearchDuration(searchResponse.getTook()); - Aggregations aggregations = searchResponse.getAggregations(); + InternalAggregations aggregations = searchResponse.getAggregations(); // This can happen if all the indices the datafeed is searching are deleted after it started. // Note that unlike the scrolled data summary method above we cannot check for this situation // by checking for zero hits, because aggregations that work on rollups return zero hits even diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java index 1d78ad22f3f85..49e25c95713ef 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java @@ -26,7 +26,7 @@ import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.metrics.Cardinality; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.xpack.core.ClientHelper; @@ -156,7 +156,7 @@ private static void buildFieldCardinalitiesMap( SearchResponse searchResponse, ActionListener> listener ) { - Aggregations aggs = searchResponse.getAggregations(); + InternalAggregations aggs = searchResponse.getAggregations(); if (aggs == null) { listener.onFailure(ExceptionsHelper.serverError("Unexpected null response when gathering field cardinalities")); return; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/traintestsplit/TrainTestSplitterFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/traintestsplit/TrainTestSplitterFactory.java index ebe4295f8efbf..3ef2affa5d399 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/traintestsplit/TrainTestSplitterFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/traintestsplit/TrainTestSplitterFactory.java @@ -14,7 +14,7 @@ import org.elasticsearch.client.internal.Client; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig; @@ -101,7 +101,7 @@ private TrainTestSplitter createStratifiedSplitter(Classification classification searchRequestBuilder::get ); try { - Aggregations aggs = searchResponse.getAggregations(); + InternalAggregations aggs = searchResponse.getAggregations(); Terms terms = aggs.get(aggName); Map classCounts = new HashMap<>(); for (Terms.Bucket bucket : terms.getBuckets()) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java index becbffefff8c8..f8f1e95fecd2e 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/JobResultsProvider.java @@ -68,9 +68,9 @@ import org.elasticsearch.script.Script; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; -import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregation; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.filter.Filters; import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator; import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; @@ -546,7 +546,7 @@ public void getDataCountsModelSizeAndTimingStats( request.setParentTask(parentTaskId); } executeAsyncWithOrigin(client.threadPool().getThreadContext(), ML_ORIGIN, request, ActionListener.wrap(response -> { - Aggregations aggs = response.getAggregations(); + InternalAggregations aggs = response.getAggregations(); if (aggs == null) { handler.apply(new DataCounts(jobId), new ModelSizeStats.Builder(jobId).build(), new TimingStats(jobId)); return; @@ -1602,7 +1602,7 @@ void calculateEstablishedMemoryUsage( ML_ORIGIN, search.request(), ActionListener.wrap(response -> { - List aggregations = response.getAggregations().asList(); + List aggregations = response.getAggregations().asList(); if (aggregations.size() == 1) { ExtendedStats extendedStats = (ExtendedStats) aggregations.get(0); long count = extendedStats.getCount(); @@ -1810,12 +1810,12 @@ public void getForecastStats( ML_ORIGIN, searchRequest, ActionListener.wrap(searchResponse -> { - Aggregations aggregations = searchResponse.getAggregations(); + InternalAggregations aggregations = searchResponse.getAggregations(); if (aggregations == null) { handler.accept(new ForecastStats()); return; } - Map aggregationsAsMap = aggregations.asMap(); + Map aggregationsAsMap = aggregations.asMap(); StatsAccumulator memoryStats = StatsAccumulator.fromStatsAggregation( (Stats) aggregationsAsMap.get(ForecastStats.Fields.MEMORY) ); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/overallbuckets/OverallBucketsProvider.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/overallbuckets/OverallBucketsProvider.java index 6acffc3a6f745..055c75d252281 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/overallbuckets/OverallBucketsProvider.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/persistence/overallbuckets/OverallBucketsProvider.java @@ -8,7 +8,7 @@ import org.apache.lucene.util.PriorityQueue; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.metrics.Max; @@ -38,7 +38,7 @@ public OverallBucketsProvider(TimeValue maxJobBucketSpan, int topN, double minOv public List computeOverallBuckets(Histogram histogram) { List overallBuckets = new ArrayList<>(); for (Histogram.Bucket histogramBucket : histogram.getBuckets()) { - Aggregations histogramBucketAggs = histogramBucket.getAggregations(); + InternalAggregations histogramBucketAggs = histogramBucket.getAggregations(); Terms jobsAgg = histogramBucketAggs.get(Job.ID.getPreferredName()); int jobsCount = jobsAgg.getBuckets().size(); int bucketTopN = Math.min(topN, jobsCount); diff --git a/x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/CostCalculator.java b/x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/CostCalculator.java index ecaaee5d3bf4b..99923d19d81ac 100644 --- a/x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/CostCalculator.java +++ b/x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/CostCalculator.java @@ -13,7 +13,7 @@ final class CostCalculator { private static final double DEFAULT_SAMPLING_FREQUENCY = 20.0d; private static final double SECONDS_PER_HOUR = 60 * 60; private static final double SECONDS_PER_YEAR = SECONDS_PER_HOUR * 24 * 365.0d; // unit: seconds - private static final double DEFAULT_COST_USD_PER_CORE_HOUR = 0.0425d; // unit: USD / (core * hour) + public static final double DEFAULT_COST_USD_PER_CORE_HOUR = 0.0425d; // unit: USD / (core * hour) private static final double DEFAULT_AWS_COST_FACTOR = 1.0d; private final Map hostMetadata; private final double samplingDurationInSeconds; @@ -47,7 +47,7 @@ public double annualCostsUSD(String hostID, double samples) { return annualCoreHours * customCostPerCoreHour * providerCostFactor; } - return annualCoreHours * costs.costFactor * providerCostFactor; + return annualCoreHours * (costs.usd_per_hour / host.profilingNumCores) * providerCostFactor; } public static double annualCoreHours(double duration, double samples, double samplingFrequency) { diff --git a/x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/CostEntry.java b/x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/CostEntry.java index 8d5765fa97c51..b6795294e7f06 100644 --- a/x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/CostEntry.java +++ b/x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/CostEntry.java @@ -7,28 +7,25 @@ package org.elasticsearch.xpack.profiling; -import org.elasticsearch.xcontent.ToXContentObject; -import org.elasticsearch.xcontent.XContentBuilder; - -import java.io.IOException; import java.util.Map; -final class CostEntry implements ToXContentObject { - final double costFactor; +final class CostEntry { + final double usd_per_hour; - CostEntry(double costFactor) { - this.costFactor = costFactor; + CostEntry(double usdPerHour) { + this.usd_per_hour = usdPerHour; } public static CostEntry fromSource(Map source) { - return new CostEntry((Double) source.get("cost_factor")); - } + var val = source.get("usd_per_hour"); + + if (val instanceof Number n) { + // Some JSON values have no decimal places and are passed in as Integers. + return new CostEntry(n.doubleValue()); + } else if (val == null) { + return new CostEntry(CostCalculator.DEFAULT_COST_USD_PER_CORE_HOUR * HostMetadata.DEFAULT_PROFILING_NUM_CORES); + } - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("cost_factor", this.costFactor); - builder.endObject(); - return builder; + throw new IllegalArgumentException("[" + val + "] is an invalid value for [usd_per_hour]"); } } diff --git a/x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/HostMetadata.java b/x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/HostMetadata.java index e0b634b5fb9dd..e9f912a3f60e5 100644 --- a/x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/HostMetadata.java +++ b/x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/HostMetadata.java @@ -15,23 +15,29 @@ import java.util.Objects; final class HostMetadata implements ToXContentObject { + // "present_cpu_cores" is missing in the host metadata when collected before 8.12.0. + // 4 seems to be a reasonable default value. + static final int DEFAULT_PROFILING_NUM_CORES = 4; final String hostID; final InstanceType instanceType; final String profilingHostMachine; // aarch64 or x86_64 + final int profilingNumCores; // number of cores on the profiling host machine - HostMetadata(String hostID, InstanceType instanceType, String profilingHostMachine) { + HostMetadata(String hostID, InstanceType instanceType, String profilingHostMachine, Integer profilingNumCores) { this.hostID = hostID; this.instanceType = instanceType; this.profilingHostMachine = profilingHostMachine; + this.profilingNumCores = profilingNumCores != null ? profilingNumCores : DEFAULT_PROFILING_NUM_CORES; } public static HostMetadata fromSource(Map source) { if (source != null) { String hostID = (String) source.get("host.id"); String profilingHostMachine = (String) source.get("profiling.host.machine"); - return new HostMetadata(hostID, InstanceType.fromHostSource(source), profilingHostMachine); + Integer profilingNumCores = (Integer) source.get("profiling.agent.config.present_cpu_cores"); + return new HostMetadata(hostID, InstanceType.fromHostSource(source), profilingHostMachine, profilingNumCores); } - return new HostMetadata("", new InstanceType("", "", ""), ""); + return new HostMetadata("", new InstanceType("", "", ""), "", null); } @Override diff --git a/x-pack/plugin/profiling/src/main/resources/profiling-costs.json.gz b/x-pack/plugin/profiling/src/main/resources/profiling-costs.json.gz index 1258fb7344b62a8eb0c4b50800ad35e164e11599..590c0ff606201159cac935611c542f5a5cdea50e 100644 GIT binary patch literal 72998 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 UPWKi5KmW)7105h={#>9C07z*^5&!@I literal 59986 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

    kxH&TvLa@cR6 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≤Y2}FS= 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 { if (searchResponse == null @@ -386,7 +384,7 @@ private static List unrollAgg( count = getAggCount(agg, rolled.getAsMap()); } - return unrollAgg((InternalAggregation) agg, original.get(agg.getName()), currentTree.get(agg.getName()), count); + return unrollAgg(agg, original.get(agg.getName()), currentTree.get(agg.getName()), count); }).collect(Collectors.toList()); } @@ -580,7 +578,7 @@ private static InternalAggregations unrollSubAggsFromMulti(InternalBucket bucket currentSubAgg = currentTree.getAggregations().get(subAgg.getName()); } - return unrollAgg((InternalAggregation) subAgg, originalSubAgg, currentSubAgg, count); + return unrollAgg(subAgg, originalSubAgg, currentSubAgg, count); }) .collect(Collectors.toList()) ); @@ -619,7 +617,7 @@ private static InternalAggregation unrollMetric(SingleValue metric, long count) } } - private static long getAggCount(Aggregation agg, Map aggMap) { + private static long getAggCount(Aggregation agg, Map aggMap) { String countPath = null; if (agg.getType().equals(DateHistogramAggregationBuilder.NAME) diff --git a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/IndexerUtils.java b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/IndexerUtils.java index 095eb141bb39d..2b995b0e56da0 100644 --- a/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/IndexerUtils.java +++ b/x-pack/plugin/rollup/src/main/java/org/elasticsearch/xpack/rollup/job/IndexerUtils.java @@ -11,7 +11,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.common.util.Maps; -import org.elasticsearch.search.aggregations.Aggregation; +import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregationBuilder; @@ -62,7 +62,7 @@ static Stream processBuckets( // Put the composite keys into a treemap so that the key iteration order is consistent // TODO would be nice to avoid allocating this treemap in the future TreeMap keys = new TreeMap<>(b.getKey()); - List metrics = b.getAggregations().asList(); + List metrics = b.getAggregations().asList(); RollupIDGenerator idGenerator = new RollupIDGenerator(jobId); Map doc = Maps.newMapWithExpectedSize(keys.size() + metrics.size()); @@ -124,7 +124,7 @@ private static void processKeys( }); } - private static void processMetrics(List metrics, Map doc) { + private static void processMetrics(List metrics, Map doc) { List emptyCounts = new ArrayList<>(); metrics.forEach(m -> { if (m instanceof InternalNumericMetricsAggregation.SingleValue) { diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java index 3aeca660e06b3..ae0949f5bedfa 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/RollupResponseTranslationTests.java @@ -46,7 +46,6 @@ import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationReduceContext; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.Aggregator; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.AggregatorTestCase; @@ -230,7 +229,7 @@ public void testRolledMissingAggs() throws Exception { ); try { assertNotNull(response); - Aggregations responseAggs = response.getAggregations(); + InternalAggregations responseAggs = response.getAggregations(); assertThat(responseAggs.asList().size(), equalTo(0)); } finally { // this SearchResponse is not a mock, so must be decRef'd @@ -311,7 +310,7 @@ public void testTranslateRollup() throws Exception { when(filter.getName()).thenReturn("filter_foo"); aggTree.add(filter); - Aggregations mockAggs = InternalAggregations.from(aggTree); + InternalAggregations mockAggs = InternalAggregations.from(aggTree); when(response.getAggregations()).thenReturn(mockAggs); MultiSearchResponse multiSearchResponse = new MultiSearchResponse( new MultiSearchResponse.Item[] { new MultiSearchResponse.Item(response, null) }, @@ -325,7 +324,7 @@ public void testTranslateRollup() throws Exception { ); try { assertNotNull(finalResponse); - Aggregations responseAggs = finalResponse.getAggregations(); + InternalAggregations responseAggs = finalResponse.getAggregations(); assertNotNull(finalResponse); Avg avg = responseAggs.get("foo"); assertThat(avg.getValue(), equalTo(5.0)); @@ -365,7 +364,7 @@ public void testMissingFilter() { Max protoMax = mock(Max.class); when(protoMax.getName()).thenReturn("foo"); protoAggTree.add(protoMax); - Aggregations protoMockAggs = InternalAggregations.from(protoAggTree); + InternalAggregations protoMockAggs = InternalAggregations.from(protoAggTree); when(protoResponse.getAggregations()).thenReturn(protoMockAggs); MultiSearchResponse.Item unrolledResponse = new MultiSearchResponse.Item(protoResponse, null); @@ -374,7 +373,7 @@ public void testMissingFilter() { Max max = mock(Max.class); when(max.getName()).thenReturn("bizzbuzz"); aggTreeWithoutFilter.add(max); - Aggregations mockAggsWithout = InternalAggregations.from(aggTreeWithoutFilter); + InternalAggregations mockAggsWithout = InternalAggregations.from(aggTreeWithoutFilter); when(responseWithout.getAggregations()).thenReturn(mockAggsWithout); MultiSearchResponse.Item rolledResponse = new MultiSearchResponse.Item(responseWithout, null); @@ -396,7 +395,7 @@ public void testMatchingNameNotFilter() { Max protoMax = mock(Max.class); when(protoMax.getName()).thenReturn("foo"); protoAggTree.add(protoMax); - Aggregations protoMockAggs = InternalAggregations.from(protoAggTree); + InternalAggregations protoMockAggs = InternalAggregations.from(protoAggTree); when(protoResponse.getAggregations()).thenReturn(protoMockAggs); MultiSearchResponse.Item unrolledResponse = new MultiSearchResponse.Item(protoResponse, null); @@ -404,7 +403,7 @@ public void testMatchingNameNotFilter() { List aggTreeWithoutFilter = new ArrayList<>(1); Max max = new Max("filter_foo", 0, DocValueFormat.RAW, null); aggTreeWithoutFilter.add(max); - Aggregations mockAggsWithout = InternalAggregations.from(aggTreeWithoutFilter); + InternalAggregations mockAggsWithout = InternalAggregations.from(aggTreeWithoutFilter); when(responseWithout.getAggregations()).thenReturn(mockAggsWithout); MultiSearchResponse.Item rolledResponse = new MultiSearchResponse.Item(responseWithout, null); @@ -426,7 +425,7 @@ public void testSimpleReduction() throws Exception { List protoAggTree = new ArrayList<>(1); InternalAvg internalAvg = new InternalAvg("foo", 10, 2, DocValueFormat.RAW, null); protoAggTree.add(internalAvg); - Aggregations protoMockAggs = InternalAggregations.from(protoAggTree); + InternalAggregations protoMockAggs = InternalAggregations.from(protoAggTree); when(protoResponse.getAggregations()).thenReturn(protoMockAggs); MultiSearchResponse.Item unrolledResponse = new MultiSearchResponse.Item(protoResponse, null); @@ -474,7 +473,7 @@ public void testSimpleReduction() throws Exception { ); try { assertNotNull(response); - Aggregations responseAggs = response.getAggregations(); + InternalAggregations responseAggs = response.getAggregations(); assertNotNull(responseAggs); Avg avg = responseAggs.get("foo"); assertThat(avg.getValue(), equalTo(5.0)); diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java index 32b9c2df962a9..7971695ecabc1 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/SearchActionTests.java @@ -31,7 +31,6 @@ import org.elasticsearch.indices.IndicesModule; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.SearchModule; -import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.AggregatorFactories; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregations; @@ -737,7 +736,7 @@ public void testRollupOnly() throws Exception { when(filter.getName()).thenReturn("filter_foo"); aggTree.add(filter); - Aggregations mockAggs = InternalAggregations.from(aggTree); + InternalAggregations mockAggs = InternalAggregations.from(aggTree); when(response.getAggregations()).thenReturn(mockAggs); MultiSearchResponse.Item item = new MultiSearchResponse.Item(response, null); MultiSearchResponse msearchResponse = new MultiSearchResponse(new MultiSearchResponse.Item[] { item }, 1); @@ -749,7 +748,7 @@ public void testRollupOnly() throws Exception { ); try { assertNotNull(r); - Aggregations responseAggs = r.getAggregations(); + InternalAggregations responseAggs = r.getAggregations(); Avg avg = responseAggs.get("foo"); assertThat(avg.getValue(), IsEqual.equalTo(5.0)); } finally { @@ -844,7 +843,7 @@ public void testBoth() throws Exception { List protoAggTree = new ArrayList<>(1); InternalAvg internalAvg = new InternalAvg("foo", 10, 2, DocValueFormat.RAW, null); protoAggTree.add(internalAvg); - Aggregations protoMockAggs = InternalAggregations.from(protoAggTree); + InternalAggregations protoMockAggs = InternalAggregations.from(protoAggTree); when(protoResponse.getAggregations()).thenReturn(protoMockAggs); MultiSearchResponse.Item unrolledResponse = new MultiSearchResponse.Item(protoResponse, null); @@ -874,7 +873,7 @@ public void testBoth() throws Exception { when(filter.getName()).thenReturn("filter_foo"); aggTree.add(filter); - Aggregations mockAggsWithout = InternalAggregations.from(aggTree); + InternalAggregations mockAggsWithout = InternalAggregations.from(aggTree); when(responseWithout.getAggregations()).thenReturn(mockAggsWithout); MultiSearchResponse.Item rolledResponse = new MultiSearchResponse.Item(responseWithout, null); @@ -893,7 +892,7 @@ public void testBoth() throws Exception { ); try { assertNotNull(response); - Aggregations responseAggs = response.getAggregations(); + InternalAggregations responseAggs = response.getAggregations(); assertNotNull(responseAggs); Avg avg = responseAggs.get("foo"); assertThat(avg.getValue(), IsEqual.equalTo(5.0)); 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 ce9db5015a0da..b6893e853f256 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 @@ -138,6 +138,7 @@ public class Constants { "cluster:admin/xpack/connector/update_pipeline", "cluster:admin/xpack/connector/update_scheduling", "cluster:admin/xpack/connector/update_service_type", + "cluster:admin/xpack/connector/secret/delete", "cluster:admin/xpack/connector/secret/get", "cluster:admin/xpack/connector/secret/post", "cluster:admin/xpack/connector/sync_job/cancel", diff --git a/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryApiKeyIT.java b/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryApiKeyIT.java index e552befc267c8..e9c640236ceb5 100644 --- a/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryApiKeyIT.java +++ b/x-pack/plugin/security/qa/security-basic/src/javaRestTest/java/org/elasticsearch/xpack/security/QueryApiKeyIT.java @@ -63,10 +63,21 @@ public void testQuery() throws IOException { apiKeys.forEach(k -> assertThat(k, not(hasKey("_sort")))); }); + assertQuery(API_KEY_ADMIN_AUTH_HEADER, """ + { "query": { "match": {"name": {"query": "my-ingest-key-1 my-org/alert-key-1", "analyzer": "whitespace"} } } }""", apiKeys -> { + assertThat(apiKeys.size(), equalTo(2)); + assertThat(apiKeys.get(0).get("name"), oneOf("my-ingest-key-1", "my-org/alert-key-1")); + assertThat(apiKeys.get(1).get("name"), oneOf("my-ingest-key-1", "my-org/alert-key-1")); + apiKeys.forEach(k -> assertThat(k, not(hasKey("_sort")))); + }); + // An empty request body means search for all keys assertQuery(API_KEY_ADMIN_AUTH_HEADER, randomBoolean() ? "" : """ {"query":{"match_all":{}}}""", apiKeys -> assertThat(apiKeys.size(), equalTo(6))); + assertQuery(API_KEY_ADMIN_AUTH_HEADER, randomBoolean() ? "" : """ + { "query": { "match": {"type": "rest"} } }""", apiKeys -> assertThat(apiKeys.size(), equalTo(6))); + assertQuery( API_KEY_ADMIN_AUTH_HEADER, """ diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java index 3833a6466c67c..49c2da7b173ec 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java @@ -778,6 +778,58 @@ public void testQueryCrossClusterApiKeysByType() throws IOException { assertThat(queryResponse.evaluate("api_keys.0.name"), is("test-cross-key-query-2")); } + public void testSortApiKeysByType() throws IOException { + List apiKeyIds = new ArrayList<>(2); + // create regular api key + EncodedApiKey encodedApiKey = createApiKey("test-rest-key", Map.of("tag", "rest")); + apiKeyIds.add(encodedApiKey.id()); + // create cross-cluster key + Request createRequest = new Request("POST", "/_security/cross_cluster/api_key"); + createRequest.setJsonEntity(""" + { + "name": "test-cross-key", + "access": { + "search": [ + { + "names": [ "whatever" ] + } + ] + }, + "metadata": { "tag": "cross" } + }"""); + setUserForRequest(createRequest, MANAGE_SECURITY_USER, END_USER_PASSWORD); + ObjectPath createResponse = assertOKAndCreateObjectPath(client().performRequest(createRequest)); + apiKeyIds.add(createResponse.evaluate("id")); + + // desc sort all (2) keys - by type + Request queryRequest = new Request("GET", "/_security/_query/api_key"); + queryRequest.addParameter("with_limited_by", String.valueOf(randomBoolean())); + queryRequest.setJsonEntity(""" + {"sort":[{"type":{"order":"desc"}}]}"""); + setUserForRequest(queryRequest, MANAGE_API_KEY_USER, END_USER_PASSWORD); + ObjectPath queryResponse = assertOKAndCreateObjectPath(client().performRequest(queryRequest)); + assertThat(queryResponse.evaluate("total"), is(2)); + assertThat(queryResponse.evaluate("count"), is(2)); + assertThat(queryResponse.evaluate("api_keys.0.id"), is(apiKeyIds.get(0))); + assertThat(queryResponse.evaluate("api_keys.0.type"), is("rest")); + assertThat(queryResponse.evaluate("api_keys.1.id"), is(apiKeyIds.get(1))); + assertThat(queryResponse.evaluate("api_keys.1.type"), is("cross_cluster")); + + // asc sort all (2) keys - by type + queryRequest = new Request("GET", "/_security/_query/api_key"); + queryRequest.addParameter("with_limited_by", String.valueOf(randomBoolean())); + queryRequest.setJsonEntity(""" + {"sort":[{"type":{"order":"asc"}}]}"""); + setUserForRequest(queryRequest, MANAGE_API_KEY_USER, END_USER_PASSWORD); + queryResponse = assertOKAndCreateObjectPath(client().performRequest(queryRequest)); + assertThat(queryResponse.evaluate("total"), is(2)); + assertThat(queryResponse.evaluate("count"), is(2)); + assertThat(queryResponse.evaluate("api_keys.0.id"), is(apiKeyIds.get(1))); + assertThat(queryResponse.evaluate("api_keys.0.type"), is("cross_cluster")); + assertThat(queryResponse.evaluate("api_keys.1.id"), is(apiKeyIds.get(0))); + assertThat(queryResponse.evaluate("api_keys.1.type"), is("rest")); + } + public void testCreateCrossClusterApiKey() throws IOException { final Request createRequest = new Request("POST", "/_security/cross_cluster/api_key"); createRequest.setJsonEntity(""" diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java index 9d25802544d38..b1f73251cdb47 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyAction.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MAIN_ALIAS; @@ -81,22 +82,25 @@ protected void doExecute(Task task, QueryApiKeyRequest request, ActionListener { + searchSourceBuilder.query(ApiKeyBoolQueryBuilder.build(request.getQueryBuilder(), fieldName -> { if (API_KEY_TYPE_RUNTIME_MAPPING_FIELD.equals(fieldName)) { accessesApiKeyTypeField.set(true); } - }, request.isFilterForCurrentUser() ? authentication : null); - searchSourceBuilder.query(apiKeyBoolQueryBuilder); + }, request.isFilterForCurrentUser() ? authentication : null)); + + if (request.getFieldSortBuilders() != null) { + translateFieldSortBuilders(request.getFieldSortBuilders(), searchSourceBuilder, fieldName -> { + if (API_KEY_TYPE_RUNTIME_MAPPING_FIELD.equals(fieldName)) { + accessesApiKeyTypeField.set(true); + } + }); + } // only add the query-level runtime field to the search request if it's actually referring the "type" field if (accessesApiKeyTypeField.get()) { searchSourceBuilder.runtimeMappings(API_KEY_TYPE_RUNTIME_MAPPING); } - if (request.getFieldSortBuilders() != null) { - translateFieldSortBuilders(request.getFieldSortBuilders(), searchSourceBuilder); - } - if (request.getSearchAfterBuilder() != null) { searchSourceBuilder.searchAfter(request.getSearchAfterBuilder().getSortValues()); } @@ -106,7 +110,11 @@ protected void doExecute(Task task, QueryApiKeyRequest request, ActionListener fieldSortBuilders, SearchSourceBuilder searchSourceBuilder) { + static void translateFieldSortBuilders( + List fieldSortBuilders, + SearchSourceBuilder searchSourceBuilder, + Consumer fieldNameVisitor + ) { fieldSortBuilders.forEach(fieldSortBuilder -> { if (fieldSortBuilder.getNestedSort() != null) { throw new IllegalArgumentException("nested sorting is not supported for API Key query"); @@ -115,6 +123,7 @@ static void translateFieldSortBuilders(List fieldSortBuilders, searchSourceBuilder.sort(fieldSortBuilder); } else { final String translatedFieldName = ApiKeyFieldNameTranslators.translate(fieldSortBuilder.getFieldName()); + fieldNameVisitor.accept(translatedFieldName); if (translatedFieldName.equals(fieldSortBuilder.getFieldName())) { searchSourceBuilder.sort(fieldSortBuilder); } else { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java index 9f7b84e4a2698..651427d07e651 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilder.java @@ -14,6 +14,7 @@ import org.elasticsearch.index.query.IdsQueryBuilder; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.MatchNoneQueryBuilder; +import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.PrefixQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; @@ -111,7 +112,8 @@ private static QueryBuilder doProcess(QueryBuilder qb, Consumer fieldNam if (qb instanceof final BoolQueryBuilder query) { final BoolQueryBuilder newQuery = QueryBuilders.boolQuery() .minimumShouldMatch(query.minimumShouldMatch()) - .adjustPureNegative(query.adjustPureNegative()); + .adjustPureNegative(query.adjustPureNegative()) + .boost(query.boost()); query.must().stream().map(q -> ApiKeyBoolQueryBuilder.doProcess(q, fieldNameVisitor)).forEach(newQuery::must); query.should().stream().map(q -> ApiKeyBoolQueryBuilder.doProcess(q, fieldNameVisitor)).forEach(newQuery::should); query.mustNot().stream().map(q -> ApiKeyBoolQueryBuilder.doProcess(q, fieldNameVisitor)).forEach(newQuery::mustNot); @@ -124,28 +126,63 @@ private static QueryBuilder doProcess(QueryBuilder qb, Consumer fieldNam } else if (qb instanceof final TermQueryBuilder query) { final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName()); fieldNameVisitor.accept(translatedFieldName); - return QueryBuilders.termQuery(translatedFieldName, query.value()).caseInsensitive(query.caseInsensitive()); + return QueryBuilders.termQuery(translatedFieldName, query.value()) + .caseInsensitive(query.caseInsensitive()) + .boost(query.boost()); } else if (qb instanceof final ExistsQueryBuilder query) { final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName()); fieldNameVisitor.accept(translatedFieldName); - return QueryBuilders.existsQuery(translatedFieldName); + return QueryBuilders.existsQuery(translatedFieldName).boost(query.boost()); } else if (qb instanceof final TermsQueryBuilder query) { if (query.termsLookup() != null) { throw new IllegalArgumentException("terms query with terms lookup is not supported for API Key query"); } final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName()); fieldNameVisitor.accept(translatedFieldName); - return QueryBuilders.termsQuery(translatedFieldName, query.getValues()); + return QueryBuilders.termsQuery(translatedFieldName, query.getValues()).boost(query.boost()); } else if (qb instanceof final PrefixQueryBuilder query) { final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName()); fieldNameVisitor.accept(translatedFieldName); - return QueryBuilders.prefixQuery(translatedFieldName, query.value()).caseInsensitive(query.caseInsensitive()); + return QueryBuilders.prefixQuery(translatedFieldName, query.value()) + .caseInsensitive(query.caseInsensitive()) + .rewrite(query.rewrite()) + .boost(query.boost()); } else if (qb instanceof final WildcardQueryBuilder query) { final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName()); fieldNameVisitor.accept(translatedFieldName); return QueryBuilders.wildcardQuery(translatedFieldName, query.value()) .caseInsensitive(query.caseInsensitive()) - .rewrite(query.rewrite()); + .rewrite(query.rewrite()) + .boost(query.boost()); + } else if (qb instanceof final MatchQueryBuilder query) { + final String translatedFieldName = ApiKeyFieldNameTranslators.translate(query.fieldName()); + fieldNameVisitor.accept(translatedFieldName); + final MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery(translatedFieldName, query.value()); + if (query.operator() != null) { + matchQueryBuilder.operator(query.operator()); + } + if (query.analyzer() != null) { + matchQueryBuilder.analyzer(query.analyzer()); + } + if (query.fuzziness() != null) { + matchQueryBuilder.fuzziness(query.fuzziness()); + } + if (query.minimumShouldMatch() != null) { + matchQueryBuilder.minimumShouldMatch(query.minimumShouldMatch()); + } + if (query.fuzzyRewrite() != null) { + matchQueryBuilder.fuzzyRewrite(query.fuzzyRewrite()); + } + if (query.zeroTermsQuery() != null) { + matchQueryBuilder.zeroTermsQuery(query.zeroTermsQuery()); + } + matchQueryBuilder.prefixLength(query.prefixLength()) + .maxExpansions(query.maxExpansions()) + .fuzzyTranspositions(query.fuzzyTranspositions()) + .lenient(query.lenient()) + .autoGenerateSynonymsPhraseQuery(query.autoGenerateSynonymsPhraseQuery()) + .boost(query.boost()); + return matchQueryBuilder; } else if (qb instanceof final RangeQueryBuilder query) { if (query.relation() != null) { throw new IllegalArgumentException("range query with relation is not supported for API Key query"); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyActionTests.java index d1a0b5d7ca95c..4a2f707d3e902 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/apikey/TransportQueryApiKeyActionTests.java @@ -14,15 +14,18 @@ import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.test.ESTestCase; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.stream.IntStream; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; public class TransportQueryApiKeyActionTests extends ESTestCase { public void testTranslateFieldSortBuilders() { + final String metadataField = randomAlphaOfLengthBetween(3, 8); final List fieldNames = List.of( "_doc", "username", @@ -30,14 +33,16 @@ public void testTranslateFieldSortBuilders() { "name", "creation", "expiration", + "type", "invalidated", - "metadata." + randomAlphaOfLengthBetween(3, 8) + "metadata." + metadataField ); final List originals = fieldNames.stream().map(this::randomFieldSortBuilderWithName).toList(); + List sortFields = new ArrayList<>(); final SearchSourceBuilder searchSourceBuilder = SearchSourceBuilder.searchSource(); - TransportQueryApiKeyAction.translateFieldSortBuilders(originals, searchSourceBuilder); + TransportQueryApiKeyAction.translateFieldSortBuilders(originals, searchSourceBuilder, sortFields::add); IntStream.range(0, originals.size()).forEach(i -> { final FieldSortBuilder original = originals.get(i); @@ -57,6 +62,8 @@ public void testTranslateFieldSortBuilders() { assertThat(translated.getFieldName(), equalTo("api_key_invalidated")); } else if (original.getFieldName().startsWith("metadata.")) { assertThat(translated.getFieldName(), equalTo("metadata_flattened." + original.getFieldName().substring(9))); + } else if ("type".equals(original.getFieldName())) { + assertThat(translated.getFieldName(), equalTo("runtime_key_type")); } else { fail("unrecognized field name: [" + original.getFieldName() + "]"); } @@ -68,6 +75,19 @@ public void testTranslateFieldSortBuilders() { assertThat(translated.sortMode(), equalTo(original.sortMode())); } }); + assertThat( + sortFields, + containsInAnyOrder( + "creator.principal", + "creator.realm", + "name", + "creation_time", + "expiration_time", + "runtime_key_type", + "api_key_invalidated", + "metadata_flattened." + metadataField + ) + ); } public void testNestedSortingIsNotAllowed() { @@ -75,7 +95,11 @@ public void testNestedSortingIsNotAllowed() { fieldSortBuilder.setNestedSort(new NestedSortBuilder("name")); final IllegalArgumentException e = expectThrows( IllegalArgumentException.class, - () -> TransportQueryApiKeyAction.translateFieldSortBuilders(List.of(fieldSortBuilder), SearchSourceBuilder.searchSource()) + () -> TransportQueryApiKeyAction.translateFieldSortBuilders( + List.of(fieldSortBuilder), + SearchSourceBuilder.searchSource(), + ignored -> {} + ) ); assertThat(e.getMessage(), equalTo("nested sorting is not supported for API Key query")); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java index 46fde61690017..61646f5ff375b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java @@ -345,6 +345,7 @@ public void testElasticEnterpriseSearchServerAccount() { assertThat(role.cluster().check(ILMActions.PUT.name(), request, authentication), is(true)); // Connector secrets. Enterprise Search has read and write access. + assertThat(role.cluster().check("cluster:admin/xpack/connector/secret/delete", request, authentication), is(true)); assertThat(role.cluster().check("cluster:admin/xpack/connector/secret/get", request, authentication), is(true)); assertThat(role.cluster().check("cluster:admin/xpack/connector/secret/post", request, authentication), is(true)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java index 4064d9f0ce4da..44b81b96e2154 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/support/ApiKeyBoolQueryBuilderTests.java @@ -7,13 +7,16 @@ package org.elasticsearch.xpack.security.support; +import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.index.query.AbstractQueryBuilder; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.DistanceFeatureQueryBuilder; import org.elasticsearch.index.query.IdsQueryBuilder; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.MatchNoneQueryBuilder; +import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.MultiTermQueryBuilder; +import org.elasticsearch.index.query.Operator; import org.elasticsearch.index.query.PrefixQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; @@ -24,10 +27,12 @@ import org.elasticsearch.index.query.TermQueryBuilder; import org.elasticsearch.index.query.TermsQueryBuilder; import org.elasticsearch.index.query.WildcardQueryBuilder; +import org.elasticsearch.index.query.ZeroTermsQueryOption; import org.elasticsearch.indices.TermsLookup; import org.elasticsearch.script.Script; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authc.AuthenticationTests; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.user.User; @@ -47,11 +52,13 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.iterableWithSize; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -60,17 +67,144 @@ public class ApiKeyBoolQueryBuilderTests extends ESTestCase { public void testBuildFromSimpleQuery() { - final Authentication authentication = randomBoolean() ? AuthenticationTests.randomAuthentication(null, null) : null; - final QueryBuilder q1 = randomSimpleQuery("name"); - final List queryFields = new ArrayList<>(); - final ApiKeyBoolQueryBuilder apiKeyQb1 = ApiKeyBoolQueryBuilder.build(q1, queryFields::add, authentication); - assertQueryFields(queryFields, q1, authentication); - assertCommonFilterQueries(apiKeyQb1, authentication); - final List mustQueries = apiKeyQb1.must(); + { + QueryBuilder qb = randomSimpleQuery("name"); + List queryFields = new ArrayList<>(); + ApiKeyBoolQueryBuilder apiKeyQb = ApiKeyBoolQueryBuilder.build(qb, queryFields::add, null); + assertQueryFields(queryFields, qb, null); + assertCommonFilterQueries(apiKeyQb, null); + List mustQueries = apiKeyQb.must(); + assertThat(mustQueries, hasSize(1)); + assertThat(mustQueries.get(0), equalTo(qb)); + assertThat(apiKeyQb.should(), emptyIterable()); + assertThat(apiKeyQb.mustNot(), emptyIterable()); + } + { + Authentication authentication = AuthenticationTests.randomAuthentication(null, null); + QueryBuilder qb = randomSimpleQuery("name"); + List queryFields = new ArrayList<>(); + ApiKeyBoolQueryBuilder apiKeyQb = ApiKeyBoolQueryBuilder.build(qb, queryFields::add, authentication); + assertQueryFields(queryFields, qb, authentication); + assertCommonFilterQueries(apiKeyQb, authentication); + List mustQueries = apiKeyQb.must(); + assertThat(mustQueries, hasSize(1)); + assertThat(mustQueries.get(0), equalTo(qb)); + assertThat(apiKeyQb.should(), emptyIterable()); + assertThat(apiKeyQb.mustNot(), emptyIterable()); + } + { + String apiKeyId = randomUUID(); + Authentication authentication = AuthenticationTests.randomApiKeyAuthentication(AuthenticationTests.randomUser(), apiKeyId); + QueryBuilder qb = randomSimpleQuery("name"); + List queryFields = new ArrayList<>(); + ApiKeyBoolQueryBuilder apiKeyQb = ApiKeyBoolQueryBuilder.build(qb, queryFields::add, authentication); + assertQueryFields(queryFields, qb, authentication); + assertCommonFilterQueries(apiKeyQb, authentication); + List mustQueries = apiKeyQb.must(); + assertThat(mustQueries, hasSize(1)); + assertThat(mustQueries.get(0), equalTo(qb)); + assertThat(apiKeyQb.should(), emptyIterable()); + assertThat(apiKeyQb.mustNot(), emptyIterable()); + } + } + + public void testPrefixQueryBuilderPropertiesArePreserved() { + Authentication authentication = randomFrom( + AuthenticationTests.randomApiKeyAuthentication(AuthenticationTests.randomUser(), randomUUID()), + AuthenticationTests.randomAuthentication(null, null), + null + ); + String fieldName = randomValidFieldName(); + PrefixQueryBuilder prefixQueryBuilder = QueryBuilders.prefixQuery(fieldName, randomAlphaOfLengthBetween(0, 4)); + if (randomBoolean()) { + prefixQueryBuilder.boost(Math.abs(randomFloat())); + } + if (randomBoolean()) { + prefixQueryBuilder.caseInsensitive(randomBoolean()); + } + if (randomBoolean()) { + prefixQueryBuilder.rewrite(randomAlphaOfLengthBetween(0, 4)); + } + List queryFields = new ArrayList<>(); + ApiKeyBoolQueryBuilder apiKeyMatchQueryBuilder = ApiKeyBoolQueryBuilder.build(prefixQueryBuilder, queryFields::add, authentication); + assertThat(queryFields, hasItem(ApiKeyFieldNameTranslators.translate(fieldName))); + List mustQueries = apiKeyMatchQueryBuilder.must(); assertThat(mustQueries, hasSize(1)); - assertThat(mustQueries.get(0), equalTo(q1)); - assertTrue(apiKeyQb1.should().isEmpty()); - assertTrue(apiKeyQb1.mustNot().isEmpty()); + assertThat(mustQueries.get(0), instanceOf(PrefixQueryBuilder.class)); + PrefixQueryBuilder prefixQueryBuilder2 = (PrefixQueryBuilder) mustQueries.get(0); + assertThat(prefixQueryBuilder2.fieldName(), is(ApiKeyFieldNameTranslators.translate(prefixQueryBuilder.fieldName()))); + assertThat(prefixQueryBuilder2.value(), is(prefixQueryBuilder.value())); + assertThat(prefixQueryBuilder2.boost(), is(prefixQueryBuilder.boost())); + assertThat(prefixQueryBuilder2.caseInsensitive(), is(prefixQueryBuilder.caseInsensitive())); + assertThat(prefixQueryBuilder2.rewrite(), is(prefixQueryBuilder.rewrite())); + } + + public void testMatchQueryBuilderPropertiesArePreserved() { + // the match query has many properties, that all must be preserved after limiting for API Key docs only + Authentication authentication = randomFrom( + AuthenticationTests.randomApiKeyAuthentication(AuthenticationTests.randomUser(), randomUUID()), + AuthenticationTests.randomAuthentication(null, null), + null + ); + String fieldName = randomValidFieldName(); + MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery(fieldName, new Object()); + if (randomBoolean()) { + matchQueryBuilder.boost(Math.abs(randomFloat())); + } + if (randomBoolean()) { + matchQueryBuilder.operator(randomFrom(Operator.OR, Operator.AND)); + } + if (randomBoolean()) { + matchQueryBuilder.analyzer(randomAlphaOfLength(4)); + } + if (randomBoolean()) { + matchQueryBuilder.fuzziness(randomFrom(Fuzziness.ZERO, Fuzziness.ONE, Fuzziness.TWO, Fuzziness.AUTO)); + } + if (randomBoolean()) { + matchQueryBuilder.minimumShouldMatch(randomAlphaOfLength(4)); + } + if (randomBoolean()) { + matchQueryBuilder.fuzzyRewrite(randomAlphaOfLength(4)); + } + if (randomBoolean()) { + matchQueryBuilder.zeroTermsQuery(randomFrom(ZeroTermsQueryOption.NONE, ZeroTermsQueryOption.ALL, ZeroTermsQueryOption.NULL)); + } + if (randomBoolean()) { + matchQueryBuilder.prefixLength(randomNonNegativeInt()); + } + if (randomBoolean()) { + matchQueryBuilder.maxExpansions(randomIntBetween(1, 100)); + } + if (randomBoolean()) { + matchQueryBuilder.fuzzyTranspositions(randomBoolean()); + } + if (randomBoolean()) { + matchQueryBuilder.lenient(randomBoolean()); + } + if (randomBoolean()) { + matchQueryBuilder.autoGenerateSynonymsPhraseQuery(randomBoolean()); + } + List queryFields = new ArrayList<>(); + ApiKeyBoolQueryBuilder apiKeyMatchQueryBuilder = ApiKeyBoolQueryBuilder.build(matchQueryBuilder, queryFields::add, authentication); + assertThat(queryFields, hasItem(ApiKeyFieldNameTranslators.translate(fieldName))); + List mustQueries = apiKeyMatchQueryBuilder.must(); + assertThat(mustQueries, hasSize(1)); + assertThat(mustQueries.get(0), instanceOf(MatchQueryBuilder.class)); + MatchQueryBuilder matchQueryBuilder2 = (MatchQueryBuilder) mustQueries.get(0); + assertThat(matchQueryBuilder2.fieldName(), is(ApiKeyFieldNameTranslators.translate(matchQueryBuilder.fieldName()))); + assertThat(matchQueryBuilder2.value(), is(matchQueryBuilder.value())); + assertThat(matchQueryBuilder2.operator(), is(matchQueryBuilder.operator())); + assertThat(matchQueryBuilder2.analyzer(), is(matchQueryBuilder.analyzer())); + assertThat(matchQueryBuilder2.fuzziness(), is(matchQueryBuilder.fuzziness())); + assertThat(matchQueryBuilder2.minimumShouldMatch(), is(matchQueryBuilder.minimumShouldMatch())); + assertThat(matchQueryBuilder2.fuzzyRewrite(), is(matchQueryBuilder.fuzzyRewrite())); + assertThat(matchQueryBuilder2.zeroTermsQuery(), is(matchQueryBuilder.zeroTermsQuery())); + assertThat(matchQueryBuilder2.prefixLength(), is(matchQueryBuilder.prefixLength())); + assertThat(matchQueryBuilder2.maxExpansions(), is(matchQueryBuilder.maxExpansions())); + assertThat(matchQueryBuilder2.fuzzyTranspositions(), is(matchQueryBuilder.fuzzyTranspositions())); + assertThat(matchQueryBuilder2.lenient(), is(matchQueryBuilder.lenient())); + assertThat(matchQueryBuilder2.autoGenerateSynonymsPhraseQuery(), is(matchQueryBuilder.autoGenerateSynonymsPhraseQuery())); + assertThat(matchQueryBuilder2.boost(), is(matchQueryBuilder.boost())); } public void testQueryForDomainAuthentication() { @@ -405,7 +539,6 @@ public void testDisallowedQueryTypes() { final Authentication authentication = randomBoolean() ? AuthenticationTests.randomAuthentication(null, null) : null; final AbstractQueryBuilder> q1 = randomFrom( - QueryBuilders.matchQuery(randomAlphaOfLength(5), randomAlphaOfLength(5)), QueryBuilders.constantScoreQuery(mock(QueryBuilder.class)), QueryBuilders.boostingQuery(mock(QueryBuilder.class), mock(QueryBuilder.class)), QueryBuilders.queryStringQuery("q=a:42"), @@ -760,20 +893,38 @@ private void assertCommonFilterQueries(ApiKeyBoolQueryBuilder qb, Authentication if (authentication == null) { return; } - assertTrue( - tqb.stream() - .anyMatch( - q -> q.equals(QueryBuilders.termQuery("creator.principal", authentication.getEffectiveSubject().getUser().principal())) + if (authentication.isApiKey()) { + List idsQueryBuilders = qb.filter() + .stream() + .filter(q -> q.getClass() == IdsQueryBuilder.class) + .map(q -> (IdsQueryBuilder) q) + .toList(); + assertThat(idsQueryBuilders, iterableWithSize(1)); + assertThat( + idsQueryBuilders.get(0), + equalTo( + QueryBuilders.idsQuery() + .addIds((String) authentication.getAuthenticatingSubject().getMetadata().get(AuthenticationField.API_KEY_ID_KEY)) ) - ); - assertTrue( - tqb.stream() - .anyMatch(q -> q.equals(QueryBuilders.termQuery("creator.realm", ApiKeyService.getCreatorRealmName(authentication)))) - ); + ); + } else { + assertTrue( + tqb.stream() + .anyMatch( + q -> q.equals( + QueryBuilders.termQuery("creator.principal", authentication.getEffectiveSubject().getUser().principal()) + ) + ) + ); + assertTrue( + tqb.stream() + .anyMatch(q -> q.equals(QueryBuilders.termQuery("creator.realm", ApiKeyService.getCreatorRealmName(authentication)))) + ); + } } private QueryBuilder randomSimpleQuery(String fieldName) { - return switch (randomIntBetween(0, 8)) { + return switch (randomIntBetween(0, 9)) { case 0 -> QueryBuilders.termQuery(fieldName, randomAlphaOfLengthBetween(3, 8)); case 1 -> QueryBuilders.termsQuery(fieldName, randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 8))); case 2 -> QueryBuilders.idsQuery().addIds(randomArray(1, 3, String[]::new, () -> randomAlphaOfLength(22))); @@ -788,6 +939,11 @@ private QueryBuilder randomSimpleQuery(String fieldName) { .field(fieldName) .lenient(randomBoolean()) .analyzeWildcard(randomBoolean()); + case 9 -> QueryBuilders.matchQuery(fieldName, randomAlphaOfLengthBetween(3, 8)) + .operator(randomFrom(Operator.OR, Operator.AND)) + .lenient(randomBoolean()) + .maxExpansions(randomIntBetween(1, 100)) + .analyzer(randomFrom(randomAlphaOfLength(4), null)); default -> throw new IllegalStateException("illegal switch case"); }; } @@ -802,4 +958,19 @@ private void assertQueryFields(List actualQueryFields, QueryBuilder quer assertThat(actualQueryFields, hasItem("creator.realm")); } } + + private static String randomValidFieldName() { + return randomFrom( + "username", + "realm_name", + "name", + "type", + "creation", + "expiration", + "invalidated", + "invalidation", + "metadata", + "metadata.what.ever" + ); + } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/ShapeUtils.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/ShapeUtils.java deleted file mode 100644 index 289fbe6e707ca..0000000000000 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/common/ShapeUtils.java +++ /dev/null @@ -1,68 +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.spatial.common; - -import org.elasticsearch.geometry.Circle; -import org.elasticsearch.geometry.Line; -import org.elasticsearch.geometry.Point; -import org.elasticsearch.geometry.Polygon; -import org.elasticsearch.geometry.Rectangle; - -/** - * Utility class that transforms Elasticsearch geometry objects to the Lucene representation - */ -public class ShapeUtils { - // no instance: - private ShapeUtils() {} - - public static org.apache.lucene.geo.XYPolygon toLuceneXYPolygon(Polygon polygon) { - org.apache.lucene.geo.XYPolygon[] holes = new org.apache.lucene.geo.XYPolygon[polygon.getNumberOfHoles()]; - for (int i = 0; i < holes.length; i++) { - holes[i] = new org.apache.lucene.geo.XYPolygon( - doubleArrayToFloatArray(polygon.getHole(i).getX()), - doubleArrayToFloatArray(polygon.getHole(i).getY()) - ); - } - return new org.apache.lucene.geo.XYPolygon( - doubleArrayToFloatArray(polygon.getPolygon().getX()), - doubleArrayToFloatArray(polygon.getPolygon().getY()), - holes - ); - } - - public static org.apache.lucene.geo.XYPolygon toLuceneXYPolygon(Rectangle r) { - return new org.apache.lucene.geo.XYPolygon( - new float[] { (float) r.getMinX(), (float) r.getMaxX(), (float) r.getMaxX(), (float) r.getMinX(), (float) r.getMinX() }, - new float[] { (float) r.getMinY(), (float) r.getMinY(), (float) r.getMaxY(), (float) r.getMaxY(), (float) r.getMinY() } - ); - } - - public static org.apache.lucene.geo.XYRectangle toLuceneXYRectangle(Rectangle r) { - return new org.apache.lucene.geo.XYRectangle((float) r.getMinX(), (float) r.getMaxX(), (float) r.getMinY(), (float) r.getMaxY()); - } - - public static org.apache.lucene.geo.XYPoint toLuceneXYPoint(Point point) { - return new org.apache.lucene.geo.XYPoint((float) point.getX(), (float) point.getY()); - } - - public static org.apache.lucene.geo.XYLine toLuceneXYLine(Line line) { - return new org.apache.lucene.geo.XYLine(doubleArrayToFloatArray(line.getX()), doubleArrayToFloatArray(line.getY())); - } - - public static org.apache.lucene.geo.XYCircle toLuceneXYCircle(Circle circle) { - return new org.apache.lucene.geo.XYCircle((float) circle.getX(), (float) circle.getY(), (float) circle.getRadiusMeters()); - } - - private static float[] doubleArrayToFloatArray(double[] array) { - float[] result = new float[array.length]; - for (int i = 0; i < array.length; ++i) { - result[i] = (float) array[i]; - } - return result; - } - -} diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeIndexer.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeIndexer.java index b8e665c0c768a..c23d63baa5791 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeIndexer.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeIndexer.java @@ -8,6 +8,7 @@ import org.apache.lucene.document.XYShape; import org.apache.lucene.index.IndexableField; +import org.elasticsearch.common.geo.LuceneGeometriesUtils; import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.geometry.GeometryCollection; @@ -21,7 +22,6 @@ import org.elasticsearch.geometry.Polygon; import org.elasticsearch.geometry.Rectangle; import org.elasticsearch.index.mapper.ShapeIndexer; -import org.elasticsearch.xpack.spatial.common.ShapeUtils; import java.util.ArrayList; import java.util.Arrays; @@ -70,7 +70,7 @@ public Void visit(GeometryCollection collection) { @Override public Void visit(Line line) { - addFields(XYShape.createIndexableFields(name, ShapeUtils.toLuceneXYLine(line))); + addFields(XYShape.createIndexableFields(name, LuceneGeometriesUtils.toXYLine(line))); return null; } @@ -111,13 +111,13 @@ public Void visit(Point point) { @Override public Void visit(Polygon polygon) { - addFields(XYShape.createIndexableFields(name, ShapeUtils.toLuceneXYPolygon(polygon), true)); + addFields(XYShape.createIndexableFields(name, LuceneGeometriesUtils.toXYPolygon(polygon), true)); return null; } @Override public Void visit(Rectangle r) { - addFields(XYShape.createIndexableFields(name, ShapeUtils.toLuceneXYPolygon(r))); + addFields(XYShape.createIndexableFields(name, toLuceneXYPolygon(r))); return null; } @@ -125,4 +125,11 @@ private void addFields(IndexableField[] fields) { this.fields.addAll(Arrays.asList(fields)); } } + + private static org.apache.lucene.geo.XYPolygon toLuceneXYPolygon(Rectangle r) { + return new org.apache.lucene.geo.XYPolygon( + new float[] { (float) r.getMinX(), (float) r.getMaxX(), (float) r.getMaxX(), (float) r.getMinX(), (float) r.getMinX() }, + new float[] { (float) r.getMinY(), (float) r.getMinY(), (float) r.getMaxY(), (float) r.getMaxY(), (float) r.getMinY() } + ); + } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java index d455d0f539cfa..a8c084e7e0f01 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryPointProcessor.java @@ -8,44 +8,43 @@ import org.apache.lucene.document.XYDocValuesField; import org.apache.lucene.document.XYPointField; -import org.apache.lucene.geo.XYCircle; -import org.apache.lucene.geo.XYRectangle; -import org.apache.lucene.search.BooleanClause; -import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.geo.XYGeometry; import org.apache.lucene.search.IndexOrDocValuesQuery; import org.apache.lucene.search.Query; +import org.elasticsearch.common.geo.LuceneGeometriesUtils; import org.elasticsearch.common.geo.ShapeRelation; -import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; -import org.elasticsearch.geometry.GeometryCollection; -import org.elasticsearch.geometry.GeometryVisitor; -import org.elasticsearch.geometry.LinearRing; -import org.elasticsearch.geometry.MultiLine; -import org.elasticsearch.geometry.MultiPoint; -import org.elasticsearch.geometry.MultiPolygon; -import org.elasticsearch.geometry.Point; -import org.elasticsearch.geometry.Polygon; -import org.elasticsearch.geometry.Rectangle; import org.elasticsearch.geometry.ShapeType; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.query.QueryShardException; import org.elasticsearch.index.query.SearchExecutionContext; -import org.elasticsearch.xpack.spatial.common.ShapeUtils; import org.elasticsearch.xpack.spatial.index.mapper.PointFieldMapper; +import java.util.function.Consumer; + public class ShapeQueryPointProcessor { - public Query shapeQuery(Geometry shape, String fieldName, ShapeRelation relation, SearchExecutionContext context) { - validateIsPointFieldType(fieldName, context); + public Query shapeQuery(Geometry geometry, String fieldName, ShapeRelation relation, SearchExecutionContext context) { + final boolean hasDocValues = validateIsPointFieldType(fieldName, context); // only the intersects relation is supported for indexed cartesian point types if (relation != ShapeRelation.INTERSECTS) { throw new QueryShardException(context, relation + " query relation not supported for Field [" + fieldName + "]."); } - // wrap XYPoint query as a ConstantScoreQuery - return getVectorQueryFromShape(shape, fieldName, relation, context); + final Consumer checker = t -> { + if (t == ShapeType.POINT || t == ShapeType.MULTIPOINT || t == ShapeType.LINESTRING || t == ShapeType.MULTILINESTRING) { + throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + t + " queries"); + } + }; + final XYGeometry[] luceneGeometries = LuceneGeometriesUtils.toXYGeometry(geometry, checker); + Query query = XYPointField.newGeometryQuery(fieldName, luceneGeometries); + if (hasDocValues) { + final Query queryDocValues = XYDocValuesField.newSlowGeometryQuery(fieldName, luceneGeometries); + query = new IndexOrDocValuesQuery(query, queryDocValues); + } + return query; } - private void validateIsPointFieldType(String fieldName, SearchExecutionContext context) { + private boolean validateIsPointFieldType(String fieldName, SearchExecutionContext context) { MappedFieldType fieldType = context.getFieldType(fieldName); if (fieldType instanceof PointFieldMapper.PointFieldType == false) { throw new QueryShardException( @@ -53,118 +52,6 @@ private void validateIsPointFieldType(String fieldName, SearchExecutionContext c "Expected " + PointFieldMapper.CONTENT_TYPE + " field type for Field [" + fieldName + "] but found " + fieldType.typeName() ); } - } - - protected Query getVectorQueryFromShape(Geometry queryShape, String fieldName, ShapeRelation relation, SearchExecutionContext context) { - ShapeVisitor shapeVisitor = new ShapeVisitor(context, fieldName, relation); - return queryShape.visit(shapeVisitor); - } - - private class ShapeVisitor implements GeometryVisitor { - SearchExecutionContext context; - MappedFieldType fieldType; - String fieldName; - ShapeRelation relation; - - ShapeVisitor(SearchExecutionContext context, String fieldName, ShapeRelation relation) { - this.context = context; - this.fieldType = context.getFieldType(fieldName); - this.fieldName = fieldName; - this.relation = relation; - } - - @Override - public Query visit(Circle circle) { - XYCircle xyCircle = ShapeUtils.toLuceneXYCircle(circle); - Query query = XYPointField.newDistanceQuery(fieldName, xyCircle.getX(), xyCircle.getY(), xyCircle.getRadius()); - if (fieldType.hasDocValues()) { - Query dvQuery = XYDocValuesField.newSlowDistanceQuery(fieldName, xyCircle.getX(), xyCircle.getY(), xyCircle.getRadius()); - query = new IndexOrDocValuesQuery(query, dvQuery); - } - return query; - } - - @Override - public Query visit(GeometryCollection collection) { - BooleanQuery.Builder bqb = new BooleanQuery.Builder(); - visit(bqb, collection); - return bqb.build(); - } - - private void visit(BooleanQuery.Builder bqb, GeometryCollection collection) { - BooleanClause.Occur occur = BooleanClause.Occur.FILTER; - for (Geometry shape : collection) { - bqb.add(shape.visit(this), occur); - } - } - - @Override - public Query visit(org.elasticsearch.geometry.Line line) { - throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + ShapeType.LINESTRING + " queries"); - } - - @Override - // don't think this is called directly - public Query visit(LinearRing ring) { - throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + ShapeType.LINEARRING + " queries"); - } - - @Override - public Query visit(MultiLine multiLine) { - throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + ShapeType.MULTILINESTRING + " queries"); - } - - @Override - public Query visit(MultiPoint multiPoint) { - throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + ShapeType.MULTIPOINT + " queries"); - } - - @Override - public Query visit(MultiPolygon multiPolygon) { - org.apache.lucene.geo.XYPolygon[] lucenePolygons = new org.apache.lucene.geo.XYPolygon[multiPolygon.size()]; - for (int i = 0; i < multiPolygon.size(); i++) { - lucenePolygons[i] = ShapeUtils.toLuceneXYPolygon(multiPolygon.get(i)); - } - Query query = XYPointField.newPolygonQuery(fieldName, lucenePolygons); - if (fieldType.hasDocValues()) { - Query dvQuery = XYDocValuesField.newSlowPolygonQuery(fieldName, lucenePolygons); - query = new IndexOrDocValuesQuery(query, dvQuery); - } - return query; - } - - @Override - public Query visit(Point point) { - // not currently supported - throw new QueryShardException(context, "Field [" + fieldName + "] does not support " + ShapeType.POINT + " queries"); - } - - @Override - public Query visit(Polygon polygon) { - org.apache.lucene.geo.XYPolygon lucenePolygon = ShapeUtils.toLuceneXYPolygon(polygon); - Query query = XYPointField.newPolygonQuery(fieldName, lucenePolygon); - if (fieldType.hasDocValues()) { - Query dvQuery = XYDocValuesField.newSlowPolygonQuery(fieldName, lucenePolygon); - query = new IndexOrDocValuesQuery(query, dvQuery); - } - return query; - } - - @Override - public Query visit(Rectangle r) { - XYRectangle xyRectangle = ShapeUtils.toLuceneXYRectangle(r); - Query query = XYPointField.newBoxQuery(fieldName, xyRectangle.minX, xyRectangle.maxX, xyRectangle.minY, xyRectangle.maxY); - if (fieldType.hasDocValues()) { - Query dvQuery = XYDocValuesField.newSlowBoxQuery( - fieldName, - xyRectangle.minX, - xyRectangle.maxX, - xyRectangle.minY, - xyRectangle.maxY - ); - query = new IndexOrDocValuesQuery(query, dvQuery); - } - return query; - } + return fieldType.hasDocValues(); } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java index ac526e6016b23..4bb9e988c0f90 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/query/ShapeQueryProcessor.java @@ -11,34 +11,20 @@ import org.apache.lucene.search.IndexOrDocValuesQuery; import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; +import org.elasticsearch.common.geo.LuceneGeometriesUtils; import org.elasticsearch.common.geo.ShapeRelation; -import org.elasticsearch.geometry.Circle; import org.elasticsearch.geometry.Geometry; -import org.elasticsearch.geometry.GeometryCollection; -import org.elasticsearch.geometry.GeometryVisitor; -import org.elasticsearch.geometry.Line; -import org.elasticsearch.geometry.LinearRing; -import org.elasticsearch.geometry.MultiLine; -import org.elasticsearch.geometry.MultiPoint; -import org.elasticsearch.geometry.MultiPolygon; -import org.elasticsearch.geometry.Point; -import org.elasticsearch.geometry.Polygon; -import org.elasticsearch.geometry.Rectangle; import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.query.QueryShardException; import org.elasticsearch.index.query.SearchExecutionContext; -import org.elasticsearch.xpack.spatial.common.ShapeUtils; import org.elasticsearch.xpack.spatial.index.mapper.CartesianShapeDocValuesQuery; import org.elasticsearch.xpack.spatial.index.mapper.ShapeFieldMapper; -import java.util.ArrayList; -import java.util.List; - public class ShapeQueryProcessor { public Query shapeQuery( - Geometry shape, + Geometry geometry, String fieldName, ShapeRelation relation, SearchExecutionContext context, @@ -49,10 +35,21 @@ public Query shapeQuery( if (relation == ShapeRelation.CONTAINS && context.indexVersionCreated().before(IndexVersions.V_7_5_0)) { throw new QueryShardException(context, ShapeRelation.CONTAINS + " query relation not supported for Field [" + fieldName + "]."); } - if (shape == null) { + if (geometry == null || geometry.isEmpty()) { return new MatchNoDocsQuery(); } - return getVectorQueryFromShape(shape, fieldName, relation, context, hasDocValues); + final XYGeometry[] luceneGeometries; + try { + luceneGeometries = LuceneGeometriesUtils.toXYGeometry(geometry, t -> {}); + } catch (IllegalArgumentException e) { + throw new QueryShardException(context, "Exception creating query on Field [" + fieldName + "] " + e.getMessage(), e); + } + Query query = XYShape.newGeometryQuery(fieldName, relation.getLuceneRelation(), luceneGeometries); + if (hasDocValues) { + final Query queryDocValues = new CartesianShapeDocValuesQuery(fieldName, relation.getLuceneRelation(), luceneGeometries); + query = new IndexOrDocValuesQuery(query, queryDocValues); + } + return query; } private void validateIsShapeFieldType(String fieldName, SearchExecutionContext context) { @@ -64,119 +61,4 @@ private void validateIsShapeFieldType(String fieldName, SearchExecutionContext c ); } } - - private Query getVectorQueryFromShape( - Geometry queryShape, - String fieldName, - ShapeRelation relation, - SearchExecutionContext context, - boolean hasDocValues - ) { - final LuceneGeometryCollector visitor = new LuceneGeometryCollector(fieldName, context); - queryShape.visit(visitor); - final List geomList = visitor.geometries(); - if (geomList.size() == 0) { - return new MatchNoDocsQuery(); - } - XYGeometry[] geometries = geomList.toArray(new XYGeometry[0]); - Query query = XYShape.newGeometryQuery(fieldName, relation.getLuceneRelation(), geometries); - if (hasDocValues) { - final Query queryDocValues = new CartesianShapeDocValuesQuery(fieldName, relation.getLuceneRelation(), geometries); - query = new IndexOrDocValuesQuery(query, queryDocValues); - } - return query; - } - - private static class LuceneGeometryCollector implements GeometryVisitor { - private final List geometries = new ArrayList<>(); - private final String name; - private final SearchExecutionContext context; - - private LuceneGeometryCollector(String name, SearchExecutionContext context) { - this.name = name; - this.context = context; - } - - List geometries() { - return geometries; - } - - @Override - public Void visit(Circle circle) { - if (circle.isEmpty() == false) { - geometries.add(ShapeUtils.toLuceneXYCircle(circle)); - } - return null; - } - - @Override - public Void visit(GeometryCollection collection) { - for (Geometry shape : collection) { - shape.visit(this); - } - return null; - } - - @Override - public Void visit(Line line) { - if (line.isEmpty() == false) { - geometries.add(ShapeUtils.toLuceneXYLine(line)); - } - return null; - } - - @Override - public Void visit(LinearRing ring) { - throw new QueryShardException(context, "Field [" + name + "] found and unsupported shape LinearRing"); - } - - @Override - public Void visit(MultiLine multiLine) { - for (Line line : multiLine) { - visit(line); - } - return null; - } - - @Override - public Void visit(MultiPoint multiPoint) { - for (Point point : multiPoint) { - visit(point); - } - return null; - } - - @Override - public Void visit(MultiPolygon multiPolygon) { - for (Polygon polygon : multiPolygon) { - visit(polygon); - } - return null; - } - - @Override - public Void visit(Point point) { - if (point.isEmpty() == false) { - geometries.add(ShapeUtils.toLuceneXYPoint(point)); - } - return null; - - } - - @Override - public Void visit(Polygon polygon) { - if (polygon.isEmpty() == false) { - geometries.add(ShapeUtils.toLuceneXYPolygon(polygon)); - } - return null; - } - - @Override - public Void visit(Rectangle r) { - if (r.isEmpty() == false) { - geometries.add(ShapeUtils.toLuceneXYRectangle(r)); - } - return null; - } - } } diff --git a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeDocValuesQueryTests.java b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeDocValuesQueryTests.java index ae5a6f182274b..f2148799d1b5f 100644 --- a/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeDocValuesQueryTests.java +++ b/x-pack/plugin/spatial/src/test/java/org/elasticsearch/xpack/spatial/index/mapper/CartesianShapeDocValuesQueryTests.java @@ -24,12 +24,12 @@ import org.apache.lucene.store.Directory; import org.apache.lucene.tests.search.CheckHits; import org.apache.lucene.tests.search.QueryUtils; +import org.elasticsearch.common.geo.LuceneGeometriesUtils; import org.elasticsearch.core.IOUtils; import org.elasticsearch.geo.ShapeTestUtils; import org.elasticsearch.geo.XShapeTestUtil; import org.elasticsearch.geometry.Geometry; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.spatial.common.ShapeUtils; import org.elasticsearch.xpack.spatial.index.fielddata.CoordinateEncoder; import java.io.IOException; @@ -41,7 +41,7 @@ public class CartesianShapeDocValuesQueryTests extends ESTestCase { private static final String FIELD_NAME = "field"; public void testEqualsAndHashcode() { - XYPolygon polygon = ShapeUtils.toLuceneXYPolygon(ShapeTestUtils.randomPolygon(false)); + XYPolygon polygon = LuceneGeometriesUtils.toXYPolygon(ShapeTestUtils.randomPolygon(false)); Query q1 = new CartesianShapeDocValuesQuery(FIELD_NAME, ShapeField.QueryRelation.INTERSECTS, polygon); Query q2 = new CartesianShapeDocValuesQuery(FIELD_NAME, ShapeField.QueryRelation.INTERSECTS, polygon); QueryUtils.checkEqual(q1, q2); @@ -160,9 +160,9 @@ private XYGeometry[] randomLuceneQueryGeometries() { private XYGeometry randomLuceneQueryGeometry() { return switch (randomInt(3)) { - case 0 -> ShapeUtils.toLuceneXYPolygon(ShapeTestUtils.randomPolygon(false)); - case 1 -> ShapeUtils.toLuceneXYCircle(ShapeTestUtils.randomCircle(false)); - case 2 -> ShapeUtils.toLuceneXYPoint(ShapeTestUtils.randomPoint(false)); + case 0 -> LuceneGeometriesUtils.toXYPolygon(ShapeTestUtils.randomPolygon(false)); + case 1 -> LuceneGeometriesUtils.toXYCircle(ShapeTestUtils.randomCircle(false)); + case 2 -> LuceneGeometriesUtils.toXYPoint(ShapeTestUtils.randomPoint(false)); default -> XShapeTestUtil.nextBox(); }; } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java index 7e20320d9d815..1d7a3cdd836ff 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java @@ -29,8 +29,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.core.Tuple; -import org.elasticsearch.search.aggregations.Aggregation; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.MultiBucketConsumerService; import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation.Bucket; @@ -215,7 +214,7 @@ public static SearchRequest prepareRequest(SearchSourceBuilder source, SqlConfig } protected static void logSearchResponse(SearchResponse response, Logger logger) { - List aggs = Collections.emptyList(); + List aggs = Collections.emptyList(); if (response.getAggregations() != null) { aggs = response.getAggregations().asList(); } @@ -405,9 +404,9 @@ protected void handleResponse(SearchResponse response, ActionListener list logSearchResponse(response, log); } - Aggregations aggs = response.getAggregations(); + InternalAggregations aggs = response.getAggregations(); if (aggs != null) { - Aggregation agg = aggs.get(Aggs.ROOT_GROUP_NAME); + InternalAggregation agg = aggs.get(Aggs.ROOT_GROUP_NAME); if (agg instanceof Filters filters) { handleBuckets(filters.getBuckets(), response); } else { diff --git a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/continuous/HistogramGroupByIT.java b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/continuous/HistogramGroupByIT.java index ccf9409d84bd8..797d592ef4571 100644 --- a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/continuous/HistogramGroupByIT.java +++ b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/continuous/HistogramGroupByIT.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.transform.integration.continuous; -import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.client.Response; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.core.Strings; @@ -29,7 +28,6 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThanOrEqualTo; -@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/97263") public class HistogramGroupByIT extends ContinuousTestCase { private static final String NAME = "continuous-histogram-pivot-test"; diff --git a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/continuous/TermsGroupByIT.java b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/continuous/TermsGroupByIT.java index 8ea0d5e62c6d3..f4b717a108762 100644 --- a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/continuous/TermsGroupByIT.java +++ b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/continuous/TermsGroupByIT.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.transform.integration.continuous; -import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.core.Strings; import org.elasticsearch.search.aggregations.AggregationBuilders; @@ -26,7 +25,6 @@ import static org.hamcrest.Matchers.equalTo; -@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/97263") public class TermsGroupByIT extends ContinuousTestCase { private static final String NAME = "continuous-terms-pivot-test"; diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/TransformUsageTransportAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/TransformUsageTransportAction.java index 2f3ed29ea08fc..d4e03475af22e 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/TransformUsageTransportAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/TransformUsageTransportAction.java @@ -23,7 +23,7 @@ import org.elasticsearch.persistent.PersistentTasksCustomMetadata; import org.elasticsearch.protocol.xpack.XPackUsageRequest; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.filter.Filters; import org.elasticsearch.search.aggregations.bucket.filter.FiltersAggregator; import org.elasticsearch.tasks.Task; @@ -180,7 +180,7 @@ protected void masterOperation( * @param aggs aggs returned by the search * @return feature usage map */ - private static Map getFeatureCounts(Aggregations aggs) { + private static Map getFeatureCounts(InternalAggregations aggs) { Filters filters = aggs.get(FEATURE_COUNTS); return filters.getBuckets().stream().collect(toMap(Filters.Bucket::getKeyAsString, Filters.Bucket::getDocCount)); } diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/common/AbstractCompositeAggFunction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/common/AbstractCompositeAggFunction.java index 189fb26e1f969..3412be813dcf6 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/common/AbstractCompositeAggFunction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/common/AbstractCompositeAggFunction.java @@ -20,7 +20,7 @@ import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; @@ -81,7 +81,7 @@ public void preview( buildSearchRequest(sourceConfig, timeout, numberOfBuckets), ActionListener.wrap(r -> { try { - final Aggregations aggregations = r.getAggregations(); + final InternalAggregations aggregations = r.getAggregations(); if (aggregations == null) { listener.onFailure( new ElasticsearchStatusException("Source indices have been deleted or closed.", RestStatus.BAD_REQUEST) @@ -158,7 +158,7 @@ public Tuple, Map> processSearchResponse( TransformIndexerStats stats, TransformProgress progress ) { - Aggregations aggregations = searchResponse.getAggregations(); + InternalAggregations aggregations = searchResponse.getAggregations(); // Treat this as a "we reached the end". // This should only happen when all underlying indices have gone away. Consequently, there is no more data to read. diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/pivot/CompositeBucketsChangeCollector.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/pivot/CompositeBucketsChangeCollector.java index 0636555459632..684e3a085405d 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/pivot/CompositeBucketsChangeCollector.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/pivot/CompositeBucketsChangeCollector.java @@ -22,7 +22,7 @@ import org.elasticsearch.index.query.TermsQueryBuilder; import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation.Bucket; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregationBuilder; @@ -92,7 +92,7 @@ interface FieldCollector { * * @return true if this collection is done and there are no more changes to look for */ - boolean collectChangesFromAggregations(Aggregations aggregations); + boolean collectChangesFromAggregations(InternalAggregations aggregations); /** * Return a composite value source builder if the collector requires it. @@ -248,7 +248,7 @@ public Collection aggregateChanges() { } @Override - public boolean collectChangesFromAggregations(Aggregations aggregations) { + public boolean collectChangesFromAggregations(InternalAggregations aggregations) { return true; } @@ -314,7 +314,7 @@ public Collection aggregateChanges() { } @Override - public boolean collectChangesFromAggregations(Aggregations aggregations) { + public boolean collectChangesFromAggregations(InternalAggregations aggregations) { return true; } @@ -401,7 +401,7 @@ public Collection aggregateChanges() { } @Override - public boolean collectChangesFromAggregations(Aggregations aggregations) { + public boolean collectChangesFromAggregations(InternalAggregations aggregations) { final SingleValue lowerBoundResult = aggregations.get(minAggregationOutputName); final SingleValue upperBoundResult = aggregations.get(maxAggregationOutputName); @@ -510,7 +510,7 @@ public Collection aggregateChanges() { } @Override - public boolean collectChangesFromAggregations(Aggregations aggregations) { + public boolean collectChangesFromAggregations(InternalAggregations aggregations) { final SingleValue lowerBoundResult = aggregations.get(minAggregationOutputName); final SingleValue upperBoundResult = aggregations.get(maxAggregationOutputName); @@ -659,7 +659,7 @@ public Collection aggregateChanges() { } @Override - public boolean collectChangesFromAggregations(Aggregations aggregations) { + public boolean collectChangesFromAggregations(InternalAggregations aggregations) { return true; } @@ -743,7 +743,7 @@ public Collection getIndicesToQuery(TransformCheckpoint lastCheckpoint, @Override public Map processSearchResponse(final SearchResponse searchResponse) { - final Aggregations aggregations = searchResponse.getAggregations(); + final InternalAggregations aggregations = searchResponse.getAggregations(); if (aggregations == null) { return null; } diff --git a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java index fe6a0b93ca7cd..ba5b97bbcb062 100644 --- a/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java +++ b/x-pack/plugin/vector-tile/src/main/java/org/elasticsearch/xpack/vectortile/rest/RestVectorTileAction.java @@ -34,7 +34,7 @@ import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.aggregations.Aggregation; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.MultiBucketConsumerService; import org.elasticsearch.search.aggregations.bucket.geogrid.GeoGridAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.geogrid.InternalGeoGrid; @@ -136,9 +136,9 @@ public RestResponse buildResponse(SearchResponse searchResponse) throws Exceptio final InternalGeoBounds bounds = searchResponse.getAggregations() != null ? searchResponse.getAggregations().get(BOUNDS_FIELD) : null; - final Aggregations aggsWithoutGridAndBounds = searchResponse.getAggregations() == null + final InternalAggregations aggsWithoutGridAndBounds = searchResponse.getAggregations() == null ? null - : new Aggregations( + : InternalAggregations.from( searchResponse.getAggregations() .asList() .stream() diff --git a/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateEmailMappingsTests.java b/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateEmailMappingsTests.java index edee4fb515a81..5b7ea39079f28 100644 --- a/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateEmailMappingsTests.java +++ b/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateEmailMappingsTests.java @@ -9,7 +9,7 @@ import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.protocol.xpack.watcher.PutWatchResponse; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.xpack.core.watcher.execution.ExecutionState; import org.elasticsearch.xpack.core.watcher.history.HistoryStoreField; @@ -106,7 +106,7 @@ public void testEmailFields() throws Exception { response -> { assertThat(response, notNullValue()); assertThat(response.getHits().getTotalHits().value, greaterThanOrEqualTo(1L)); - Aggregations aggs = response.getAggregations(); + InternalAggregations aggs = response.getAggregations(); assertThat(aggs, notNullValue()); Terms terms = aggs.get("from"); diff --git a/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateHttpMappingsTests.java b/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateHttpMappingsTests.java index 01400c3192289..97347de1ea23e 100644 --- a/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateHttpMappingsTests.java +++ b/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateHttpMappingsTests.java @@ -11,7 +11,7 @@ import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.protocol.xpack.watcher.PutWatchResponse; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.test.http.MockResponse; import org.elasticsearch.test.http.MockWebServer; @@ -103,7 +103,7 @@ public void testHttpFields() throws Exception { response -> { assertThat(response, notNullValue()); assertThat(response.getHits().getTotalHits().value, is(oneOf(1L, 2L))); - Aggregations aggs = response.getAggregations(); + InternalAggregations aggs = response.getAggregations(); assertThat(aggs, notNullValue()); Terms terms = aggs.get("input_result_path"); diff --git a/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateIndexActionMappingsTests.java b/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateIndexActionMappingsTests.java index 1f2810c4d82f3..7dde279fb90db 100644 --- a/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateIndexActionMappingsTests.java +++ b/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateIndexActionMappingsTests.java @@ -7,7 +7,7 @@ package org.elasticsearch.xpack.watcher.history; import org.elasticsearch.protocol.xpack.watcher.PutWatchResponse; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.xpack.core.watcher.execution.ExecutionState; import org.elasticsearch.xpack.core.watcher.history.HistoryStoreField; @@ -55,7 +55,7 @@ public void testIndexActionFields() throws Exception { response -> { assertThat(response, notNullValue()); assertThat(response.getHits().getTotalHits().value, is(oneOf(1L, 2L))); - Aggregations aggs = response.getAggregations(); + InternalAggregations aggs = response.getAggregations(); assertThat(aggs, notNullValue()); Terms terms = aggs.get("index_action_indices"); diff --git a/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateSearchInputMappingsTests.java b/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateSearchInputMappingsTests.java index 2c86df184dc22..567d4acfa45e5 100644 --- a/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateSearchInputMappingsTests.java +++ b/x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/history/HistoryTemplateSearchInputMappingsTests.java @@ -9,7 +9,7 @@ import org.elasticsearch.action.search.SearchType; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.protocol.xpack.watcher.PutWatchResponse; -import org.elasticsearch.search.aggregations.Aggregations; +import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.xpack.core.watcher.execution.ExecutionState; import org.elasticsearch.xpack.core.watcher.history.HistoryStoreField; @@ -73,7 +73,7 @@ public void testHttpFields() throws Exception { response -> { assertThat(response, notNullValue()); assertThat(response.getHits().getTotalHits().value, is(oneOf(1L, 2L))); - Aggregations aggs = response.getAggregations(); + InternalAggregations aggs = response.getAggregations(); assertThat(aggs, notNullValue()); Terms terms = aggs.get("input_search_type"); From 2846aa707fc0d954bfc31ee65c1d4b5a273613d6 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 1 Feb 2024 07:10:16 +0000 Subject: [PATCH 065/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-3d8ad990397 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 9b8d8497b0219..7942f8de859de 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-1e36b461474 +lucene = 9.10.0-snapshot-3d8ad990397 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index bbbf62bb7b252..a45ee4632d234 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 9a7de59c519d9fa728c0bc1a1c7bf92e551a4778 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 2 Feb 2024 07:09:49 +0000 Subject: [PATCH 066/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-4e73a4b2aca --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 7942f8de859de..0c652ca3d2d18 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-3d8ad990397 +lucene = 9.10.0-snapshot-4e73a4b2aca bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index a45ee4632d234..5f839142ddf05 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 38709e2330e38f679184d998c30739d2d4ae4421 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sat, 3 Feb 2024 07:09:31 +0000 Subject: [PATCH 067/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-70bab56f6fe --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 0c652ca3d2d18..70f9bae0dfa63 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-4e73a4b2aca +lucene = 9.10.0-snapshot-70bab56f6fe bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 5f839142ddf05..ce0542b3e6673 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 6deb746e1ff549627a710c955329d6bf7508b5f3 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sun, 4 Feb 2024 07:09:41 +0000 Subject: [PATCH 068/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-70bab56f6fe --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index ce0542b3e6673..6fd4c78907f28 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2635,122 +2635,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From f1213721dd6b7c051c7c8345f7564c7181e1bbf3 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 5 Feb 2024 07:09:56 +0000 Subject: [PATCH 069/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-3da32a257be --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 70f9bae0dfa63..217330652047b 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-70bab56f6fe +lucene = 9.10.0-snapshot-3da32a257be bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 6fd4c78907f28..fcb4fbcff17d0 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From c572ca1c9528134a0c6648aca98de7253e8b1002 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 6 Feb 2024 07:10:14 +0000 Subject: [PATCH 070/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-c4df3e13ad8 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 217330652047b..123e9ad257a6f 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-3da32a257be +lucene = 9.10.0-snapshot-c4df3e13ad8 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index fcb4fbcff17d0..85914977867d4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From e562d3b317d04c377f0638d3b983542dced49a5f Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 7 Feb 2024 07:09:42 +0000 Subject: [PATCH 071/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-c4df3e13ad8 --- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 85914977867d4..b1289882ddeed 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2635,122 +2635,122 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From e6f2e70f130abcd62c0e692217eb40feed2caf9c Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Wed, 7 Feb 2024 14:41:29 +0100 Subject: [PATCH 072/250] Fix compile failure For some reason #105014 is included only partially, probably a bad merge. --- .../org/elasticsearch/index/rankeval/RatedRequestsTests.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RatedRequestsTests.java b/modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RatedRequestsTests.java index d7e50fe4e1a8b..c5a09d67d94d0 100644 --- a/modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RatedRequestsTests.java +++ b/modules/rank-eval/src/test/java/org/elasticsearch/index/rankeval/RatedRequestsTests.java @@ -109,8 +109,6 @@ public static RatedRequest createTestItem(boolean forceRequest) { } public void testXContentRoundtrip() throws IOException { - assumeFalse("https://github.com/elastic/elasticsearch/issues/104570", Constants.WINDOWS); - RatedRequest testItem = createTestItem(randomBoolean()); XContentBuilder builder = XContentFactory.contentBuilder(randomFrom(XContentType.values())); XContentBuilder shuffled = shuffleXContent(testItem.toXContent(builder, ToXContent.EMPTY_PARAMS)); @@ -301,8 +299,6 @@ public void testProfileNotAllowed() { * matter for parsing xContent */ public void testParseFromXContent() throws IOException { - assumeFalse("https://github.com/elastic/elasticsearch/issues/104570", Constants.WINDOWS); - String querySpecString = """ { "id": "my_qa_query", From 4661023fa5265f7e0eb8924f0d86ef77bc256269 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 8 Feb 2024 07:09:50 +0000 Subject: [PATCH 073/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-f3e2929a52c --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 123e9ad257a6f..224859226e0da 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-c4df3e13ad8 +lucene = 9.10.0-snapshot-f3e2929a52c bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index b1289882ddeed..4a43364f53544 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 739072c00bc1e0fcb009de14aa7fa87cb124d1bf Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Thu, 8 Feb 2024 15:31:22 -0500 Subject: [PATCH 074/250] Fix compilation for ESKnnQuery objects (#105302) --- .../search/vectors/ESKnnByteVectorQuery.java | 18 ------------------ .../search/vectors/ESKnnFloatVectorQuery.java | 18 ------------------ 2 files changed, 36 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/vectors/ESKnnByteVectorQuery.java b/server/src/main/java/org/elasticsearch/search/vectors/ESKnnByteVectorQuery.java index 091ce6f8a0f6d..05cf52fd23f24 100644 --- a/server/src/main/java/org/elasticsearch/search/vectors/ESKnnByteVectorQuery.java +++ b/server/src/main/java/org/elasticsearch/search/vectors/ESKnnByteVectorQuery.java @@ -8,35 +8,17 @@ package org.elasticsearch.search.vectors; -import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.KnnByteVectorQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TopDocs; -import org.apache.lucene.search.TopDocsCollector; -import org.apache.lucene.util.Bits; import org.elasticsearch.search.profile.query.QueryProfiler; -import java.io.IOException; - public class ESKnnByteVectorQuery extends KnnByteVectorQuery implements ProfilingQuery { - private static final TopDocs NO_RESULTS = TopDocsCollector.EMPTY_TOPDOCS; private long vectorOpsCount; - private final byte[] target; public ESKnnByteVectorQuery(String field, byte[] target, int k, Query filter) { super(field, target, k, filter); - this.target = target; - } - - @Override - protected TopDocs approximateSearch(LeafReaderContext context, Bits acceptDocs, int visitedLimit) throws IOException { - // We increment visit limit by one to bypass a fencepost error in the collector - if (visitedLimit < Integer.MAX_VALUE) { - visitedLimit += 1; - } - TopDocs results = context.reader().searchNearestVectors(field, target, k, acceptDocs, visitedLimit); - return results != null ? results : NO_RESULTS; } @Override diff --git a/server/src/main/java/org/elasticsearch/search/vectors/ESKnnFloatVectorQuery.java b/server/src/main/java/org/elasticsearch/search/vectors/ESKnnFloatVectorQuery.java index 4fa4db1f4ea95..e83a90a3c4df8 100644 --- a/server/src/main/java/org/elasticsearch/search/vectors/ESKnnFloatVectorQuery.java +++ b/server/src/main/java/org/elasticsearch/search/vectors/ESKnnFloatVectorQuery.java @@ -8,24 +8,16 @@ package org.elasticsearch.search.vectors; -import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.KnnFloatVectorQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TopDocs; -import org.apache.lucene.search.TopDocsCollector; -import org.apache.lucene.util.Bits; import org.elasticsearch.search.profile.query.QueryProfiler; -import java.io.IOException; - public class ESKnnFloatVectorQuery extends KnnFloatVectorQuery implements ProfilingQuery { - private static final TopDocs NO_RESULTS = TopDocsCollector.EMPTY_TOPDOCS; private long vectorOpsCount; - private final float[] target; public ESKnnFloatVectorQuery(String field, float[] target, int k, Query filter) { super(field, target, k, filter); - this.target = target; } @Override @@ -35,16 +27,6 @@ protected TopDocs mergeLeafResults(TopDocs[] perLeafResults) { return topK; } - @Override - protected TopDocs approximateSearch(LeafReaderContext context, Bits acceptDocs, int visitedLimit) throws IOException { - // We increment visit limit by one to bypass a fencepost error in the collector - if (visitedLimit < Integer.MAX_VALUE) { - visitedLimit += 1; - } - TopDocs results = context.reader().searchNearestVectors(field, target, k, acceptDocs, visitedLimit); - return results != null ? results : NO_RESULTS; - } - @Override public void profile(QueryProfiler queryProfiler) { queryProfiler.setVectorOpsCount(vectorOpsCount); From 844e75fd3f1c9c67d47564ba1c88fbf4aa4bfa21 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 9 Feb 2024 07:09:53 +0000 Subject: [PATCH 075/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-06ee710c3c4 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 224859226e0da..687b1a3135bd9 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-f3e2929a52c +lucene = 9.10.0-snapshot-06ee710c3c4 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 4a43364f53544..92190d4ad7aa6 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 8c911a1a84659c2b8e21c00e8a9fddd6fd86f041 Mon Sep 17 00:00:00 2001 From: Mike Pellegrini Date: Fri, 9 Feb 2024 12:39:25 -0500 Subject: [PATCH 076/250] Added skeleton code for SemanticQueryBuilder --- .../queries/SemanticQueryBuilder.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.java 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 new file mode 100644 index 0000000000000..6808075ac69cb --- /dev/null +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/queries/SemanticQueryBuilder.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.xpack.inference.queries; + +import org.apache.lucene.search.Query; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.index.query.AbstractQueryBuilder; +import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +public class SemanticQueryBuilder extends AbstractQueryBuilder { + public static final String NAME = "semantic_query"; + + private static final ParseField QUERY_FIELD = new ParseField("query"); + + private final String fieldName; + private final String query; + + public SemanticQueryBuilder(String fieldName, String query) { + if (fieldName == null) { + throw new IllegalArgumentException("[" + NAME + "] requires a fieldName"); + } + if (query == null) { + throw new IllegalArgumentException("[" + NAME + "] requires a " + QUERY_FIELD.getPreferredName() + " value"); + } + this.fieldName = fieldName; + this.query = query; + } + + public SemanticQueryBuilder(StreamInput in) throws IOException { + super(in); + this.fieldName = in.readString(); + this.query = in.readString(); + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.SEMANTIC_TEXT_FIELD_ADDED; + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + out.writeString(fieldName); + out.writeString(query); + } + + @Override + protected void doXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(NAME); + builder.startObject(fieldName); + builder.field(QUERY_FIELD.getPreferredName(), query); + builder.endObject(); + builder.endObject(); + } + + @Override + protected Query doToQuery(SearchExecutionContext context) throws IOException { + // TODO: Implement + return null; + } + + @Override + protected boolean doEquals(SemanticQueryBuilder other) { + return Objects.equals(fieldName, other.fieldName) && Objects.equals(query, other.query); + } + + @Override + protected int doHashCode() { + return Objects.hash(fieldName, query); + } +} From 95c42fb0c6c5ef922ae520a9b7988799b82fd1af Mon Sep 17 00:00:00 2001 From: Mike Pellegrini Date: Fri, 9 Feb 2024 12:41:33 -0500 Subject: [PATCH 077/250] Add boost and query name to XContent --- .../xpack/inference/queries/SemanticQueryBuilder.java | 1 + 1 file changed, 1 insertion(+) 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 6808075ac69cb..c44a7125a6fd7 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 @@ -66,6 +66,7 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep builder.startObject(NAME); builder.startObject(fieldName); builder.field(QUERY_FIELD.getPreferredName(), query); + boostAndQueryNameToXContent(builder); builder.endObject(); builder.endObject(); } From 1be3e9a1b7cc7e3cdd9c238d3b1c5b7b91870fc7 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Sun, 11 Feb 2024 07:09:10 +0000 Subject: [PATCH 078/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-f4dbab4e10e --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 687b1a3135bd9..4a630e47dc5dd 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-06ee710c3c4 +lucene = 9.10.0-snapshot-f4dbab4e10e bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 92190d4ad7aa6..389e5c933dedf 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From 42b658570893abd3ec8f602bd7ab73c524710f94 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 12 Feb 2024 07:09:56 +0000 Subject: [PATCH 079/250] [Automated] Update Lucene snapshot to 9.10.0-snapshot-695c0ac8450 --- build-tools-internal/version.properties | 2 +- gradle/verification-metadata.xml | 144 ++++++++++++------------ 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 4a630e47dc5dd..645ae67927f6b 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.13.0 -lucene = 9.10.0-snapshot-f4dbab4e10e +lucene = 9.10.0-snapshot-695c0ac8450 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 389e5c933dedf..45c12ab8983e3 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2633,124 +2633,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From a55ce085e7b9f1b1e3eaf7df3e0f9559a2d76351 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Thu, 15 Feb 2024 22:23:45 +0100 Subject: [PATCH 080/250] Move to final 9.10.0 artifacts. --- build-tools-internal/version.properties | 2 +- build.gradle | 5 + gradle/verification-metadata.xml | 144 ++++++++++++------------ 3 files changed, 78 insertions(+), 73 deletions(-) diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index 46352fb23e164..6750ac59204ad 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -1,5 +1,5 @@ elasticsearch = 8.14.0 -lucene = 9.10.0-snapshot-695c0ac8450 +lucene = 9.10.0 bundled_jdk_vendor = openjdk bundled_jdk = 21.0.2+13@f2283984656d49d69e91c558476027ac diff --git a/build.gradle b/build.gradle index c0b613beefea4..e4e8d62766dcc 100644 --- a/build.gradle +++ b/build.gradle @@ -195,6 +195,11 @@ if (project.gradle.startParameter.taskNames.any { it.startsWith("checkPart") || subprojects { proj -> apply plugin: 'elasticsearch.base' + + repositories { + // TODO: Temporary for Lucene RC builds. REMOVE + maven { url "https://dist.apache.org/repos/dist/dev/lucene/lucene-9.10.0-RC1-rev-695c0ac84508438302cd346a812cfa2fdc5a10df/lucene/maven" } + } } allprojects { diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 24a65542caf35..56964aca95357 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2648,124 +2648,124 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From a04712b383928cde359bd1ef95256bbcb27f98d9 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Thu, 15 Feb 2024 22:27:09 +0100 Subject: [PATCH 081/250] Update docs/changelog/105578.yaml --- docs/changelog/105578.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/105578.yaml diff --git a/docs/changelog/105578.yaml b/docs/changelog/105578.yaml new file mode 100644 index 0000000000000..cbc58e9636a4d --- /dev/null +++ b/docs/changelog/105578.yaml @@ -0,0 +1,5 @@ +pr: 105578 +summary: Upgrade to Lucene 9.10.0 +area: Search +type: enhancement +issues: [] From 950d46a9d104b90646e590eca23441a1b90300e5 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Thu, 15 Feb 2024 22:33:09 +0100 Subject: [PATCH 082/250] Fix compilation. --- .../java/org/elasticsearch/index/mapper/FieldTypeTestCase.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/FieldTypeTestCase.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/FieldTypeTestCase.java index d1a07cd0ee089..d4c6f8f3df873 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/FieldTypeTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/FieldTypeTestCase.java @@ -146,7 +146,8 @@ public FieldInfo getFieldInfoWithName(String name) { 1, VectorEncoding.BYTE, VectorSimilarityFunction.COSINE, - randomBoolean() + randomBoolean(), + false ); } } From 98ceb06d9337abb69dd6a60ef949a33de7ab5607 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Tue, 20 Feb 2024 11:41:02 +0100 Subject: [PATCH 083/250] Remove temporary repository, go to Maven central instead. --- build.gradle | 5 ----- 1 file changed, 5 deletions(-) diff --git a/build.gradle b/build.gradle index e4e8d62766dcc..c0b613beefea4 100644 --- a/build.gradle +++ b/build.gradle @@ -195,11 +195,6 @@ if (project.gradle.startParameter.taskNames.any { it.startsWith("checkPart") || subprojects { proj -> apply plugin: 'elasticsearch.base' - - repositories { - // TODO: Temporary for Lucene RC builds. REMOVE - maven { url "https://dist.apache.org/repos/dist/dev/lucene/lucene-9.10.0-RC1-rev-695c0ac84508438302cd346a812cfa2fdc5a10df/lucene/maven" } - } } allprojects { From b8dc5c3041be8085f45cb2ff3cb7e01d5e65aa2c Mon Sep 17 00:00:00 2001 From: Panagiotis Bailis Date: Tue, 20 Feb 2024 13:50:02 +0200 Subject: [PATCH 084/250] Fix for SearchServiceTests#testWaitOnRefreshFailsIfCheckpointNotIndexed - increasing timeout for randomly failing test (#105395) --- .../java/org/elasticsearch/search/SearchServiceTests.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java index 551874a2d271a..b0c4ef00230d5 100644 --- a/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java +++ b/server/src/test/java/org/elasticsearch/search/SearchServiceTests.java @@ -1767,7 +1767,9 @@ public void testWaitOnRefreshFailsIfCheckpointNotIndexed() { final IndexService indexService = indicesService.indexServiceSafe(resolveIndex("index")); final IndexShard indexShard = indexService.getShard(0); SearchRequest searchRequest = new SearchRequest().allowPartialSearchResults(true); - searchRequest.setWaitForCheckpointsTimeout(TimeValue.timeValueMillis(randomIntBetween(10, 100))); + // Increased timeout to avoid cancelling the search task prior to its completion, + // as we expect to raise an Exception. Timeout itself is tested on the following `testWaitOnRefreshTimeout` test. + searchRequest.setWaitForCheckpointsTimeout(TimeValue.timeValueMillis(randomIntBetween(200, 300))); searchRequest.setWaitForCheckpoints(Collections.singletonMap("index", new long[] { 1 })); final DocWriteResponse response = prepareIndex("index").setSource("id", "1").get(); From 410efb6fb6d71433bc242d3aed5d3fa51a5acf29 Mon Sep 17 00:00:00 2001 From: Panagiotis Bailis Date: Tue, 20 Feb 2024 15:10:22 +0200 Subject: [PATCH 085/250] Fixing NPE when requesting [_none_] for stored_fields (#104711) --- docs/changelog/104711.yaml | 5 ++++ .../search/builder/SearchSourceBuilder.java | 8 +++++-- .../builder/SearchSourceBuilderTests.java | 23 +++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/104711.yaml diff --git a/docs/changelog/104711.yaml b/docs/changelog/104711.yaml new file mode 100644 index 0000000000000..f0f9bf7f10e45 --- /dev/null +++ b/docs/changelog/104711.yaml @@ -0,0 +1,5 @@ +pr: 104711 +summary: "Fixing NPE when requesting [_none_] for `stored_fields`" +area: Search +type: bug +issues: [] diff --git a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index 649c40c856fe8..72fd84cda760b 100644 --- a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -1337,7 +1337,10 @@ private SearchSourceBuilder parseXContent( SearchSourceBuilder.STORED_FIELDS_FIELD.getPreferredName(), parser ); - searchUsage.trackSectionUsage(STORED_FIELDS_FIELD.getPreferredName()); + if (storedFieldsContext.fetchFields() == false + || (storedFieldsContext.fieldNames() != null && storedFieldsContext.fieldNames().size() > 0)) { + searchUsage.trackSectionUsage(STORED_FIELDS_FIELD.getPreferredName()); + } } else if (SORT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { sort(parser.text()); } else if (PROFILE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { @@ -1493,7 +1496,8 @@ private SearchSourceBuilder parseXContent( } else if (token == XContentParser.Token.START_ARRAY) { if (STORED_FIELDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { storedFieldsContext = StoredFieldsContext.fromXContent(STORED_FIELDS_FIELD.getPreferredName(), parser); - if (storedFieldsContext.fieldNames().size() > 0 || storedFieldsContext.fetchFields() == false) { + if (storedFieldsContext.fetchFields() == false + || (storedFieldsContext.fieldNames() != null && storedFieldsContext.fieldNames().size() > 0)) { searchUsage.trackSectionUsage(STORED_FIELDS_FIELD.getPreferredName()); } } else if (DOCVALUE_FIELDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { diff --git a/server/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java b/server/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java index 8ee1c64ddbb22..26eefe850fc8f 100644 --- a/server/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java @@ -65,6 +65,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.function.ToLongFunction; @@ -592,6 +593,28 @@ public void testNegativeTrackTotalHits() throws IOException { } } + public void testStoredFieldsUsage() throws IOException { + Set storedFieldRestVariations = Set.of( + "{\"stored_fields\" : [\"_none_\"]}", + "{\"stored_fields\" : \"_none_\"}", + "{\"stored_fields\" : [\"field\"]}", + "{\"stored_fields\" : \"field\"}" + ); + for (String storedFieldRest : storedFieldRestVariations) { + SearchUsageHolder searchUsageHolder = new UsageService().getSearchUsageHolder(); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, storedFieldRest)) { + new SearchSourceBuilder().parseXContent(parser, true, searchUsageHolder); + SearchUsageStats searchUsageStats = searchUsageHolder.getSearchUsageStats(); + Map sectionsUsage = searchUsageStats.getSectionsUsage(); + assertEquals( + "Failed to correctly parse and record usage of '" + storedFieldRest + "'", + 1L, + sectionsUsage.get("stored_fields").longValue() + ); + } + } + } + public void testEmptySectionsAreNotTracked() throws IOException { SearchUsageHolder searchUsageHolder = new UsageService().getSearchUsageHolder(); From 7fb4b74b95da14e947c47d6ec3c2542e3bc1f1be Mon Sep 17 00:00:00 2001 From: Pat Whelan Date: Tue, 20 Feb 2024 08:22:27 -0500 Subject: [PATCH 086/250] [Transform] Test waits for next iteration (#105560) Currently, the `testStopWaitForCheckpoint` only verifies that the transform state is `stopped`, which might be the previous iteration's state. There is a small chance that we may exit the loop before the transform starts and stops for that iteration, where the test might fail the final `stopped` check. Now, we check the `trigger_count` to verify that the transform has at least had a chance to move from the `STARTED` state into the `INDEXING` and eventually `STOPPED` state before we finish the iteration. Fix #105388 Co-authored-by: Elastic Machine --- .../transform/integration/TransformIT.java | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformIT.java b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformIT.java index 394732742e528..073f604e608da 100644 --- a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformIT.java +++ b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformIT.java @@ -37,7 +37,9 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import static org.elasticsearch.core.Strings.format; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -375,7 +377,7 @@ public void testStopWaitForCheckpoint() throws Exception { // wait until transform has been triggered and indexed at least 1 document assertBusy(() -> { - var stateAndStats = getBasicTransformStats(config.getId()); + var stateAndStats = getBasicTransformStats(transformId); assertThat((Integer) XContentMapValues.extractValue("stats.documents_indexed", stateAndStats), greaterThan(1)); }); @@ -384,39 +386,51 @@ public void testStopWaitForCheckpoint() throws Exception { // Wait until the first checkpoint waitUntilCheckpoint(config.getId(), 1L); + var previousTriggerCount = new AtomicInteger(0); // Even though we are continuous, we should be stopped now as we needed to stop at the first checkpoint assertBusy(() -> { - var stateAndStats = getBasicTransformStats(config.getId()); + var stateAndStats = getBasicTransformStats(transformId); assertThat(stateAndStats.get("state"), equalTo("stopped")); assertThat((Integer) XContentMapValues.extractValue("stats.documents_indexed", stateAndStats), equalTo(1000)); + previousTriggerCount.set((int) XContentMapValues.extractValue("stats.trigger_count", stateAndStats)); }); + // Create N additional runs of starting and stopping int additionalRuns = randomIntBetween(1, 10); for (int i = 0; i < additionalRuns; ++i) { + var testFailureMessage = format("Can't determine if Transform ran for iteration number [%d] out of [%d].", i, additionalRuns); // index some more docs using a new user - long timeStamp = Instant.now().toEpochMilli() - 1_000; - long user = 42 + i; + var timeStamp = Instant.now().toEpochMilli() - 1_000; + var user = 42 + i; indexMoreDocs(timeStamp, user, indexName); - startTransformWithRetryOnConflict(config.getId(), RequestOptions.DEFAULT); + startTransformWithRetryOnConflict(transformId, RequestOptions.DEFAULT); - boolean waitForCompletion = randomBoolean(); - stopTransform(transformId, waitForCompletion, null, true); + assertBusy(() -> { + var stateAndStats = getBasicTransformStats(transformId); + var currentTriggerCount = (int) XContentMapValues.extractValue("stats.trigger_count", stateAndStats); + // We should verify that we are retrieving the stats *after* this run had been started. + // If the trigger_count has increased, we know we have started this test iteration. + assertThat(testFailureMessage, previousTriggerCount.get(), lessThan(currentTriggerCount)); + }); + var waitForCompletion = randomBoolean(); + stopTransform(transformId, waitForCompletion, null, true); assertBusy(() -> { - var stateAndStats = getBasicTransformStats(config.getId()); + var stateAndStats = getBasicTransformStats(transformId); assertThat(stateAndStats.get("state"), equalTo("stopped")); + previousTriggerCount.set((int) XContentMapValues.extractValue("stats.trigger_count", stateAndStats)); }); } - var stateAndStats = getBasicTransformStats(config.getId()); + var stateAndStats = getBasicTransformStats(transformId); assertThat(stateAndStats.get("state"), equalTo("stopped")); // Despite indexing new documents into the source index, the number of documents in the destination index stays the same. assertThat((Integer) XContentMapValues.extractValue("stats.documents_indexed", stateAndStats), equalTo(1000)); stopTransform(transformId); - deleteTransform(config.getId()); + deleteTransform(transformId); } public void testContinuousTransformRethrottle() throws Exception { From 0d0b319bf9714395aa7439b6fe8cea5d52a2bceb Mon Sep 17 00:00:00 2001 From: Panagiotis Bailis Date: Tue, 20 Feb 2024 16:28:27 +0200 Subject: [PATCH 087/250] Fixing compilation error in SearchSourceBuilderTests#testStoredFieldsUsage (#105656) --- .../elasticsearch/search/builder/SearchSourceBuilderTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java b/server/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java index 26eefe850fc8f..7b67bc5b94f7f 100644 --- a/server/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/search/builder/SearchSourceBuilderTests.java @@ -603,7 +603,7 @@ public void testStoredFieldsUsage() throws IOException { for (String storedFieldRest : storedFieldRestVariations) { SearchUsageHolder searchUsageHolder = new UsageService().getSearchUsageHolder(); try (XContentParser parser = createParser(JsonXContent.jsonXContent, storedFieldRest)) { - new SearchSourceBuilder().parseXContent(parser, true, searchUsageHolder); + new SearchSourceBuilder().parseXContent(parser, true, searchUsageHolder, nf -> false); SearchUsageStats searchUsageStats = searchUsageHolder.getSearchUsageStats(); Map sectionsUsage = searchUsageStats.getSectionsUsage(); assertEquals( From 5920c917aa933bf8078e3c88f43c217957bf9dd0 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Tue, 20 Feb 2024 15:53:14 +0100 Subject: [PATCH 088/250] Encapsulate Mapper.Builder#name and make it private (#105648) This is in preparation to make the field mutable, which is needed in the context of https://github.com/elastic/elasticsearch/pull/103542 --- .../legacygeo/mapper/LegacyGeoShapeFieldMapper.java | 4 ++-- .../index/mapper/extras/MatchOnlyTextFieldMapper.java | 4 ++-- .../index/mapper/extras/RankFeatureFieldMapper.java | 4 ++-- .../index/mapper/extras/RankFeaturesFieldMapper.java | 4 ++-- .../index/mapper/extras/ScaledFloatFieldMapper.java | 4 ++-- .../mapper/extras/SearchAsYouTypeFieldMapper.java | 8 ++++---- .../index/mapper/extras/TokenCountFieldMapper.java | 6 +++--- .../join/mapper/ParentJoinFieldMapper.java | 8 ++++---- .../percolator/PercolatorFieldMapper.java | 4 ++-- .../analysis/icu/ICUCollationKeywordFieldMapper.java | 4 ++-- .../mapper/annotatedtext/AnnotatedTextFieldMapper.java | 6 +++--- .../index/mapper/murmur3/Murmur3FieldMapper.java | 4 ++-- .../elasticsearch/index/mapper/BinaryFieldMapper.java | 4 ++-- .../elasticsearch/index/mapper/BooleanFieldMapper.java | 6 +++--- .../index/mapper/CompletionFieldMapper.java | 4 ++-- .../elasticsearch/index/mapper/DateFieldMapper.java | 4 ++-- .../index/mapper/GeoPointFieldMapper.java | 10 +++++----- .../index/mapper/GeoShapeFieldMapper.java | 6 +++--- .../org/elasticsearch/index/mapper/IpFieldMapper.java | 6 +++--- .../elasticsearch/index/mapper/KeywordFieldMapper.java | 8 ++++---- .../java/org/elasticsearch/index/mapper/Mapper.java | 2 +- .../elasticsearch/index/mapper/NestedObjectMapper.java | 6 +++--- .../elasticsearch/index/mapper/NumberFieldMapper.java | 6 +++--- .../org/elasticsearch/index/mapper/ObjectMapper.java | 6 +++--- .../index/mapper/PassThroughObjectMapper.java | 6 +++--- .../index/mapper/PlaceHolderFieldMapper.java | 4 ++-- .../elasticsearch/index/mapper/RangeFieldMapper.java | 4 ++-- .../elasticsearch/index/mapper/RootObjectMapper.java | 2 +- .../elasticsearch/index/mapper/TextFieldMapper.java | 10 +++++----- .../index/mapper/flattened/FlattenedFieldMapper.java | 8 ++++---- .../index/mapper/vectors/DenseVectorFieldMapper.java | 4 ++-- .../index/mapper/vectors/SparseVectorFieldMapper.java | 4 ++-- .../index/mapper/ParametrizedMapperTests.java | 2 +- .../xpack/analytics/mapper/HistogramFieldMapper.java | 4 ++-- .../mapper/AggregateDoubleMetricFieldMapper.java | 6 +++--- .../mapper/ConstantKeywordFieldMapper.java | 4 ++-- .../countedkeyword/CountedKeywordFieldMapper.java | 6 +++--- .../xpack/unsignedlong/UnsignedLongFieldMapper.java | 4 ++-- .../xpack/versionfield/VersionStringFieldMapper.java | 4 ++-- .../index/mapper/GeoShapeWithDocValuesFieldMapper.java | 8 ++++---- .../xpack/spatial/index/mapper/PointFieldMapper.java | 6 +++--- .../xpack/spatial/index/mapper/ShapeFieldMapper.java | 4 ++-- .../xpack/wildcard/mapper/WildcardFieldMapper.java | 4 ++-- 43 files changed, 111 insertions(+), 111 deletions(-) diff --git a/modules/legacy-geo/src/main/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldMapper.java b/modules/legacy-geo/src/main/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldMapper.java index afd969cc17ad4..4ef2b2e07bb26 100644 --- a/modules/legacy-geo/src/main/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldMapper.java +++ b/modules/legacy-geo/src/main/java/org/elasticsearch/legacygeo/mapper/LegacyGeoShapeFieldMapper.java @@ -324,7 +324,7 @@ private static void setupPrefixTrees(GeoShapeFieldType ft) { private GeoShapeFieldType buildFieldType(LegacyGeoShapeParser parser, MapperBuilderContext context) { GeoShapeFieldType ft = new GeoShapeFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexed.get(), orientation.get().value(), parser, @@ -353,7 +353,7 @@ private static int getLevels(int treeLevels, double precisionInMeters, int defau public LegacyGeoShapeFieldMapper build(MapperBuilderContext context) { LegacyGeoShapeParser parser = new LegacyGeoShapeParser(); GeoShapeFieldType ft = buildFieldType(parser, context); - return new LegacyGeoShapeFieldMapper(name, ft, multiFieldsBuilder.build(this, context), copyTo, parser, this); + return new LegacyGeoShapeFieldMapper(name(), ft, multiFieldsBuilder.build(this, context), copyTo, parser, this); } } diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapper.java index fa83e2600de9b..a965b9a2bbce4 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/MatchOnlyTextFieldMapper.java @@ -127,7 +127,7 @@ private MatchOnlyTextFieldType buildFieldType(MapperBuilderContext context) { NamedAnalyzer indexAnalyzer = analyzers.getIndexAnalyzer(); TextSearchInfo tsi = new TextSearchInfo(Defaults.FIELD_TYPE, null, searchAnalyzer, searchQuoteAnalyzer); MatchOnlyTextFieldType ft = new MatchOnlyTextFieldType( - context.buildFullName(name), + context.buildFullName(name()), tsi, indexAnalyzer, context.isSourceSynthetic(), @@ -140,7 +140,7 @@ private MatchOnlyTextFieldType buildFieldType(MapperBuilderContext context) { public MatchOnlyTextFieldMapper build(MapperBuilderContext context) { MatchOnlyTextFieldType tft = buildFieldType(context); MultiFields multiFields = multiFieldsBuilder.build(this, context); - return new MatchOnlyTextFieldMapper(name, Defaults.FIELD_TYPE, tft, multiFields, copyTo, context.isSourceSynthetic(), this); + return new MatchOnlyTextFieldMapper(name(), Defaults.FIELD_TYPE, tft, multiFields, copyTo, context.isSourceSynthetic(), this); } } diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureFieldMapper.java index b5a5ce87d5096..f63f290bf58fc 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureFieldMapper.java @@ -92,9 +92,9 @@ protected Parameter[] getParameters() { @Override public RankFeatureFieldMapper build(MapperBuilderContext context) { return new RankFeatureFieldMapper( - name, + name(), new RankFeatureFieldType( - context.buildFullName(name), + context.buildFullName(name()), meta.getValue(), positiveScoreImpact.getValue(), nullValue.getValue() diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeaturesFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeaturesFieldMapper.java index f36dfb5605633..5f0d44d1fb796 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeaturesFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeaturesFieldMapper.java @@ -64,8 +64,8 @@ protected Parameter[] getParameters() { @Override public RankFeaturesFieldMapper build(MapperBuilderContext context) { return new RankFeaturesFieldMapper( - name, - new RankFeaturesFieldType(context.buildFullName(name), meta.getValue(), positiveScoreImpact.getValue()), + name(), + new RankFeaturesFieldType(context.buildFullName(name()), meta.getValue(), positiveScoreImpact.getValue()), multiFieldsBuilder.build(this, context), copyTo, positiveScoreImpact.getValue() diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/ScaledFloatFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/ScaledFloatFieldMapper.java index cc2ceb3c017ba..e2b932b01a516 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/ScaledFloatFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/ScaledFloatFieldMapper.java @@ -186,7 +186,7 @@ protected Parameter[] getParameters() { @Override public ScaledFloatFieldMapper build(MapperBuilderContext context) { ScaledFloatFieldType type = new ScaledFloatFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexed.getValue(), stored.getValue(), hasDocValues.getValue(), @@ -196,7 +196,7 @@ public ScaledFloatFieldMapper build(MapperBuilderContext context) { metric.getValue(), indexMode ); - return new ScaledFloatFieldMapper(name, type, multiFieldsBuilder.build(this, context), copyTo, this); + return new ScaledFloatFieldMapper(name(), type, multiFieldsBuilder.build(this, context), copyTo, this); } } diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/SearchAsYouTypeFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/SearchAsYouTypeFieldMapper.java index ca8231c46736f..a5e011d5772f0 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/SearchAsYouTypeFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/SearchAsYouTypeFieldMapper.java @@ -187,7 +187,7 @@ public SearchAsYouTypeFieldMapper build(MapperBuilderContext context) { NamedAnalyzer searchAnalyzer = analyzers.getSearchAnalyzer(); SearchAsYouTypeFieldType ft = new SearchAsYouTypeFieldType( - context.buildFullName(name), + context.buildFullName(name()), fieldType, similarity.getValue(), analyzers.getSearchAnalyzer(), @@ -202,7 +202,7 @@ public SearchAsYouTypeFieldMapper build(MapperBuilderContext context) { prefixft.setIndexOptions(fieldType.indexOptions()); prefixft.setOmitNorms(true); prefixft.setStored(false); - final String fullName = context.buildFullName(name); + final String fullName = context.buildFullName(name()); // wrap the root field's index analyzer with shingles and edge ngrams final Analyzer prefixIndexWrapper = SearchAsYouTypeAnalyzer.withShingleAndPrefix( indexAnalyzer.analyzer(), @@ -228,7 +228,7 @@ public SearchAsYouTypeFieldMapper build(MapperBuilderContext context) { final int shingleSize = i + 2; FieldType shingleft = new FieldType(fieldType); shingleft.setStored(false); - String fieldName = getShingleFieldName(context.buildFullName(name), shingleSize); + String fieldName = getShingleFieldName(context.buildFullName(name()), shingleSize); // wrap the root field's index, search, and search quote analyzers with shingles final SearchAsYouTypeAnalyzer shingleIndexWrapper = SearchAsYouTypeAnalyzer.withShingle( indexAnalyzer.analyzer(), @@ -260,7 +260,7 @@ public SearchAsYouTypeFieldMapper build(MapperBuilderContext context) { ft.setPrefixField(prefixFieldType); ft.setShingleFields(shingleFieldTypes); return new SearchAsYouTypeFieldMapper( - name, + name(), ft, copyTo, indexAnalyzers, diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/TokenCountFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/TokenCountFieldMapper.java index 4d04e83361252..831306a8e8594 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/TokenCountFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/TokenCountFieldMapper.java @@ -77,17 +77,17 @@ protected Parameter[] getParameters() { @Override public TokenCountFieldMapper build(MapperBuilderContext context) { if (analyzer.getValue() == null) { - throw new MapperParsingException("Analyzer must be set for field [" + name + "] but wasn't."); + throw new MapperParsingException("Analyzer must be set for field [" + name() + "] but wasn't."); } MappedFieldType ft = new TokenCountFieldType( - context.buildFullName(name), + context.buildFullName(name()), index.getValue(), store.getValue(), hasDocValues.getValue(), nullValue.getValue(), meta.getValue() ); - return new TokenCountFieldMapper(name, ft, multiFieldsBuilder.build(this, context), copyTo, this); + return new TokenCountFieldMapper(name(), ft, multiFieldsBuilder.build(this, context), copyTo, this); } } diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/mapper/ParentJoinFieldMapper.java b/modules/parent-join/src/main/java/org/elasticsearch/join/mapper/ParentJoinFieldMapper.java index 2bbd5e81444b7..d6b7ccad4f3c5 100644 --- a/modules/parent-join/src/main/java/org/elasticsearch/join/mapper/ParentJoinFieldMapper.java +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/mapper/ParentJoinFieldMapper.java @@ -112,16 +112,16 @@ protected Parameter[] getParameters() { @Override public ParentJoinFieldMapper build(MapperBuilderContext context) { - checkObjectOrNested(context, name); + checkObjectOrNested(context, name()); final Map parentIdFields = new HashMap<>(); relations.get() .stream() - .map(relation -> new ParentIdFieldMapper(name + "#" + relation.parent(), eagerGlobalOrdinals.get())) + .map(relation -> new ParentIdFieldMapper(name() + "#" + relation.parent(), eagerGlobalOrdinals.get())) .forEach(mapper -> parentIdFields.put(mapper.name(), mapper)); Joiner joiner = new Joiner(name(), relations.get()); return new ParentJoinFieldMapper( - name, - new JoinFieldType(context.buildFullName(name), joiner, meta.get()), + name(), + new JoinFieldType(context.buildFullName(name()), joiner, meta.get()), Collections.unmodifiableMap(parentIdFields), eagerGlobalOrdinals.get(), relations.get() diff --git a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java index be8d342254afd..7ba83f9ce71b5 100644 --- a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java +++ b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java @@ -135,10 +135,10 @@ protected Parameter[] getParameters() { @Override public PercolatorFieldMapper build(MapperBuilderContext context) { - PercolatorFieldType fieldType = new PercolatorFieldType(context.buildFullName(name), meta.getValue()); + PercolatorFieldType fieldType = new PercolatorFieldType(context.buildFullName(name()), meta.getValue()); // TODO should percolator even allow multifields? MultiFields multiFields = multiFieldsBuilder.build(this, context); - context = context.createChildContext(name); + context = context.createChildContext(name()); KeywordFieldMapper extractedTermsField = createExtractQueryFieldBuilder( EXTRACTED_TERMS_FIELD_NAME, context, diff --git a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/ICUCollationKeywordFieldMapper.java b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/ICUCollationKeywordFieldMapper.java index 19f1d0455630d..1da274ff236da 100644 --- a/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/ICUCollationKeywordFieldMapper.java +++ b/plugins/analysis-icu/src/main/java/org/elasticsearch/plugin/analysis/icu/ICUCollationKeywordFieldMapper.java @@ -327,7 +327,7 @@ public ICUCollationKeywordFieldMapper build(MapperBuilderContext context) { final CollatorParams params = collatorParams(); final Collator collator = params.buildCollator(); CollationFieldType ft = new CollationFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexed.getValue(), stored.getValue(), hasDocValues.getValue(), @@ -337,7 +337,7 @@ public ICUCollationKeywordFieldMapper build(MapperBuilderContext context) { meta.getValue() ); return new ICUCollationKeywordFieldMapper( - name, + name(), buildFieldType(), ft, multiFieldsBuilder.build(this, context), diff --git a/plugins/mapper-annotated-text/src/main/java/org/elasticsearch/index/mapper/annotatedtext/AnnotatedTextFieldMapper.java b/plugins/mapper-annotated-text/src/main/java/org/elasticsearch/index/mapper/annotatedtext/AnnotatedTextFieldMapper.java index 7153fcf4d46b3..fae2ab19aee39 100644 --- a/plugins/mapper-annotated-text/src/main/java/org/elasticsearch/index/mapper/annotatedtext/AnnotatedTextFieldMapper.java +++ b/plugins/mapper-annotated-text/src/main/java/org/elasticsearch/index/mapper/annotatedtext/AnnotatedTextFieldMapper.java @@ -122,7 +122,7 @@ private AnnotatedTextFieldType buildFieldType(FieldType fieldType, MapperBuilder wrapAnalyzer(analyzers.getSearchQuoteAnalyzer()) ); return new AnnotatedTextFieldType( - context.buildFullName(name), + context.buildFullName(name()), store.getValue(), tsi, context.isSourceSynthetic(), @@ -139,12 +139,12 @@ public AnnotatedTextFieldMapper build(MapperBuilderContext context) { if (analyzers.positionIncrementGap.isConfigured()) { if (fieldType.indexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS) < 0) { throw new IllegalArgumentException( - "Cannot set position_increment_gap on field [" + name + "] without positions enabled" + "Cannot set position_increment_gap on field [" + name() + "] without positions enabled" ); } } return new AnnotatedTextFieldMapper( - name, + name(), fieldType, buildFieldType(fieldType, context), multiFieldsBuilder.build(this, context), diff --git a/plugins/mapper-murmur3/src/main/java/org/elasticsearch/index/mapper/murmur3/Murmur3FieldMapper.java b/plugins/mapper-murmur3/src/main/java/org/elasticsearch/index/mapper/murmur3/Murmur3FieldMapper.java index c1e2888c47c62..08a133bcb69c8 100644 --- a/plugins/mapper-murmur3/src/main/java/org/elasticsearch/index/mapper/murmur3/Murmur3FieldMapper.java +++ b/plugins/mapper-murmur3/src/main/java/org/elasticsearch/index/mapper/murmur3/Murmur3FieldMapper.java @@ -55,8 +55,8 @@ protected Parameter[] getParameters() { @Override public Murmur3FieldMapper build(MapperBuilderContext context) { return new Murmur3FieldMapper( - name, - new Murmur3FieldType(context.buildFullName(name), stored.getValue(), meta.getValue()), + name(), + new Murmur3FieldType(context.buildFullName(name()), stored.getValue(), meta.getValue()), multiFieldsBuilder.build(this, context), copyTo ); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/BinaryFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/BinaryFieldMapper.java index 403156c95540e..948baf0dff830 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/BinaryFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/BinaryFieldMapper.java @@ -64,8 +64,8 @@ public Parameter[] getParameters() { @Override public BinaryFieldMapper build(MapperBuilderContext context) { return new BinaryFieldMapper( - name, - new BinaryFieldType(context.buildFullName(name), stored.getValue(), hasDocValues.getValue(), meta.getValue()), + name(), + new BinaryFieldType(context.buildFullName(name()), stored.getValue(), hasDocValues.getValue(), meta.getValue()), multiFieldsBuilder.build(this, context), copyTo, this diff --git a/server/src/main/java/org/elasticsearch/index/mapper/BooleanFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/BooleanFieldMapper.java index 43e6e662dc8f2..cc01a487ad7b8 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/BooleanFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/BooleanFieldMapper.java @@ -115,7 +115,7 @@ protected Parameter[] getParameters() { @Override public BooleanFieldMapper build(MapperBuilderContext context) { MappedFieldType ft = new BooleanFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexed.getValue() && indexCreatedVersion.isLegacyIndexVersion() == false, stored.getValue(), docValues.getValue(), @@ -123,7 +123,7 @@ public BooleanFieldMapper build(MapperBuilderContext context) { scriptValues(), meta.getValue() ); - return new BooleanFieldMapper(name, ft, multiFieldsBuilder.build(this, context), copyTo, context.isSourceSynthetic(), this); + return new BooleanFieldMapper(name(), ft, multiFieldsBuilder.build(this, context), copyTo, context.isSourceSynthetic(), this); } private FieldValues scriptValues() { @@ -133,7 +133,7 @@ private FieldValues scriptValues() { BooleanFieldScript.Factory scriptFactory = scriptCompiler.compile(script.get(), BooleanFieldScript.CONTEXT); return scriptFactory == null ? null - : (lookup, ctx, doc, consumer) -> scriptFactory.newFactory(name, script.get().getParams(), lookup, OnScriptError.FAIL) + : (lookup, ctx, doc, consumer) -> scriptFactory.newFactory(name(), script.get().getParams(), lookup, OnScriptError.FAIL) .newInstance(ctx) .runForDoc(doc, consumer); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java index 94b937c534491..5d5ef076852a8 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/CompletionFieldMapper.java @@ -205,9 +205,9 @@ public CompletionFieldMapper build(MapperBuilderContext context) { new CompletionAnalyzer(this.searchAnalyzer.getValue(), preserveSeparators.getValue(), preservePosInc.getValue()) ); - CompletionFieldType ft = new CompletionFieldType(context.buildFullName(name), completionAnalyzer, meta.getValue()); + CompletionFieldType ft = new CompletionFieldType(context.buildFullName(name()), completionAnalyzer, meta.getValue()); ft.setContextMappings(contexts.getValue()); - return new CompletionFieldMapper(name, ft, multiFieldsBuilder.build(this, context), copyTo, this); + return new CompletionFieldMapper(name(), ft, multiFieldsBuilder.build(this, context), copyTo, this); } private void checkCompletionContextsLimit() { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java index 0c54b58aae0e3..1b926734c1713 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DateFieldMapper.java @@ -306,7 +306,7 @@ private FieldValues scriptValues() { return factory == null ? null : (lookup, ctx, doc, consumer) -> factory.newFactory( - name, + name(), script.get().getParams(), lookup, buildFormatter(), @@ -364,7 +364,7 @@ public DateFieldMapper build(MapperBuilderContext context) { && ignoreMalformed.isConfigured() == false) { ignoreMalformed.setValue(false); } - return new DateFieldMapper(name, ft, multiFieldsBuilder.build(this, context), copyTo, nullTimestamp, resolution, this); + return new DateFieldMapper(name(), ft, multiFieldsBuilder.build(this, context), copyTo, nullTimestamp, resolution, this); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoPointFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoPointFieldMapper.java index 4effc380646ff..85a9b8377e6f0 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoPointFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoPointFieldMapper.java @@ -186,7 +186,7 @@ private FieldValues scriptValues() { GeoPointFieldScript.Factory factory = scriptCompiler.compile(this.script.get(), GeoPointFieldScript.CONTEXT); return factory == null ? null - : (lookup, ctx, doc, consumer) -> factory.newFactory(name, script.get().getParams(), lookup, OnScriptError.FAIL) + : (lookup, ctx, doc, consumer) -> factory.newFactory(name(), script.get().getParams(), lookup, OnScriptError.FAIL) .newInstance(ctx) .runForDoc(doc, consumer); } @@ -194,7 +194,7 @@ private FieldValues scriptValues() { @Override public FieldMapper build(MapperBuilderContext context) { Parser geoParser = new GeoPointParser( - name, + name(), (parser) -> GeoUtils.parseGeoPoint(parser, ignoreZValue.get().value()), nullValue.get(), ignoreZValue.get().value(), @@ -202,7 +202,7 @@ public FieldMapper build(MapperBuilderContext context) { metric.get() != TimeSeriesParams.MetricType.POSITION ); GeoPointFieldType ft = new GeoPointFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexed.get() && indexCreatedVersion.isLegacyIndexVersion() == false, stored.get(), hasDocValues.get(), @@ -214,9 +214,9 @@ public FieldMapper build(MapperBuilderContext context) { indexMode ); if (this.script.get() == null) { - return new GeoPointFieldMapper(name, ft, multiFieldsBuilder.build(this, context), copyTo, geoParser, this); + return new GeoPointFieldMapper(name(), ft, multiFieldsBuilder.build(this, context), copyTo, geoParser, this); } - return new GeoPointFieldMapper(name, ft, geoParser, this); + return new GeoPointFieldMapper(name(), ft, geoParser, this); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java index e39684705e26a..541538f65a550 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/GeoShapeFieldMapper.java @@ -99,18 +99,18 @@ public GeoShapeFieldMapper build(MapperBuilderContext context) { ); GeoShapeParser geoShapeParser = new GeoShapeParser(geometryParser, orientation.get().value()); GeoShapeFieldType ft = new GeoShapeFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexed.get(), orientation.get().value(), geoShapeParser, meta.get() ); return new GeoShapeFieldMapper( - name, + name(), ft, multiFieldsBuilder.build(this, context), copyTo, - new GeoShapeIndexer(orientation.get().value(), context.buildFullName(name)), + new GeoShapeIndexer(orientation.get().value(), context.buildFullName(name())), geoShapeParser, this ); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java index 8ce726b49ff66..355b38d4dcb96 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java @@ -154,7 +154,7 @@ private FieldValues scriptValues() { IpFieldScript.Factory factory = scriptCompiler.compile(this.script.get(), IpFieldScript.CONTEXT); return factory == null ? null - : (lookup, ctx, doc, consumer) -> factory.newFactory(name, script.get().getParams(), lookup, OnScriptError.FAIL) + : (lookup, ctx, doc, consumer) -> factory.newFactory(name(), script.get().getParams(), lookup, OnScriptError.FAIL) .newInstance(ctx) .runForDoc(doc, consumer); } @@ -170,9 +170,9 @@ public IpFieldMapper build(MapperBuilderContext context) { dimension.setValue(true); } return new IpFieldMapper( - name, + name(), new IpFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexed.getValue() && indexCreatedVersion.isLegacyIndexVersion() == false, stored.getValue(), hasDocValues.getValue(), diff --git a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java index a5a571fb82d85..06e689784b087 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java @@ -254,7 +254,7 @@ private FieldValues scriptValues() { StringFieldScript.Factory scriptFactory = scriptCompiler.compile(script.get(), StringFieldScript.CONTEXT); return scriptFactory == null ? null - : (lookup, ctx, doc, consumer) -> scriptFactory.newFactory(name, script.get().getParams(), lookup, OnScriptError.FAIL) + : (lookup, ctx, doc, consumer) -> scriptFactory.newFactory(name(), script.get().getParams(), lookup, OnScriptError.FAIL) .newInstance(ctx) .runForDoc(doc, consumer); } @@ -294,7 +294,7 @@ private KeywordFieldType buildFieldType(MapperBuilderContext context, FieldType ); normalizer = Lucene.KEYWORD_ANALYZER; } else { - throw new MapperParsingException("normalizer [" + normalizerName + "] not found for field [" + name + "]"); + throw new MapperParsingException("normalizer [" + normalizerName + "] not found for field [" + name() + "]"); } } searchAnalyzer = quoteAnalyzer = normalizer; @@ -308,7 +308,7 @@ private KeywordFieldType buildFieldType(MapperBuilderContext context, FieldType dimension(true); } return new KeywordFieldType( - context.buildFullName(name), + context.buildFullName(name()), fieldType, normalizer, searchAnalyzer, @@ -330,7 +330,7 @@ public KeywordFieldMapper build(MapperBuilderContext context) { fieldtype = Defaults.FIELD_TYPE; } return new KeywordFieldMapper( - name, + name(), fieldtype, buildFieldType(context, fieldtype), multiFieldsBuilder.build(this, context), 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 397f99f63030c..cf4025150584f 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java @@ -24,7 +24,7 @@ public abstract class Mapper implements ToXContentFragment, Iterable { public abstract static class Builder { - protected final String name; + private final String name; protected Builder(String name) { this.name = internFieldName(name); 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 a654819811621..1216618b1e986 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java @@ -62,8 +62,8 @@ public NestedObjectMapper build(MapperBuilderContext context) { this.includeInRoot = Explicit.IMPLICIT_FALSE; } } - NestedMapperBuilderContext nestedContext = new NestedMapperBuilderContext(context.buildFullName(name), parentIncludedInRoot); - final String fullPath = context.buildFullName(name); + NestedMapperBuilderContext nestedContext = new NestedMapperBuilderContext(context.buildFullName(name()), parentIncludedInRoot); + final String fullPath = context.buildFullName(name()); final String nestedTypePath; if (indexCreatedVersion.before(IndexVersions.V_8_0_0)) { nestedTypePath = "__" + fullPath; @@ -71,7 +71,7 @@ public NestedObjectMapper build(MapperBuilderContext context) { nestedTypePath = fullPath; } return new NestedObjectMapper( - name, + name(), fullPath, buildMappers(nestedContext), enabled, diff --git a/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java index 5935eaf2c3d14..2245e527c2aa2 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/NumberFieldMapper.java @@ -227,7 +227,7 @@ private FieldValues scriptValues() { if (this.script.get() == null) { return null; } - return type.compile(name, script.get(), scriptCompiler); + return type.compile(name(), script.get(), scriptCompiler); } public Builder dimension(boolean dimension) { @@ -271,8 +271,8 @@ public NumberFieldMapper build(MapperBuilderContext context) { dimension.setValue(true); } - MappedFieldType ft = new NumberFieldType(context.buildFullName(name), this); - return new NumberFieldMapper(name, ft, multiFieldsBuilder.build(this, context), copyTo, context.isSourceSynthetic(), this); + MappedFieldType ft = new NumberFieldType(context.buildFullName(name()), this); + return new NumberFieldMapper(name(), ft, multiFieldsBuilder.build(this, context), copyTo, context.isSourceSynthetic(), this); } } 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 7a807f767611b..a9de4bdd1467a 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java @@ -172,12 +172,12 @@ protected final Map buildMappers(MapperBuilderContext mapperBuil @Override public ObjectMapper build(MapperBuilderContext context) { return new ObjectMapper( - name, - context.buildFullName(name), + name(), + context.buildFullName(name()), enabled, subobjects, dynamic, - buildMappers(context.createChildContext(name)) + buildMappers(context.createChildContext(name())) ); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/PassThroughObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/PassThroughObjectMapper.java index b49c9328fcc79..4ce7f51ed7386 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/PassThroughObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/PassThroughObjectMapper.java @@ -56,11 +56,11 @@ public PassThroughObjectMapper.Builder setContainsDimensions() { @Override public PassThroughObjectMapper build(MapperBuilderContext context) { return new PassThroughObjectMapper( - name, - context.buildFullName(name), + name(), + context.buildFullName(name()), enabled, dynamic, - buildMappers(context.createChildContext(name)), + buildMappers(context.createChildContext(name())), timeSeriesDimensionSubFields ); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/PlaceHolderFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/PlaceHolderFieldMapper.java index 98f8f21be704a..67260273bc5a5 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/PlaceHolderFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/PlaceHolderFieldMapper.java @@ -90,8 +90,8 @@ protected Parameter[] getParameters() { @Override public PlaceHolderFieldMapper build(MapperBuilderContext context) { - PlaceHolderFieldType mappedFieldType = new PlaceHolderFieldType(context.buildFullName(name), type, Map.of()); - return new PlaceHolderFieldMapper(name, mappedFieldType, multiFieldsBuilder.build(this, context), copyTo, unknownParams); + PlaceHolderFieldType mappedFieldType = new PlaceHolderFieldType(context.buildFullName(name()), type, Map.of()); + return new PlaceHolderFieldMapper(name(), mappedFieldType, multiFieldsBuilder.build(this, context), copyTo, unknownParams); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java index fcd2a425a6625..3836915e65753 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RangeFieldMapper.java @@ -116,7 +116,7 @@ protected Parameter[] getParameters() { } protected RangeFieldType setupFieldType(MapperBuilderContext context) { - String fullName = context.buildFullName(name); + String fullName = context.buildFullName(name()); if (format.isConfigured()) { if (type != RangeType.DATE) { throw new IllegalArgumentException( @@ -163,7 +163,7 @@ protected RangeFieldType setupFieldType(MapperBuilderContext context) { @Override public RangeFieldMapper build(MapperBuilderContext context) { RangeFieldType ft = setupFieldType(context); - return new RangeFieldMapper(name, ft, multiFieldsBuilder.build(this, context), copyTo, type, this); + return new RangeFieldMapper(name(), ft, multiFieldsBuilder.build(this, context), copyTo, type, this); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java index d7cc9e8f7e71f..a730d8c2da89e 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java @@ -114,7 +114,7 @@ public RootObjectMapper build(MapperBuilderContext context) { Map mappers = buildMappers(context); mappers.putAll(getAliasMappers(mappers, context)); return new RootObjectMapper( - name, + name(), enabled, subobjects, dynamic, diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java index 1885869073711..faa840dacc732 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TextFieldMapper.java @@ -355,18 +355,18 @@ private TextFieldType buildFieldType( if (analyzers.positionIncrementGap.isConfigured()) { if (fieldType.indexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS) < 0) { throw new IllegalArgumentException( - "Cannot set position_increment_gap on field [" + name + "] without positions enabled" + "Cannot set position_increment_gap on field [" + name() + "] without positions enabled" ); } } TextSearchInfo tsi = new TextSearchInfo(fieldType, similarity.getValue(), searchAnalyzer, searchQuoteAnalyzer); TextFieldType ft; if (indexCreatedVersion.isLegacyIndexVersion()) { - ft = new LegacyTextFieldType(context.buildFullName(name), index.getValue(), store.getValue(), tsi, meta.getValue()); + ft = new LegacyTextFieldType(context.buildFullName(name()), index.getValue(), store.getValue(), tsi, meta.getValue()); // ignore fieldData and eagerGlobalOrdinals } else { ft = new TextFieldType( - context.buildFullName(name), + context.buildFullName(name()), index.getValue(), store.getValue(), tsi, @@ -412,7 +412,7 @@ private SubFieldInfo buildPrefixInfo(MapperBuilderContext context, FieldType fie * or a multi-field). This way search will continue to work on old indices and new indices * will use the expected full name. */ - String fullName = indexCreatedVersion.before(IndexVersions.V_7_2_1) ? name() : context.buildFullName(name); + String fullName = indexCreatedVersion.before(IndexVersions.V_7_2_1) ? name() : context.buildFullName(name()); // Copy the index options of the main field to allow phrase queries on // the prefix field. FieldType pft = new FieldType(fieldType); @@ -476,7 +476,7 @@ public TextFieldMapper build(MapperBuilderContext context) { throw new MapperParsingException("Cannot use reserved field name [" + mapper.name() + "]"); } } - return new TextFieldMapper(name, fieldType, tft, prefixFieldInfo, phraseFieldInfo, multiFields, copyTo, this); + return new TextFieldMapper(name(), fieldType, tft, prefixFieldInfo, phraseFieldInfo, multiFields, copyTo, this); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java index c15adfb3be116..5a8efb6c8ed59 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/flattened/FlattenedFieldMapper.java @@ -202,13 +202,13 @@ protected Parameter[] getParameters() { public FlattenedFieldMapper build(MapperBuilderContext context) { MultiFields multiFields = multiFieldsBuilder.build(this, context); if (multiFields.iterator().hasNext()) { - throw new IllegalArgumentException(CONTENT_TYPE + " field [" + name + "] does not support [fields]"); + throw new IllegalArgumentException(CONTENT_TYPE + " field [" + name() + "] does not support [fields]"); } if (copyTo.copyToFields().isEmpty() == false) { - throw new IllegalArgumentException(CONTENT_TYPE + " field [" + name + "] does not support [copy_to]"); + throw new IllegalArgumentException(CONTENT_TYPE + " field [" + name() + "] does not support [copy_to]"); } MappedFieldType ft = new RootFlattenedFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexed.get(), hasDocValues.get(), meta.get(), @@ -216,7 +216,7 @@ public FlattenedFieldMapper build(MapperBuilderContext context) { eagerGlobalOrdinals.get(), dimensions.get() ); - return new FlattenedFieldMapper(name, ft, this); + return new FlattenedFieldMapper(name(), ft, this); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java index d36ca9e0b25c1..598a6383bfdaa 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java @@ -213,9 +213,9 @@ protected Parameter[] getParameters() { @Override public DenseVectorFieldMapper build(MapperBuilderContext context) { return new DenseVectorFieldMapper( - name, + name(), new DenseVectorFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexVersionCreated, elementType.getValue(), dims.getValue(), 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 3b892fc1647b6..6532abed19044 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 @@ -66,8 +66,8 @@ protected Parameter[] getParameters() { @Override public SparseVectorFieldMapper build(MapperBuilderContext context) { return new SparseVectorFieldMapper( - name, - new SparseVectorFieldType(context.buildFullName(name), meta.getValue()), + name(), + new SparseVectorFieldType(context.buildFullName(name()), meta.getValue()), multiFieldsBuilder.build(this, context), copyTo ); diff --git a/server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java index 562a30ba4f389..b1b7f80ba865f 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/ParametrizedMapperTests.java @@ -175,7 +175,7 @@ protected Parameter[] getParameters() { @Override public FieldMapper build(MapperBuilderContext context) { - return new TestMapper(name(), context.buildFullName(name), multiFieldsBuilder.build(this, context), copyTo, this); + return new TestMapper(name(), context.buildFullName(name()), multiFieldsBuilder.build(this, context), copyTo, this); } } diff --git a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java index 421973723837d..b8e4f77f7da7b 100644 --- a/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java +++ b/x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/mapper/HistogramFieldMapper.java @@ -94,8 +94,8 @@ protected Parameter[] getParameters() { @Override public HistogramFieldMapper build(MapperBuilderContext context) { return new HistogramFieldMapper( - name, - new HistogramFieldType(context.buildFullName(name), meta.getValue()), + name(), + new HistogramFieldType(context.buildFullName(name()), meta.getValue()), multiFieldsBuilder.build(this, context), copyTo, this diff --git a/x-pack/plugin/mapper-aggregate-metric/src/main/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapper.java b/x-pack/plugin/mapper-aggregate-metric/src/main/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapper.java index b5c35e758a65c..1581803920cdc 100644 --- a/x-pack/plugin/mapper-aggregate-metric/src/main/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapper.java +++ b/x-pack/plugin/mapper-aggregate-metric/src/main/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldMapper.java @@ -209,7 +209,7 @@ public AggregateDoubleMetricFieldMapper build(MapperBuilderContext context) { EnumMap metricMappers = new EnumMap<>(Metric.class); // Instantiate one NumberFieldMapper instance for each metric for (Metric m : this.metrics.getValue()) { - String fieldName = subfieldName(name, m); + String fieldName = subfieldName(name(), m); NumberFieldMapper.Builder builder; if (m == Metric.value_count) { @@ -245,14 +245,14 @@ public AggregateDoubleMetricFieldMapper build(MapperBuilderContext context) { }, () -> new EnumMap<>(Metric.class))); AggregateDoubleMetricFieldType metricFieldType = new AggregateDoubleMetricFieldType( - context.buildFullName(name), + context.buildFullName(name()), meta.getValue(), timeSeriesMetric.getValue() ); metricFieldType.setMetricFields(metricFields); metricFieldType.setDefaultMetric(defaultMetric.getValue()); - return new AggregateDoubleMetricFieldMapper(name, metricFieldType, metricMappers, this); + return new AggregateDoubleMetricFieldMapper(name(), metricFieldType, metricMappers, this); } } diff --git a/x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java b/x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java index cee397d906149..f2b1f013212db 100644 --- a/x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java +++ b/x-pack/plugin/mapper-constant-keyword/src/main/java/org/elasticsearch/xpack/constantkeyword/mapper/ConstantKeywordFieldMapper.java @@ -99,8 +99,8 @@ protected Parameter[] getParameters() { @Override public ConstantKeywordFieldMapper build(MapperBuilderContext context) { return new ConstantKeywordFieldMapper( - name, - new ConstantKeywordFieldType(context.buildFullName(name), value.getValue(), meta.getValue()) + name(), + new ConstantKeywordFieldType(context.buildFullName(name()), value.getValue(), meta.getValue()) ); } } diff --git a/x-pack/plugin/mapper-counted-keyword/src/main/java/org/elasticsearch/xpack/countedkeyword/CountedKeywordFieldMapper.java b/x-pack/plugin/mapper-counted-keyword/src/main/java/org/elasticsearch/xpack/countedkeyword/CountedKeywordFieldMapper.java index ad5e224efd5db..878a949a69841 100644 --- a/x-pack/plugin/mapper-counted-keyword/src/main/java/org/elasticsearch/xpack/countedkeyword/CountedKeywordFieldMapper.java +++ b/x-pack/plugin/mapper-counted-keyword/src/main/java/org/elasticsearch/xpack/countedkeyword/CountedKeywordFieldMapper.java @@ -289,14 +289,14 @@ protected Parameter[] getParameters() { @Override public FieldMapper build(MapperBuilderContext context) { - BinaryFieldMapper countFieldMapper = new BinaryFieldMapper.Builder(name + COUNT_FIELD_NAME_SUFFIX, true).build(context); + BinaryFieldMapper countFieldMapper = new BinaryFieldMapper.Builder(name() + COUNT_FIELD_NAME_SUFFIX, true).build(context); boolean isIndexed = indexed.getValue(); FieldType ft = isIndexed ? FIELD_TYPE_INDEXED : FIELD_TYPE_NOT_INDEXED; return new CountedKeywordFieldMapper( - name, + name(), ft, new CountedKeywordFieldType( - context.buildFullName(name), + context.buildFullName(name()), isIndexed, false, true, diff --git a/x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java b/x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java index c468d7bcd6718..955d658b01bab 100644 --- a/x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java +++ b/x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java @@ -199,7 +199,7 @@ public UnsignedLongFieldMapper build(MapperBuilderContext context) { dimension.setValue(true); } UnsignedLongFieldType fieldType = new UnsignedLongFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexed.getValue(), stored.getValue(), hasDocValues.getValue(), @@ -209,7 +209,7 @@ public UnsignedLongFieldMapper build(MapperBuilderContext context) { metric.getValue(), indexMode ); - return new UnsignedLongFieldMapper(name, fieldType, multiFieldsBuilder.build(this, context), copyTo, this); + return new UnsignedLongFieldMapper(name(), fieldType, multiFieldsBuilder.build(this, context), copyTo, this); } } diff --git a/x-pack/plugin/mapper-version/src/main/java/org/elasticsearch/xpack/versionfield/VersionStringFieldMapper.java b/x-pack/plugin/mapper-version/src/main/java/org/elasticsearch/xpack/versionfield/VersionStringFieldMapper.java index e233df2af3fbd..40b8bcf208a2d 100644 --- a/x-pack/plugin/mapper-version/src/main/java/org/elasticsearch/xpack/versionfield/VersionStringFieldMapper.java +++ b/x-pack/plugin/mapper-version/src/main/java/org/elasticsearch/xpack/versionfield/VersionStringFieldMapper.java @@ -112,14 +112,14 @@ static class Builder extends FieldMapper.Builder { } private VersionStringFieldType buildFieldType(MapperBuilderContext context, FieldType fieldtype) { - return new VersionStringFieldType(context.buildFullName(name), fieldtype, meta.getValue()); + return new VersionStringFieldType(context.buildFullName(name()), fieldtype, meta.getValue()); } @Override public VersionStringFieldMapper build(MapperBuilderContext context) { FieldType fieldtype = new FieldType(Defaults.FIELD_TYPE); return new VersionStringFieldMapper( - name, + name(), fieldtype, buildFieldType(context, fieldtype), multiFieldsBuilder.build(this, context), diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/GeoShapeWithDocValuesFieldMapper.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/GeoShapeWithDocValuesFieldMapper.java index 71fb9b0f3126a..a8f437f476ada 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/GeoShapeWithDocValuesFieldMapper.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/GeoShapeWithDocValuesFieldMapper.java @@ -173,7 +173,7 @@ private FieldValues scriptValues() { GeometryFieldScript.Factory factory = scriptCompiler.compile(this.script.get(), GeometryFieldScript.CONTEXT); return factory == null ? null - : (lookup, ctx, doc, consumer) -> factory.newFactory(name, script.get().getParams(), lookup, OnScriptError.FAIL) + : (lookup, ctx, doc, consumer) -> factory.newFactory(name(), script.get().getParams(), lookup, OnScriptError.FAIL) .newInstance(ctx) .runForDoc(doc, consumer); } @@ -194,7 +194,7 @@ public GeoShapeWithDocValuesFieldMapper build(MapperBuilderContext context) { ); GeoShapeParser parser = new GeoShapeParser(geometryParser, orientation.get().value()); GeoShapeWithDocValuesFieldType ft = new GeoShapeWithDocValuesFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexed.get(), hasDocValues.get(), stored.get(), @@ -206,7 +206,7 @@ public GeoShapeWithDocValuesFieldMapper build(MapperBuilderContext context) { ); if (script.get() == null) { return new GeoShapeWithDocValuesFieldMapper( - name, + name(), ft, multiFieldsBuilder.build(this, context), copyTo, @@ -216,7 +216,7 @@ public GeoShapeWithDocValuesFieldMapper build(MapperBuilderContext context) { ); } return new GeoShapeWithDocValuesFieldMapper( - name, + name(), ft, multiFieldsBuilder.build(this, context), copyTo, diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java index 01a2b5f0e5598..1657a3bf7fbce 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/PointFieldMapper.java @@ -105,14 +105,14 @@ public FieldMapper build(MapperBuilderContext context) { ); } CartesianPointParser parser = new CartesianPointParser( - name, + name(), p -> CartesianPoint.parsePoint(p, ignoreZValue.get().value()), nullValue.get(), ignoreZValue.get().value(), ignoreMalformed.get().value() ); PointFieldType ft = new PointFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexed.get(), stored.get(), hasDocValues.get(), @@ -120,7 +120,7 @@ public FieldMapper build(MapperBuilderContext context) { nullValue.get(), meta.get() ); - return new PointFieldMapper(name, ft, multiFieldsBuilder.build(this, context), copyTo, parser, this); + return new PointFieldMapper(name(), ft, multiFieldsBuilder.build(this, context), copyTo, parser, this); } } diff --git a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapper.java b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapper.java index 0a1c0278d88d7..83e434f829591 100644 --- a/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapper.java +++ b/x-pack/plugin/spatial/src/main/java/org/elasticsearch/xpack/spatial/index/mapper/ShapeFieldMapper.java @@ -118,14 +118,14 @@ public ShapeFieldMapper build(MapperBuilderContext context) { ); Parser parser = new ShapeParser(geometryParser); ShapeFieldType ft = new ShapeFieldType( - context.buildFullName(name), + context.buildFullName(name()), indexed.get(), hasDocValues.get(), orientation.get().value(), parser, meta.get() ); - return new ShapeFieldMapper(name, ft, multiFieldsBuilder.build(this, context), copyTo, parser, this); + return new ShapeFieldMapper(name(), ft, multiFieldsBuilder.build(this, context), copyTo, parser, this); } } diff --git a/x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java b/x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java index 1954e291b1a7f..62306a18d946b 100644 --- a/x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java +++ b/x-pack/plugin/wildcard/src/main/java/org/elasticsearch/xpack/wildcard/mapper/WildcardFieldMapper.java @@ -240,8 +240,8 @@ Builder nullValue(String nullValue) { @Override public WildcardFieldMapper build(MapperBuilderContext context) { return new WildcardFieldMapper( - name, - new WildcardFieldType(context.buildFullName(name), nullValue.get(), ignoreAbove.get(), indexVersionCreated, meta.get()), + name(), + new WildcardFieldType(context.buildFullName(name()), nullValue.get(), ignoreAbove.get(), indexVersionCreated, meta.get()), ignoreAbove.get(), context.isSourceSynthetic(), multiFieldsBuilder.build(this, context), From 2dc9e89ed65dc2ecddedc87ae6e88a0c846e4fb7 Mon Sep 17 00:00:00 2001 From: Luca Cavanna Date: Tue, 20 Feb 2024 17:35:06 +0100 Subject: [PATCH 089/250] Make name getter method in Mapper.Builder final (#105661) FieldAliasMapper used to override it but it would not change any behaviour, it can rather call the existing getter for it. --- .../elasticsearch/index/mapper/FieldAliasMapper.java | 10 ++-------- .../java/org/elasticsearch/index/mapper/Mapper.java | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) 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 97d1b9368a6c9..8aa29e6317d51 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldAliasMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldAliasMapper.java @@ -138,16 +138,10 @@ public boolean supportsVersion(IndexVersion indexCreatedVersion) { } public static class Builder extends Mapper.Builder { - private String name; private String path; protected Builder(String name) { super(name); - this.name = name; - } - - public String name() { - return this.name; } public Builder path(String path) { @@ -157,8 +151,8 @@ public Builder path(String path) { @Override public FieldAliasMapper build(MapperBuilderContext context) { - String fullName = context.buildFullName(name); - return new FieldAliasMapper(name, fullName, path); + String fullName = context.buildFullName(name()); + return new FieldAliasMapper(name(), fullName, path); } } 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 cf4025150584f..14a71531c6abb 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java @@ -31,7 +31,7 @@ protected Builder(String name) { } // TODO rename this to leafName? - public String name() { + public final String name() { return this.name; } From ac08fe6076b12fa223e8a204e62bf59085ed0634 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 20 Feb 2024 17:13:49 +0000 Subject: [PATCH 090/250] Fix HTTP corner-case response leaks (#105617) Enhances `Netty4PipeliningIT` to demonstrate that the pipelined requests do run concurrently, and to explore some corner cases around failures (both client-side and server-side). This extra testing found two response leaks: one when the channel has closed before even starting to process a request, and a second when we throw an exception during serialization of a chunk in a chunked response with other pipelined responses enqueued for transmission behind it. --- docs/changelog/105617.yaml | 5 + .../http/netty4/Netty4PipeliningIT.java | 245 ++++++++++++++++-- .../netty4/Netty4HttpPipeliningHandler.java | 92 +++---- .../http/netty4/Netty4HttpClient.java | 28 +- .../http/AbstractHttpServerTransport.java | 9 +- .../rest/action/RestActionListener.java | 4 +- 6 files changed, 308 insertions(+), 75 deletions(-) create mode 100644 docs/changelog/105617.yaml diff --git a/docs/changelog/105617.yaml b/docs/changelog/105617.yaml new file mode 100644 index 0000000000000..7fd8203336fff --- /dev/null +++ b/docs/changelog/105617.yaml @@ -0,0 +1,5 @@ +pr: 105617 +summary: Fix HTTP corner-case response leaks +area: Network +type: bug +issues: [] diff --git a/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/http/netty4/Netty4PipeliningIT.java b/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/http/netty4/Netty4PipeliningIT.java index b381e0ea8bfb7..653733b064ba9 100644 --- a/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/http/netty4/Netty4PipeliningIT.java +++ b/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/http/netty4/Netty4PipeliningIT.java @@ -8,43 +8,134 @@ package org.elasticsearch.http.netty4; -import io.netty.handler.codec.http.FullHttpResponse; import io.netty.util.ReferenceCounted; +import org.apache.lucene.util.BytesRef; import org.elasticsearch.ESNetty4IntegTestCase; -import org.elasticsearch.common.transport.TransportAddress; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.CountDownActionListener; +import org.elasticsearch.action.support.SubscribableListener; +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.bytes.ReleasableBytesReference; +import org.elasticsearch.common.bytes.ZeroBytesReference; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.recycler.Recycler; +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.common.unit.ByteSizeUnit; +import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.core.Strings; +import org.elasticsearch.features.NodeFeature; import org.elasticsearch.http.HttpServerTransport; -import org.elasticsearch.test.ESIntegTestCase.ClusterScope; -import org.elasticsearch.test.ESIntegTestCase.Scope; +import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.ChunkedRestResponseBody; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xcontent.ToXContentObject; +import java.io.IOException; +import java.util.Arrays; import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; +import java.util.function.Supplier; +import static org.elasticsearch.http.HttpTransportSettings.SETTING_PIPELINING_MAX_EVENTS; +import static org.elasticsearch.rest.RestRequest.Method.GET; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; -@ClusterScope(scope = Scope.TEST, supportsDedicatedMasters = false, numDataNodes = 1) +@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST) public class Netty4PipeliningIT extends ESNetty4IntegTestCase { + @Override + protected Collection> nodePlugins() { + return CollectionUtils.concatLists(List.of(CountDown3Plugin.class, ChunkAndFailPlugin.class), super.nodePlugins()); + } + + private static final int MAX_PIPELINE_EVENTS = 10; + + @Override + protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { + return Settings.builder() + .put(super.nodeSettings(nodeOrdinal, otherSettings)) + .put(SETTING_PIPELINING_MAX_EVENTS.getKey(), MAX_PIPELINE_EVENTS) + .build(); + } + @Override protected boolean addMockHttpTransport() { return false; // enable http } public void testThatNettyHttpServerSupportsPipelining() throws Exception { - String[] requests = new String[] { "/", "/_nodes/stats", "/", "/_cluster/state", "/" }; + runPipeliningTest( + CountDown3Plugin.ROUTE, + "/_nodes", + "/_nodes/stats", + CountDown3Plugin.ROUTE, + "/_cluster/health", + "/_cluster/state", + CountDown3Plugin.ROUTE, + "/_cat/shards" + ); + } + + public void testChunkingFailures() throws Exception { + runPipeliningTest(0, ChunkAndFailPlugin.randomRequestUri()); + runPipeliningTest(0, ChunkAndFailPlugin.randomRequestUri(), "/_cluster/state"); + runPipeliningTest( + -1, // typically get the first 2 responses, but we can hit the failing chunk and close the channel soon enough to lose them too + CountDown3Plugin.ROUTE, + CountDown3Plugin.ROUTE, + ChunkAndFailPlugin.randomRequestUri(), + "/_cluster/health", + CountDown3Plugin.ROUTE + ); + } - HttpServerTransport httpServerTransport = internalCluster().getInstance(HttpServerTransport.class); - TransportAddress[] boundAddresses = httpServerTransport.boundAddress().boundAddresses(); - TransportAddress transportAddress = randomFrom(boundAddresses); + public void testPipelineOverflow() throws Exception { + final var routes = new String[1 // the first request which never returns a response so doesn't consume a spot in the queue + + MAX_PIPELINE_EVENTS // the responses which fill up the queue + + 1 // to cause the overflow + + between(0, 5) // for good measure, to e.g. make sure we don't leak these responses + ]; + Arrays.fill(routes, "/_cluster/health"); + routes[0] = CountDown3Plugin.ROUTE; // never returns + runPipeliningTest(0, routes); + } - try (Netty4HttpClient nettyHttpClient = new Netty4HttpClient()) { - Collection responses = nettyHttpClient.get(transportAddress.address(), requests); - try { - assertThat(responses, hasSize(5)); + private void runPipeliningTest(String... routes) throws InterruptedException { + runPipeliningTest(routes.length, routes); + } - Collection opaqueIds = Netty4HttpClient.returnOpaqueIds(responses); - assertOpaqueIdsInOrder(opaqueIds); + private void runPipeliningTest(int expectedResponseCount, String... routes) throws InterruptedException { + try (var client = new Netty4HttpClient()) { + final var responses = client.get( + randomFrom(internalCluster().getInstance(HttpServerTransport.class).boundAddress().boundAddresses()).address(), + routes + ); + try { + logger.info("response codes: {}", responses.stream().mapToInt(r -> r.status().code()).toArray()); + if (expectedResponseCount >= 0) { + assertThat(responses, hasSize(expectedResponseCount)); + } + assertThat(responses.size(), lessThanOrEqualTo(routes.length)); + assertTrue(responses.stream().allMatch(r -> r.status().code() == 200)); + assertOpaqueIdsInOrder(Netty4HttpClient.returnOpaqueIds(responses)); } finally { responses.forEach(ReferenceCounted::release); } @@ -60,4 +151,128 @@ private void assertOpaqueIdsInOrder(Collection opaqueIds) { } } + private static final ToXContentObject EMPTY_RESPONSE = (builder, params) -> builder.startObject().endObject(); + + /** + * Adds an HTTP route that waits for 3 concurrent executions before returning any of them + */ + public static class CountDown3Plugin extends Plugin implements ActionPlugin { + + static final String ROUTE = "/_test/countdown_3"; + + @Override + public Collection getRestHandlers( + Settings settings, + NamedWriteableRegistry namedWriteableRegistry, + RestController restController, + ClusterSettings clusterSettings, + IndexScopedSettings indexScopedSettings, + SettingsFilter settingsFilter, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier nodesInCluster, + Predicate clusterSupportsFeature + ) { + return List.of(new BaseRestHandler() { + private final SubscribableListener subscribableListener = new SubscribableListener<>(); + private final CountDownActionListener countDownActionListener = new CountDownActionListener( + 3, + subscribableListener.map(v -> EMPTY_RESPONSE) + ); + + private void addListener(ActionListener listener) { + subscribableListener.addListener(listener); + countDownActionListener.onResponse(null); + } + + @Override + public String getName() { + return ROUTE; + } + + @Override + public List routes() { + return List.of(new Route(GET, ROUTE)); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + return channel -> addListener(new RestToXContentListener<>(channel)); + } + }); + } + } + + /** + * Adds an HTTP route that waits for 3 concurrent executions before returning any of them + */ + public static class ChunkAndFailPlugin extends Plugin implements ActionPlugin { + + static final String ROUTE = "/_test/chunk_and_fail"; + static final String FAIL_AFTER_BYTES_PARAM = "fail_after_bytes"; + + static String randomRequestUri() { + return ROUTE + '?' + FAIL_AFTER_BYTES_PARAM + '=' + between(0, ByteSizeUnit.MB.toIntBytes(2)); + } + + @Override + public Collection getRestHandlers( + Settings settings, + NamedWriteableRegistry namedWriteableRegistry, + RestController restController, + ClusterSettings clusterSettings, + IndexScopedSettings indexScopedSettings, + SettingsFilter settingsFilter, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier nodesInCluster, + Predicate clusterSupportsFeature + ) { + return List.of(new BaseRestHandler() { + @Override + public String getName() { + return ROUTE; + } + + @Override + public List routes() { + return List.of(new Route(GET, ROUTE)); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + final var failAfterBytes = request.paramAsInt(FAIL_AFTER_BYTES_PARAM, -1); + if (failAfterBytes < 0) { + throw new IllegalArgumentException("[" + FAIL_AFTER_BYTES_PARAM + "] must be present and non-negative"); + } + return channel -> client.threadPool() + .executor(randomFrom(ThreadPool.Names.SAME, ThreadPool.Names.GENERIC)) + .execute(() -> channel.sendResponse(RestResponse.chunked(RestStatus.OK, new ChunkedRestResponseBody() { + int bytesRemaining = failAfterBytes; + + @Override + public boolean isDone() { + return false; + } + + @Override + public ReleasableBytesReference encodeChunk(int sizeHint, Recycler recycler) throws IOException { + assert bytesRemaining >= 0 : "already failed"; + if (bytesRemaining == 0) { + bytesRemaining = -1; + throw new IOException("simulated failure"); + } else { + final var bytesToSend = between(1, bytesRemaining); + bytesRemaining -= bytesToSend; + return ReleasableBytesReference.wrap(new ZeroBytesReference(bytesToSend)); + } + } + + @Override + public String getResponseContentTypeString() { + return RestResponse.TEXT_CONTENT_TYPE; + } + }, null))); + } + }); + } + } } diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java index 908bfa8a9fc3b..86fa635078d4f 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java @@ -28,6 +28,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.ReleasableBytesReference; import org.elasticsearch.core.Booleans; import org.elasticsearch.core.Nullable; @@ -41,9 +42,7 @@ import java.io.IOException; import java.nio.channels.ClosedChannelException; import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.Comparator; -import java.util.List; import java.util.PriorityQueue; import java.util.Queue; @@ -165,8 +164,10 @@ public void write(final ChannelHandlerContext ctx, final Object msg, final Chann } catch (IllegalStateException e) { ctx.channel().close(); } finally { - if (success == false) { - promise.setFailure(new ClosedChannelException()); + if (success == false && promise.isDone() == false) { + // The preceding failure may already have failed the promise; use tryFailure() to avoid log noise about double-completion, + // but also check isDone() first to avoid even constructing another exception in most cases. + promise.tryFailure(new ClosedChannelException()); } } } @@ -190,7 +191,7 @@ private void doWriteQueued(ChannelHandlerContext ctx) throws IOException { SPLIT_THRESHOLD = (int) (NettyAllocator.suggestedMaxAllocationSize() * 0.99); } - private void doWrite(ChannelHandlerContext ctx, Netty4HttpResponse readyResponse, ChannelPromise promise) throws IOException { + private void doWrite(ChannelHandlerContext ctx, Netty4HttpResponse readyResponse, ChannelPromise promise) { assert currentChunkedWrite == null : "unexpected existing write [" + currentChunkedWrite + "]"; assert readyResponse != null : "cannot write null response"; assert readyResponse.getSequence() == writeSequence; @@ -216,8 +217,7 @@ private void doWriteFullResponse(ChannelHandlerContext ctx, Netty4FullHttpRespon writeSequence++; } - private void doWriteChunkedResponse(ChannelHandlerContext ctx, Netty4ChunkedHttpResponse readyResponse, ChannelPromise promise) - throws IOException { + private void doWriteChunkedResponse(ChannelHandlerContext ctx, Netty4ChunkedHttpResponse readyResponse, ChannelPromise promise) { final PromiseCombiner combiner = new PromiseCombiner(ctx.executor()); final ChannelPromise first = ctx.newPromise(); combiner.add((Future) first); @@ -228,7 +228,7 @@ private void doWriteChunkedResponse(ChannelHandlerContext ctx, Netty4ChunkedHttp // We were able to write out the first chunk directly, try writing out subsequent chunks until the channel becomes unwritable. // NB "writable" means there's space in the downstream ChannelOutboundBuffer, we aren't trying to saturate the physical channel. while (ctx.channel().isWritable()) { - if (writeChunk(ctx, combiner, responseBody)) { + if (writeChunk(ctx, currentChunkedWrite)) { finishChunkedWrite(); return; } @@ -237,12 +237,15 @@ private void doWriteChunkedResponse(ChannelHandlerContext ctx, Netty4ChunkedHttp } private void finishChunkedWrite() { - assert currentChunkedWrite != null; + if (currentChunkedWrite == null) { + // failure during chunked response serialization, we're closing the channel + return; + } assert currentChunkedWrite.responseBody().isDone(); final var finishingWrite = currentChunkedWrite; currentChunkedWrite = null; writeSequence++; - finishingWrite.combiner.finish(finishingWrite.onDone()); + finishingWrite.combiner().finish(finishingWrite.onDone()); } private void splitAndWrite(ChannelHandlerContext ctx, Netty4FullHttpResponse msg, ChannelPromise promise) { @@ -286,7 +289,7 @@ private boolean doFlush(ChannelHandlerContext ctx) throws IOException { assert ctx.executor().inEventLoop(); final Channel channel = ctx.channel(); if (channel.isActive() == false) { - failQueuedWrites(); + failQueuedWrites(ctx); return false; } while (channel.isWritable()) { @@ -302,7 +305,7 @@ private boolean doFlush(ChannelHandlerContext ctx) throws IOException { if (currentWrite == null) { // no bytes were found queued, check if a chunked message might have become writable if (currentChunkedWrite != null) { - if (writeChunk(ctx, currentChunkedWrite.combiner, currentChunkedWrite.responseBody())) { + if (writeChunk(ctx, currentChunkedWrite)) { finishChunkedWrite(); } continue; @@ -313,17 +316,21 @@ private boolean doFlush(ChannelHandlerContext ctx) throws IOException { } ctx.flush(); if (channel.isActive() == false) { - failQueuedWrites(); + failQueuedWrites(ctx); } return true; } - private boolean writeChunk(ChannelHandlerContext ctx, PromiseCombiner combiner, ChunkedRestResponseBody body) throws IOException { + private boolean writeChunk(ChannelHandlerContext ctx, ChunkedWrite chunkedWrite) { + final var body = chunkedWrite.responseBody(); + final var combiner = chunkedWrite.combiner(); assert body.isDone() == false : "should not continue to try and serialize once done"; - final ReleasableBytesReference bytes = body.encodeChunk( - Netty4WriteThrottlingHandler.MAX_BYTES_PER_WRITE, - serverTransport.recycler() - ); + final ReleasableBytesReference bytes; + try { + bytes = body.encodeChunk(Netty4WriteThrottlingHandler.MAX_BYTES_PER_WRITE, serverTransport.recycler()); + } catch (Exception e) { + return handleChunkingFailure(ctx, chunkedWrite, e); + } final ByteBuf content = Netty4Utils.toByteBuf(bytes); final boolean done = body.isDone(); final ChannelFuture f = ctx.write(done ? new DefaultLastHttpContent(content) : new DefaultHttpContent(content)); @@ -332,39 +339,30 @@ private boolean writeChunk(ChannelHandlerContext ctx, PromiseCombiner combiner, return done; } - private void failQueuedWrites() { + private boolean handleChunkingFailure(ChannelHandlerContext ctx, ChunkedWrite chunkedWrite, Exception e) { + logger.error(Strings.format("caught exception while encoding response chunk, closing connection %s", ctx.channel()), e); + assert currentChunkedWrite == chunkedWrite; + currentChunkedWrite = null; + chunkedWrite.combiner().add(ctx.channel().close()); + chunkedWrite.combiner().add(ctx.newFailedFuture(e)); + chunkedWrite.combiner().finish(chunkedWrite.onDone()); + return true; + } + + private void failQueuedWrites(ChannelHandlerContext ctx) { WriteOperation queuedWrite; while ((queuedWrite = queuedWrites.poll()) != null) { queuedWrite.failAsClosedChannel(); } if (currentChunkedWrite != null) { - safeFailPromise(currentChunkedWrite.onDone, new ClosedChannelException()); - currentChunkedWrite = null; - } - } - - @Override - public void close(ChannelHandlerContext ctx, ChannelPromise promise) { - if (currentChunkedWrite != null) { - safeFailPromise(currentChunkedWrite.onDone, new ClosedChannelException()); + final var chunkedWrite = currentChunkedWrite; currentChunkedWrite = null; + chunkedWrite.combiner().add(ctx.newFailedFuture(new ClosedChannelException())); + chunkedWrite.combiner().finish(chunkedWrite.onDone()); } - List> inflightResponses = removeAllInflightResponses(); - - if (inflightResponses.isEmpty() == false) { - ClosedChannelException closedChannelException = new ClosedChannelException(); - for (Tuple inflightResponse : inflightResponses) { - safeFailPromise(inflightResponse.v2(), closedChannelException); - } - } - ctx.close(promise); - } - - private void safeFailPromise(ChannelPromise promise, Exception ex) { - try { - promise.setFailure(ex); - } catch (RuntimeException e) { - logger.error("unexpected error while releasing pipelined http responses", e); + Tuple pipelinedWrite; + while ((pipelinedWrite = outboundHoldingQueue.poll()) != null) { + pipelinedWrite.v2().tryFailure(new ClosedChannelException()); } } @@ -398,12 +396,6 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { } } - private List> removeAllInflightResponses() { - ArrayList> responses = new ArrayList<>(outboundHoldingQueue); - outboundHoldingQueue.clear(); - return responses; - } - private record WriteOperation(HttpObject msg, ChannelPromise promise) { void failAsClosedChannel() { diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java index 2524be154414e..d6ee096b8dfd8 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java @@ -18,6 +18,7 @@ import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.PrematureChannelClosureException; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; @@ -31,8 +32,8 @@ import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpVersion; +import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.common.unit.ByteSizeUnit; -import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.core.Tuple; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.netty4.NettyAllocator; @@ -173,10 +174,9 @@ private static class CountDownLatchHandler extends ChannelInitializer() { @Override protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { @@ -189,9 +189,25 @@ protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { } @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - super.exceptionCaught(ctx, cause); - latch.countDown(); + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (cause instanceof PrematureChannelClosureException) { + // no more requests coming, so fast-forward the latch + fastForward(); + } else { + ExceptionsHelper.maybeDieOnAnotherThread(new AssertionError(cause)); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + fastForward(); + super.channelInactive(ctx); + } + + private void fastForward() { + while (latch.getCount() > 0) { + latch.countDown(); + } } }); } diff --git a/server/src/main/java/org/elasticsearch/http/AbstractHttpServerTransport.java b/server/src/main/java/org/elasticsearch/http/AbstractHttpServerTransport.java index cfd72bf6ae4a5..f9005f6e37889 100644 --- a/server/src/main/java/org/elasticsearch/http/AbstractHttpServerTransport.java +++ b/server/src/main/java/org/elasticsearch/http/AbstractHttpServerTransport.java @@ -424,7 +424,14 @@ public void incomingRequest(final HttpRequest httpRequest, final HttpChannel htt // The channel may not be present if the close listener (set in serverAcceptedChannel) runs before this method because the // connection closed early if (trackingChannel == null) { - logger.warn("http channel [{}] missing tracking channel", httpChannel); + httpRequest.release(); + logger.warn( + "http channel [{}] closed before starting to handle [{}][{}][{}]", + httpChannel, + httpRequest.header(Task.X_OPAQUE_ID_HTTP_HEADER), + httpRequest.method(), + httpRequest.uri() + ); return; } trackingChannel.incomingRequest(); diff --git a/server/src/main/java/org/elasticsearch/rest/action/RestActionListener.java b/server/src/main/java/org/elasticsearch/rest/action/RestActionListener.java index c893a43417069..13155d00c8368 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/RestActionListener.java +++ b/server/src/main/java/org/elasticsearch/rest/action/RestActionListener.java @@ -21,9 +21,7 @@ */ public abstract class RestActionListener implements ActionListener { - // we use static here so we won't have to pass the actual logger each time for a very rare case of logging - // where the settings don't matter that much - private static final Logger logger = LogManager.getLogger(RestResponseListener.class); + private static final Logger logger = LogManager.getLogger(RestActionListener.class); protected final RestChannel channel; From f4702fa2f0b97afbfbc37999e6f082ad36a47186 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Tue, 20 Feb 2024 09:32:55 -0800 Subject: [PATCH 091/250] Fix dynamic mapping condition when create tsid (#105636) We accidentally reversed the dynamicMappersExists condition. The impact of this bug is minor, primarily resulting in the return of a different error message. --- docs/changelog/105636.yaml | 5 ++ .../rest-api-spec/test/tsdb/20_mapping.yml | 6 +- .../mapper/TsidExtractingIdFieldMapper.java | 2 +- .../mapper/TimeSeriesIdFieldMapperTests.java | 72 +++++++++++++++++++ 4 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 docs/changelog/105636.yaml diff --git a/docs/changelog/105636.yaml b/docs/changelog/105636.yaml new file mode 100644 index 0000000000000..01f27199771d4 --- /dev/null +++ b/docs/changelog/105636.yaml @@ -0,0 +1,5 @@ +pr: 105636 +summary: Flip dynamic mapping condition when create tsid +area: TSDB +type: bug +issues: [] diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/20_mapping.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/20_mapping.yml index 7edae8f264c76..1ff32192b9e08 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/20_mapping.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/tsdb/20_mapping.yml @@ -241,8 +241,8 @@ runtime field matching routing path: --- "dynamic: runtime matches routing_path": - skip: - version: " - 8.7.99" - reason: routing_path error message updated in 8.8.0 + version: " - 8.13.99" + reason: routing_path error message updated in 8.8.0 and has_dynamic_mapping condition fixed in 8.14.0 - do: indices.create: @@ -272,7 +272,7 @@ runtime field matching routing path: index: test body: - '{"index": {}}' - - '{"@timestamp": "2021-04-28T18:50:04.467Z", "dim_kw": "dim", "dim": {"foo": "a"}}' + - '{"@timestamp": "2021-04-28T18:50:04.467Z", "dim_kw": "dim", "dim": {"foo": "a"}, "extra_field": 100}' - match: {items.0.index.error.reason: "All fields that match routing_path must be keywords with [time_series_dimension: true] or flattened fields with a list of dimensions in [time_series_dimensions] and without the [script] parameter. [dim.foo] was a runtime [keyword]."} --- diff --git a/server/src/main/java/org/elasticsearch/index/mapper/TsidExtractingIdFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/TsidExtractingIdFieldMapper.java index 54c495a2c9a6c..1e613767c2c89 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/TsidExtractingIdFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/TsidExtractingIdFieldMapper.java @@ -53,7 +53,7 @@ public static void createField(DocumentParserContext context, IndexRouting.Extra } long timestamp = timestampField.numericValue().longValue(); byte[] suffix = new byte[16]; - String id = createId(context.hasDynamicMappers() == false, routingBuilder, tsid, timestamp, suffix); + String id = createId(context.hasDynamicMappers(), routingBuilder, tsid, timestamp, suffix); /* * Make sure that _id from extracting the tsid matches that _id * from extracting the _source. This should be true for all valid diff --git a/server/src/test/java/org/elasticsearch/index/mapper/TimeSeriesIdFieldMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/TimeSeriesIdFieldMapperTests.java index d4dc03d22441b..94a0f2296bbfb 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/TimeSeriesIdFieldMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/TimeSeriesIdFieldMapperTests.java @@ -12,16 +12,23 @@ import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.io.stream.ByteArrayStreamInput; +import org.elasticsearch.common.lucene.uid.Versions; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.index.IndexMode; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersions; +import org.elasticsearch.index.VersionType; +import org.elasticsearch.index.engine.Engine; +import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.test.index.IndexVersionUtils; import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentType; import java.io.IOException; +import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -653,4 +660,69 @@ public void testFewerDimensions() throws IOException { ParsedDocument doc2 = parseDocument(docMapper, d -> d.field("a", a).field("b", b).field("c", c)); assertThat(doc1.rootDoc().getBinaryValue("_tsid").bytes, not(doc2.rootDoc().getBinaryValue("_tsid").bytes)); } + + public void testParseWithDynamicMapping() { + Settings indexSettings = Settings.builder() + .put(IndexSettings.MODE.getKey(), "time_series") + .put(IndexMetadata.INDEX_ROUTING_PATH.getKey(), "dim") + .build(); + // without _id + { + MapperService mapper = createMapperService(IndexVersion.current(), indexSettings, () -> false); + SourceToParse source = new SourceToParse(null, new BytesArray(""" + { + "@timestamp": 1609459200000, + "dim": "6a841a21", + "value": 100 + }"""), XContentType.JSON); + Engine.Index index = IndexShard.prepareIndex( + mapper, + source, + UNASSIGNED_SEQ_NO, + randomNonNegativeLong(), + Versions.MATCH_ANY, + VersionType.INTERNAL, + Engine.Operation.Origin.PRIMARY, + -1, + false, + UNASSIGNED_SEQ_NO, + 0, + System.nanoTime() + ); + assertNotNull(index.parsedDoc().dynamicMappingsUpdate()); + } + // with _id + { + MapperService mapper = createMapperService(IndexVersion.current(), indexSettings, () -> false); + SourceToParse source = new SourceToParse("no-such-tsid", new BytesArray(""" + { + "@timestamp": 1609459200000, + "dim": "6a841a21", + "value": 100 + }"""), XContentType.JSON); + var failure = expectThrows(DocumentParsingException.class, () -> { + IndexShard.prepareIndex( + mapper, + source, + UNASSIGNED_SEQ_NO, + randomNonNegativeLong(), + Versions.MATCH_ANY, + VersionType.INTERNAL, + Engine.Operation.Origin.PRIMARY, + -1, + false, + UNASSIGNED_SEQ_NO, + 0, + System.nanoTime() + ); + }); + assertThat( + failure.getMessage(), + equalTo( + "[5:1] failed to parse: _id must be unset or set to [AAAAAMpxfIC8Wpr0AAABdrs-cAA]" + + " but was [no-such-tsid] because [index] is in time_series mode" + ) + ); + } + } } From 065158e2229d02f4fe71b07a3a197195417ab312 Mon Sep 17 00:00:00 2001 From: Albert Zaharovits Date: Tue, 20 Feb 2024 20:55:27 +0200 Subject: [PATCH 092/250] Expose owner realm_type in the returned API key information (#105629) When querying or getting API key information, ES returns the key owner's username and realm (i.e. the realm name that authenticated the username that last updated the API key). This PR adds the realm_type to the information on the key's owner. --- docs/changelog/105629.yaml | 5 + .../rest-api/security/get-api-keys.asciidoc | 2 + .../rest-api/security/query-api-key.asciidoc | 2 + .../core/security/action/apikey/ApiKey.java | 36 ++++- .../security/action/apikey/ApiKeyTests.java | 3 + .../action/apikey/GetApiKeyResponseTests.java | 9 ++ .../xpack/security/apikey/ApiKeyRestIT.java | 4 + .../authc/apikey/ApiKeySingleNodeTests.java | 9 +- .../xpack/security/authc/ApiKeyService.java | 2 +- .../security/authc/ApiKeyServiceTests.java | 142 +++++++++++++++++- .../apikey/RestGetApiKeyActionTests.java | 4 + 11 files changed, 205 insertions(+), 13 deletions(-) create mode 100644 docs/changelog/105629.yaml diff --git a/docs/changelog/105629.yaml b/docs/changelog/105629.yaml new file mode 100644 index 0000000000000..00fa73a759558 --- /dev/null +++ b/docs/changelog/105629.yaml @@ -0,0 +1,5 @@ +pr: 105629 +summary: Show owner `realm_type` for returned API keys +area: Security +type: enhancement +issues: [] diff --git a/docs/reference/rest-api/security/get-api-keys.asciidoc b/docs/reference/rest-api/security/get-api-keys.asciidoc index d75edda9296a5..a02e8adb67b4f 100644 --- a/docs/reference/rest-api/security/get-api-keys.asciidoc +++ b/docs/reference/rest-api/security/get-api-keys.asciidoc @@ -134,6 +134,7 @@ A successful call returns a JSON structure that contains the information of the "invalidated": false, <6> "username": "myuser", <7> "realm": "native1", <8> + "realm_type": "native", "metadata": { <9> "application": "myapp" }, @@ -289,6 +290,7 @@ A successful call returns a JSON structure that contains the information of one "invalidated": false, "username": "myuser", "realm": "native1", + "realm_type": "native", "metadata": { "application": "myapp" }, diff --git a/docs/reference/rest-api/security/query-api-key.asciidoc b/docs/reference/rest-api/security/query-api-key.asciidoc index 88fef9a21ff88..e16ba267203b8 100644 --- a/docs/reference/rest-api/security/query-api-key.asciidoc +++ b/docs/reference/rest-api/security/query-api-key.asciidoc @@ -299,6 +299,7 @@ retrieved from one or more API keys: "invalidated": false, "username": "elastic", "realm": "reserved", + "realm_type": "reserved", "metadata": { "letter": "a" }, @@ -411,6 +412,7 @@ A successful call returns a JSON structure for API key information including its "invalidated": false, "username": "myuser", "realm": "native1", + "realm_type": "native", "metadata": { "application": "my-application" }, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java index 3ab487560f2b8..ae345870e718b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java @@ -16,6 +16,7 @@ import org.elasticsearch.xcontent.ToXContentObject; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.RoleDescriptorsIntersection; @@ -88,6 +89,8 @@ public String value() { private final Instant invalidation; private final String username; private final String realm; + @Nullable + private final String realmType; private final Map metadata; @Nullable private final List roleDescriptors; @@ -104,6 +107,7 @@ public ApiKey( @Nullable Instant invalidation, String username, String realm, + @Nullable String realmType, @Nullable Map metadata, @Nullable List roleDescriptors, @Nullable List limitedByRoleDescriptors @@ -118,6 +122,7 @@ public ApiKey( invalidation, username, realm, + realmType, metadata, roleDescriptors, limitedByRoleDescriptors == null ? null : new RoleDescriptorsIntersection(List.of(Set.copyOf(limitedByRoleDescriptors))) @@ -134,6 +139,7 @@ private ApiKey( Instant invalidation, String username, String realm, + @Nullable String realmType, @Nullable Map metadata, @Nullable List roleDescriptors, @Nullable RoleDescriptorsIntersection limitedBy @@ -150,6 +156,7 @@ private ApiKey( this.invalidation = (invalidation != null) ? Instant.ofEpochMilli(invalidation.toEpochMilli()) : null; this.username = username; this.realm = realm; + this.realmType = realmType; this.metadata = metadata == null ? Map.of() : metadata; this.roleDescriptors = roleDescriptors != null ? List.copyOf(roleDescriptors) : null; // This assertion will need to be changed (or removed) when derived keys are properly supported @@ -193,6 +200,17 @@ public String getRealm() { return realm; } + public @Nullable String getRealmType() { + return realmType; + } + + public @Nullable RealmConfig.RealmIdentifier getRealmIdentifier() { + if (realm != null && realmType != null) { + return new RealmConfig.RealmIdentifier(realmType, realm); + } + return null; + } + public Map getMetadata() { return metadata; } @@ -223,7 +241,11 @@ public XContentBuilder innerToXContent(XContentBuilder builder, Params params) t if (invalidation != null) { builder.field("invalidation", invalidation.toEpochMilli()); } - builder.field("username", username).field("realm", realm).field("metadata", (metadata == null ? Map.of() : metadata)); + builder.field("username", username).field("realm", realm); + if (realmType != null) { + builder.field("realm_type", realmType); + } + builder.field("metadata", (metadata == null ? Map.of() : metadata)); if (roleDescriptors != null) { builder.startObject("role_descriptors"); for (var roleDescriptor : roleDescriptors) { @@ -287,6 +309,7 @@ public int hashCode() { invalidation, username, realm, + realmType, metadata, roleDescriptors, limitedBy @@ -314,6 +337,7 @@ public boolean equals(Object obj) { && Objects.equals(invalidation, other.invalidation) && Objects.equals(username, other.username) && Objects.equals(realm, other.realm) + && Objects.equals(realmType, other.realmType) && Objects.equals(metadata, other.metadata) && Objects.equals(roleDescriptors, other.roleDescriptors) && Objects.equals(limitedBy, other.limitedBy); @@ -331,9 +355,10 @@ public boolean equals(Object obj) { (args[6] == null) ? null : Instant.ofEpochMilli((Long) args[6]), (String) args[7], (String) args[8], - (args[9] == null) ? null : (Map) args[9], - (List) args[10], - (RoleDescriptorsIntersection) args[11] + (String) args[9], + (args[10] == null) ? null : (Map) args[10], + (List) args[11], + (RoleDescriptorsIntersection) args[12] ); }); static { @@ -346,6 +371,7 @@ public boolean equals(Object obj) { PARSER.declareLong(optionalConstructorArg(), new ParseField("invalidation")); PARSER.declareString(constructorArg(), new ParseField("username")); PARSER.declareString(constructorArg(), new ParseField("realm")); + PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("realm_type")); PARSER.declareObject(optionalConstructorArg(), (p, c) -> p.map(), new ParseField("metadata")); PARSER.declareNamedObjects(optionalConstructorArg(), (p, c, n) -> { p.nextToken(); @@ -383,6 +409,8 @@ public String toString() { + username + ", realm=" + realm + + ", realm_type=" + + realmType + ", metadata=" + metadata + ", role_descriptors=" diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java index 02bce50ed3483..361928590556a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java @@ -68,6 +68,7 @@ public void testXContent() throws IOException { assertThat(map.get("invalidated"), is(apiKey.isInvalidated())); assertThat(map.get("username"), equalTo(apiKey.getUsername())); assertThat(map.get("realm"), equalTo(apiKey.getRealm())); + assertThat(map.get("realm_type"), equalTo(apiKey.getRealmType())); assertThat(map.get("metadata"), equalTo(Objects.requireNonNullElseGet(apiKey.getMetadata(), Map::of))); if (apiKey.getRoleDescriptors() == null) { @@ -172,6 +173,7 @@ public static ApiKey randomApiKeyInstance() { : null; final String username = randomAlphaOfLengthBetween(4, 10); final String realmName = randomAlphaOfLengthBetween(3, 8); + final String realmType = randomFrom(randomAlphaOfLengthBetween(3, 8), null); final Map metadata = randomMetadata(); final List roleDescriptors = type == ApiKey.Type.CROSS_CLUSTER ? List.of(randomCrossClusterAccessRoleDescriptor()) @@ -190,6 +192,7 @@ public static ApiKey randomApiKeyInstance() { invalidation, username, realmName, + realmType, metadata, roleDescriptors, limitedByRoleDescriptors diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java index 0b287f2fb6329..d5de84045096a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java @@ -61,6 +61,7 @@ public void testToXContent() throws IOException { "realm-x", null, null, + null, List.of() // empty limited-by role descriptor to simulate derived keys ); ApiKey apiKeyInfo2 = createApiKeyInfo( @@ -73,6 +74,7 @@ public void testToXContent() throws IOException { Instant.ofEpochMilli(100000000L), "user-b", "realm-y", + "realm-type-y", Map.of(), List.of(), limitedByRoleDescriptors @@ -87,6 +89,7 @@ public void testToXContent() throws IOException { Instant.ofEpochMilli(100000000L), "user-c", "realm-z", + "realm-type-z", Map.of("foo", "bar"), roleDescriptors, limitedByRoleDescriptors @@ -111,6 +114,7 @@ public void testToXContent() throws IOException { Instant.ofEpochMilli(100000000L), "user-c", "realm-z", + "realm-type-z", Map.of("foo", "bar"), crossClusterAccessRoleDescriptors, null @@ -145,6 +149,7 @@ public void testToXContent() throws IOException { "invalidation": 100000000, "username": "user-b", "realm": "realm-y", + "realm_type": "realm-type-y", "metadata": {}, "role_descriptors": {}, "limited_by": [ @@ -185,6 +190,7 @@ public void testToXContent() throws IOException { "invalidation": 100000000, "username": "user-c", "realm": "realm-z", + "realm_type": "realm-type-z", "metadata": { "foo": "bar" }, @@ -252,6 +258,7 @@ public void testToXContent() throws IOException { "invalidation": 100000000, "username": "user-c", "realm": "realm-z", + "realm_type": "realm-type-z", "metadata": { "foo": "bar" }, @@ -321,6 +328,7 @@ private ApiKey createApiKeyInfo( Instant invalidation, String username, String realm, + String realmType, Map metadata, List roleDescriptors, List limitedByRoleDescriptors @@ -335,6 +343,7 @@ private ApiKey createApiKeyInfo( invalidation, username, realm, + realmType, metadata, roleDescriptors, limitedByRoleDescriptors diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java index 4e639e14eda6e..850dfe5dffa99 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java @@ -300,6 +300,8 @@ public void testGrantApiKeyForOtherUserWithPassword() throws IOException { ApiKey apiKey = getApiKey((String) responseBody.get("id")); assertThat(apiKey.getUsername(), equalTo(END_USER)); + assertThat(apiKey.getRealm(), equalTo("default_native")); + assertThat(apiKey.getRealmType(), equalTo("native")); } public void testGrantApiKeyForOtherUserWithAccessToken() throws IOException { @@ -329,6 +331,8 @@ public void testGrantApiKeyForOtherUserWithAccessToken() throws IOException { ApiKey apiKey = getApiKey((String) responseBody.get("id")); assertThat(apiKey.getUsername(), equalTo(END_USER)); + assertThat(apiKey.getRealm(), equalTo("default_native")); + assertThat(apiKey.getRealmType(), equalTo("native")); Instant minExpiry = before.plus(2, ChronoUnit.HOURS); Instant maxExpiry = after.plus(2, ChronoUnit.HOURS); diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java index bffc6c165c818..707e7b2846a9b 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java @@ -312,7 +312,10 @@ public void testGrantApiKeyForUserWithRunAs() throws IOException { final String apiKeyId = createApiKeyResponse.getId(); final String base64ApiKeyKeyValue = Base64.getEncoder() .encodeToString((apiKeyId + ":" + createApiKeyResponse.getKey().toString()).getBytes(StandardCharsets.UTF_8)); - assertThat(securityClient.getApiKey(apiKeyId).getUsername(), equalTo("user2")); + ApiKey apiKey = securityClient.getApiKey(apiKeyId); + assertThat(apiKey.getUsername(), equalTo("user2")); + assertThat(apiKey.getRealm(), equalTo("index")); + assertThat(apiKey.getRealmType(), equalTo("native")); final Client clientWithGrantedKey = client().filterWithHeader(Map.of("Authorization", "ApiKey " + base64ApiKeyKeyValue)); // The API key has privileges (inherited from user2) to check cluster health clientWithGrantedKey.execute(TransportClusterHealthAction.TYPE, new ClusterHealthRequest()).actionGet(); @@ -618,6 +621,7 @@ public void testCreateCrossClusterApiKey() throws IOException { assertThat(getApiKeyInfo.getMetadata(), anEmptyMap()); assertThat(getApiKeyInfo.getUsername(), equalTo("test_user")); assertThat(getApiKeyInfo.getRealm(), equalTo("file")); + assertThat(getApiKeyInfo.getRealmType(), equalTo("file")); // Check the API key attributes with Query API final QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest( @@ -638,6 +642,7 @@ public void testCreateCrossClusterApiKey() throws IOException { assertThat(queryApiKeyInfo.getMetadata(), anEmptyMap()); assertThat(queryApiKeyInfo.getUsername(), equalTo("test_user")); assertThat(queryApiKeyInfo.getRealm(), equalTo("file")); + assertThat(queryApiKeyInfo.getRealmType(), equalTo("file")); } public void testUpdateCrossClusterApiKey() throws IOException { @@ -672,6 +677,7 @@ public void testUpdateCrossClusterApiKey() throws IOException { assertThat(getApiKeyInfo.getMetadata(), anEmptyMap()); assertThat(getApiKeyInfo.getUsername(), equalTo("test_user")); assertThat(getApiKeyInfo.getRealm(), equalTo("file")); + assertThat(getApiKeyInfo.getRealmType(), equalTo("file")); final CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder; final boolean shouldUpdateAccess = randomBoolean(); @@ -745,6 +751,7 @@ public void testUpdateCrossClusterApiKey() throws IOException { assertThat(queryApiKeyInfo.getMetadata(), equalTo(updateMetadata == null ? Map.of() : updateMetadata)); assertThat(queryApiKeyInfo.getUsername(), equalTo("test_user")); assertThat(queryApiKeyInfo.getRealm(), equalTo("file")); + assertThat(queryApiKeyInfo.getRealmType(), equalTo("file")); } // Cross-cluster API keys cannot be created by an API key even if it has manage_security privilege diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 7cf045ad0f9f5..fea0c812e7e42 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -1937,7 +1937,6 @@ public void getApiKeys( public void queryApiKeys(SearchRequest searchRequest, boolean withLimitedBy, ActionListener listener) { ensureEnabled(); - final SecurityIndexManager frozenSecurityIndex = securityIndex.defensiveCopy(); if (frozenSecurityIndex.indexExists() == false) { logger.debug("security index does not exist"); @@ -2004,6 +2003,7 @@ private ApiKey convertSearchHitToApiKeyInfo(SearchHit hit, boolean withLimitedBy apiKeyDoc.invalidation != -1 ? Instant.ofEpochMilli(apiKeyDoc.invalidation) : null, (String) apiKeyDoc.creator.get("principal"), (String) apiKeyDoc.creator.get("realm"), + (String) apiKeyDoc.creator.get("realm_type"), metadata, roleDescriptors, limitedByRoleDescriptors diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index ac11dee8d4a48..df454ddffe96f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -30,12 +30,14 @@ import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.TransportSearchAction; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.update.UpdateRequestBuilder; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.CheckedSupplier; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; @@ -54,6 +56,7 @@ import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.TimeValue; +import org.elasticsearch.core.Tuple; import org.elasticsearch.index.get.GetResult; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; @@ -91,12 +94,14 @@ import org.elasticsearch.xpack.core.security.action.apikey.CrossClusterApiKeyRoleDescriptorBuilder; import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyResponse; import org.elasticsearch.xpack.core.security.action.apikey.InvalidateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.apikey.QueryApiKeyResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationField; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper; import org.elasticsearch.xpack.core.security.authc.AuthenticationTests; +import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.RealmDomain; import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer; import org.elasticsearch.xpack.core.security.authc.support.Hasher; @@ -324,6 +329,109 @@ public void testGetApiKeys() throws Exception { assertThat(getApiKeyResponse.getApiKeyInfos(), emptyArray()); } + @SuppressWarnings("unchecked") + public void testApiKeysOwnerRealmIdentifier() throws Exception { + String realm1 = randomAlphaOfLength(4); + String realm1Type = randomAlphaOfLength(4); + String realm2 = randomAlphaOfLength(4); + when(clock.instant()).thenReturn(Instant.ofEpochMilli(randomMillisUpToYear9999())); + when(client.threadPool()).thenReturn(threadPool); + when(client.prepareSearch(eq(SECURITY_MAIN_ALIAS))).thenReturn(new SearchRequestBuilder(client)); + ApiKeyService service = createApiKeyService( + Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true).build() + ); + CheckedSupplier searchResponseSupplier = () -> { + // 2 API keys, one with a "null" (missing) realm type + SearchHit[] searchHits = new SearchHit[2]; + searchHits[0] = SearchHit.unpooled(randomIntBetween(0, Integer.MAX_VALUE), "0"); + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + Map apiKeySourceDoc = buildApiKeySourceDoc("some_hash".toCharArray()); + ((Map) apiKeySourceDoc.get("creator")).put("realm", realm1); + ((Map) apiKeySourceDoc.get("creator")).put("realm_type", realm1Type); + builder.map(apiKeySourceDoc); + searchHits[0].sourceRef(BytesReference.bytes(builder)); + } + searchHits[1] = SearchHit.unpooled(randomIntBetween(0, Integer.MAX_VALUE), "1"); + try (XContentBuilder builder = JsonXContent.contentBuilder()) { + Map apiKeySourceDoc = buildApiKeySourceDoc("some_hash".toCharArray()); + ((Map) apiKeySourceDoc.get("creator")).put("realm", realm2); + if (randomBoolean()) { + ((Map) apiKeySourceDoc.get("creator")).put("realm_type", null); + } else { + ((Map) apiKeySourceDoc.get("creator")).remove("realm_type"); + } + builder.map(apiKeySourceDoc); + searchHits[1].sourceRef(BytesReference.bytes(builder)); + } + return new SearchResponse( + SearchHits.unpooled( + searchHits, + new TotalHits(searchHits.length, TotalHits.Relation.EQUAL_TO), + randomFloat(), + null, + null, + null + ), + null, + null, + false, + null, + null, + 0, + randomAlphaOfLengthBetween(3, 8), + 1, + 1, + 0, + 10, + null, + null + ); + }; + doAnswer(invocation -> { + ActionListener.respondAndRelease((ActionListener) invocation.getArguments()[1], searchResponseSupplier.get()); + return null; + }).when(client).search(any(SearchRequest.class), anyActionListener()); + doAnswer(invocation -> { + ActionListener.respondAndRelease((ActionListener) invocation.getArguments()[2], searchResponseSupplier.get()); + return null; + }).when(client).execute(eq(TransportSearchAction.TYPE), any(SearchRequest.class), anyActionListener()); + { + PlainActionFuture getApiKeyResponsePlainActionFuture = new PlainActionFuture<>(); + service.getApiKeys( + generateRandomStringArray(4, 4, true, true), + randomFrom(randomAlphaOfLengthBetween(3, 8), null), + randomFrom(randomAlphaOfLengthBetween(3, 8), null), + generateRandomStringArray(4, 4, true, true), + randomBoolean(), + randomBoolean(), + getApiKeyResponsePlainActionFuture + ); + GetApiKeyResponse getApiKeyResponse = getApiKeyResponsePlainActionFuture.get(); + assertThat(getApiKeyResponse.getApiKeyInfos().length, is(2)); + assertThat(getApiKeyResponse.getApiKeyInfos()[0].getRealm(), is(realm1)); + assertThat(getApiKeyResponse.getApiKeyInfos()[0].getRealmType(), is(realm1Type)); + assertThat(getApiKeyResponse.getApiKeyInfos()[0].getRealmIdentifier(), is(new RealmConfig.RealmIdentifier(realm1Type, realm1))); + assertThat(getApiKeyResponse.getApiKeyInfos()[1].getRealm(), is(realm2)); + assertThat(getApiKeyResponse.getApiKeyInfos()[1].getRealmType(), nullValue()); + assertThat(getApiKeyResponse.getApiKeyInfos()[1].getRealmIdentifier(), nullValue()); + } + { + PlainActionFuture queryApiKeyResponsePlainActionFuture = new PlainActionFuture<>(); + service.queryApiKeys(new SearchRequest(".security"), false, queryApiKeyResponsePlainActionFuture); + QueryApiKeyResponse queryApiKeyResponse = queryApiKeyResponsePlainActionFuture.get(); + assertThat(queryApiKeyResponse.getItems().length, is(2)); + assertThat(queryApiKeyResponse.getItems()[0].getApiKey().getRealm(), is(realm1)); + assertThat(queryApiKeyResponse.getItems()[0].getApiKey().getRealmType(), is(realm1Type)); + assertThat( + queryApiKeyResponse.getItems()[0].getApiKey().getRealmIdentifier(), + is(new RealmConfig.RealmIdentifier(realm1Type, realm1)) + ); + assertThat(queryApiKeyResponse.getItems()[1].getApiKey().getRealm(), is(realm2)); + assertThat(queryApiKeyResponse.getItems()[1].getApiKey().getRealmType(), nullValue()); + assertThat(queryApiKeyResponse.getItems()[1].getApiKey().getRealmIdentifier(), nullValue()); + } + } + @SuppressWarnings("unchecked") public void testInvalidateApiKeys() throws Exception { final Settings settings = Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true).build(); @@ -890,6 +998,24 @@ private Map mockKeyDocument( Duration expiry, @Nullable List keyRoles, ApiKey.Type type + ) throws IOException { + var apiKeyDoc = newApiKeyDocument(key, user, authUser, invalidated, expiry, keyRoles, type); + SecurityMocks.mockGetRequest( + client, + id, + BytesReference.bytes(XContentBuilder.builder(XContentType.JSON.xContent()).map(apiKeyDoc.v1())) + ); + return apiKeyDoc.v2(); + } + + private static Tuple, Map> newApiKeyDocument( + String key, + User user, + @Nullable User authUser, + boolean invalidated, + Duration expiry, + @Nullable List keyRoles, + ApiKey.Type type ) throws IOException { final Authentication authentication; if (authUser != null) { @@ -906,7 +1032,7 @@ private Map mockKeyDocument( .realmRef(new RealmRef("realm1", "native", "node01")) .build(false); } - final Map metadata = ApiKeyTests.randomMetadata(); + Map metadataMap = ApiKeyTests.randomMetadata(); XContentBuilder docSource = ApiKeyService.newDocument( getFastStoredHashAlgoForTests().hash(new SecureString(key.toCharArray())), "test", @@ -917,15 +1043,13 @@ private Map mockKeyDocument( keyRoles, type, Version.CURRENT, - metadata + metadataMap ); + Map keyMap = XContentHelper.convertToMap(BytesReference.bytes(docSource), true, XContentType.JSON).v2(); if (invalidated) { - Map map = XContentHelper.convertToMap(BytesReference.bytes(docSource), true, XContentType.JSON).v2(); - map.put("api_key_invalidated", true); - docSource = XContentBuilder.builder(XContentType.JSON.xContent()).map(map); + keyMap.put("api_key_invalidated", true); } - SecurityMocks.mockGetRequest(client, id, BytesReference.bytes(docSource)); - return metadata; + return new Tuple<>(keyMap, metadataMap); } private AuthenticationResult tryAuthenticate(ApiKeyService service, String id, String key, ApiKey.Type type) throws Exception { @@ -2860,6 +2984,10 @@ private Map buildApiKeySourceDoc(char[] hash) { creatorMap.put("full_name", "test user"); creatorMap.put("email", "test@user.com"); creatorMap.put("metadata", Collections.emptyMap()); + creatorMap.put("realm", randomAlphaOfLength(4)); + if (randomBoolean()) { + creatorMap.put("realm_type", randomAlphaOfLength(4)); + } sourceMap.put("creator", creatorMap); sourceMap.put("api_key_invalidated", false); // noinspection unchecked diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java index 2ee42b360f02a..76a01f100b8ad 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java @@ -117,6 +117,7 @@ public void sendResponse(RestResponse restResponse) { null, "user-x", "realm-1", + "realm-type-1", metadata, roleDescriptors, limitedByRoleDescriptors @@ -176,6 +177,7 @@ public void doE null, "user-x", "realm-1", + "realm-type-1", metadata, roleDescriptors, limitedByRoleDescriptors @@ -226,6 +228,7 @@ public void sendResponse(RestResponse restResponse) { null, "user-x", "realm-1", + "realm-type-1", ApiKeyTests.randomMetadata(), type == ApiKey.Type.CROSS_CLUSTER ? List.of(randomCrossClusterAccessRoleDescriptor()) @@ -242,6 +245,7 @@ public void sendResponse(RestResponse restResponse) { null, "user-y", "realm-1", + "realm-type-1", ApiKeyTests.randomMetadata(), type == ApiKey.Type.CROSS_CLUSTER ? List.of(randomCrossClusterAccessRoleDescriptor()) From b3fc714b8729c8e08c300e96e5a4eb3c703a44f6 Mon Sep 17 00:00:00 2001 From: Stuart Tettemer Date: Tue, 20 Feb 2024 13:50:48 -0600 Subject: [PATCH 093/250] Painless: Apply true regex limit factor with FIND and MATCH operator (#105670) `script.painless.regex.enabled`: `true` changes the effective `script.painless.regex.regex.limit-factor`. This worked if the limit factor was looked up via the static `$COMPILERSETTINGS` such as during a call but it did not work if used via a binary operation such as `FIND`, `=~`, or `MATCH`, `==~`. Only expose the applied limit factor and use that everywhere. Fixes: #105669 --- docs/changelog/105670.yaml | 5 +++++ .../painless/CompilerSettings.java | 20 +++++++++---------- .../painless/PainlessScriptEngine.java | 2 +- .../phase/DefaultUserTreeToIRTreePhase.java | 2 +- .../painless/RegexLimitTests.java | 10 ++++++++++ 5 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 docs/changelog/105670.yaml diff --git a/docs/changelog/105670.yaml b/docs/changelog/105670.yaml new file mode 100644 index 0000000000000..234f4b6af5a73 --- /dev/null +++ b/docs/changelog/105670.yaml @@ -0,0 +1,5 @@ +pr: 105670 +summary: "Painless: Apply true regex limit factor with FIND and MATCH operation" +area: Infra/Scripting +type: bug +issues: [] diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/CompilerSettings.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/CompilerSettings.java index 5dfe2f19604c0..4080507a4e893 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/CompilerSettings.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/CompilerSettings.java @@ -160,10 +160,14 @@ public void setRegexLimitFactor(int regexLimitFactor) { } /** - * What is the limit factor for regexes? - */ - public int getRegexLimitFactor() { - return regexLimitFactor; + * What is the effective limit factor for regexes? + */ + public int getAppliedRegexLimitFactor() { + return switch (regexesEnabled) { + case TRUE -> Augmentation.UNLIMITED_PATTERN_FACTOR; + case FALSE -> Augmentation.DISABLED_PATTERN_FACTOR; + case LIMITED -> regexLimitFactor; + }; } /** @@ -171,14 +175,8 @@ public int getRegexLimitFactor() { * annotation. */ public Map asMap() { - int regexLimitFactorToApply = this.regexLimitFactor; - if (regexesEnabled == RegexEnabled.TRUE) { - regexLimitFactorToApply = Augmentation.UNLIMITED_PATTERN_FACTOR; - } else if (regexesEnabled == RegexEnabled.FALSE) { - regexLimitFactorToApply = Augmentation.DISABLED_PATTERN_FACTOR; - } Map map = new HashMap<>(); - map.put("regex_limit_factor", regexLimitFactorToApply); + map.put("regex_limit_factor", getAppliedRegexLimitFactor()); // for testing only map.put("testInject0", testInject0); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java index 7b84e3c9f1417..005148a6fcd5d 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java @@ -425,7 +425,7 @@ private CompilerSettings buildCompilerSettings(Map params) { // Except regexes enabled - this is a node level setting and can't be changed in the request. compilerSettings.setRegexesEnabled(defaultCompilerSettings.areRegexesEnabled()); - compilerSettings.setRegexLimitFactor(defaultCompilerSettings.getRegexLimitFactor()); + compilerSettings.setRegexLimitFactor(defaultCompilerSettings.getAppliedRegexLimitFactor()); Map copy = new HashMap<>(params); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java index 76babcdb9d26e..5e9ba3601e11c 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/DefaultUserTreeToIRTreePhase.java @@ -1076,7 +1076,7 @@ public void visitBinary(EBinary userBinaryNode, ScriptScope scriptScope) { irBinaryMathNode.attachDecoration(new IRDOperation(operation)); if (operation == Operation.MATCH || operation == Operation.FIND) { - irBinaryMathNode.attachDecoration(new IRDRegexLimit(scriptScope.getCompilerSettings().getRegexLimitFactor())); + irBinaryMathNode.attachDecoration(new IRDRegexLimit(scriptScope.getCompilerSettings().getAppliedRegexLimitFactor())); } irBinaryMathNode.attachDecoration(new IRDBinaryType(binaryType)); diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/RegexLimitTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/RegexLimitTests.java index e2c1d186e4cb9..265fba1327191 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/RegexLimitTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/RegexLimitTests.java @@ -73,6 +73,16 @@ public void testMethodRegexInject_Ref_Matcher() { assertTrue(cbe.getMessage().contains(regexCircuitMessage)); } + public void testInjectBinary() { + String script = "Pattern p = /.*a.*b.*c.*/; return 'abcxyz123abc' =~ p;"; + Settings settings = Settings.builder() + .put(CompilerSettings.REGEX_LIMIT_FACTOR.getKey(), 1) + .put(CompilerSettings.REGEX_ENABLED.getKey(), "true") + .build(); + scriptEngine = new PainlessScriptEngine(settings, scriptContexts()); + assertEquals(Boolean.TRUE, exec(script)); + } + public void testRegexInject_DefMethodRef_Matcher() { String script = "boolean isMatch(Function func) { func.apply(" + charSequence From 35df385ef961390745174f45b1213ded94dad33d Mon Sep 17 00:00:00 2001 From: Pat Whelan Date: Tue, 20 Feb 2024 15:06:56 -0500 Subject: [PATCH 094/250] [Transform] Fix testStopAtCheckpoint (#105664) Currently, there is a small chance that testStopAtCheckpoint will fail to correctly count the amount of times `doSaveState` is invoked: ``` Expected: <5> but: was <4> ``` There are two potential issues: 1. The test thread starts the Transform thread, which starts a Search thread. If the Search thread starts reading from the `saveStateListeners` while the test thread writes to the `saveStateListeners`, then there is a chance our testing logic will not be able to count the number of times we read from `saveStateListeners`. 2. The non-volatile integer may be read as one value and written as another value. Two fixes: 1. The test thread blocks the Transform thread until after the test thread writes all the listeners. The subsequent test will continue to verify that we can safely interlace reading and writing. 2. The counter is now an AtomicInteger to provide thread safety. Fixes #90549 --- .../TransformIndexerStateTests.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformIndexerStateTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformIndexerStateTests.java index 3e5f4bc929083..fceba25afc7fd 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformIndexerStateTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformIndexerStateTests.java @@ -64,6 +64,7 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Stream; @@ -106,7 +107,7 @@ class MockedTransformIndexer extends TransformIndexer { private final ThreadPool threadPool; private TransformState persistedState; - private int saveStateListenerCallCount = 0; + private AtomicInteger saveStateListenerCallCount = new AtomicInteger(0); // used for synchronizing with the test private CountDownLatch searchLatch; private CountDownLatch doProcessLatch; @@ -206,10 +207,10 @@ protected void doNextBulk(BulkRequest request, ActionListener next @Override protected void doSaveState(IndexerState state, TransformIndexerPosition position, Runnable next) { - Collection> saveStateListenersAtTheMomentOfCalling = saveStateListeners.get(); - saveStateListenerCallCount += (saveStateListenersAtTheMomentOfCalling != null) - ? saveStateListenersAtTheMomentOfCalling.size() - : 0; + var saveStateListenersAtTheMomentOfCalling = saveStateListeners.get(); + if (saveStateListenersAtTheMomentOfCalling != null) { + saveStateListenerCallCount.updateAndGet(count -> count + saveStateListenersAtTheMomentOfCalling.size()); + } super.doSaveState(state, position, next); } @@ -225,7 +226,7 @@ public boolean waitingForNextSearch() { } public int getSaveStateListenerCallCount() { - return saveStateListenerCallCount; + return saveStateListenerCallCount.get(); } public int getSaveStateListenerCount() { @@ -592,13 +593,13 @@ public void testStopAtCheckpoint() throws Exception { new TransformIndexerStats(), context ); + + // stop the indexer before it dispatches a search thread so we can load the listeners first + CountDownLatch searchLatch = indexer.createAwaitForSearchLatch(1); indexer.start(); assertTrue(indexer.maybeTriggerAsyncJob(System.currentTimeMillis())); assertEquals(indexer.getState(), IndexerState.INDEXING); - // slow down the indexer - CountDownLatch searchLatch = indexer.createAwaitForSearchLatch(1); - // this time call 5 times and change stopAtCheckpoint every time List responseLatches = new ArrayList<>(); for (int i = 0; i < 5; ++i) { From 3870bc8907270378cbddd2d0bed8f6f349e406d1 Mon Sep 17 00:00:00 2001 From: Mike Pellegrini Date: Tue, 20 Feb 2024 16:15:31 -0500 Subject: [PATCH 095/250] Added modelsForFields to QueryRewriteContext --- .../index/query/QueryRewriteContext.java | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) 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 e36c4d608d59f..ad0987d399fd7 100644 --- a/server/src/main/java/org/elasticsearch/index/query/QueryRewriteContext.java +++ b/server/src/main/java/org/elasticsearch/index/query/QueryRewriteContext.java @@ -37,6 +37,7 @@ import java.util.function.BooleanSupplier; import java.util.function.LongSupplier; import java.util.function.Predicate; +import java.util.stream.Collectors; /** * Context object used to rewrite {@link QueryBuilder} instances into simplified version. @@ -59,6 +60,7 @@ public class QueryRewriteContext { protected boolean allowUnmappedFields; protected boolean mapUnmappedFieldAsString; protected Predicate allowedFields; + private final Map> modelsForFields; public QueryRewriteContext( final XContentParserConfiguration parserConfiguration, @@ -74,7 +76,8 @@ public QueryRewriteContext( final NamedWriteableRegistry namedWriteableRegistry, final ValuesSourceRegistry valuesSourceRegistry, final BooleanSupplier allowExpensiveQueries, - final ScriptCompiler scriptService + final ScriptCompiler scriptService, + final Map> modelsForFields ) { this.parserConfiguration = parserConfiguration; @@ -92,6 +95,9 @@ public QueryRewriteContext( this.valuesSourceRegistry = valuesSourceRegistry; this.allowExpensiveQueries = allowExpensiveQueries; this.scriptService = scriptService; + this.modelsForFields = modelsForFields != null ? + modelsForFields.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> Set.copyOf(e.getValue()))) : + Collections.emptyMap(); } public QueryRewriteContext(final XContentParserConfiguration parserConfiguration, final Client client, final LongSupplier nowInMillis) { @@ -109,10 +115,36 @@ public QueryRewriteContext(final XContentParserConfiguration parserConfiguration null, null, null, + null, null ); } + public QueryRewriteContext( + final XContentParserConfiguration parserConfiguration, + final Client client, + final LongSupplier nowInMillis, + final Map> modelsForFields + ) { + this( + parserConfiguration, + client, + nowInMillis, + null, + MappingLookup.EMPTY, + Collections.emptyMap(), + null, + null, + null, + null, + null, + null, + null, + null, + modelsForFields + ); + } + /** * The registry used to build new {@link XContentParser}s. Contains registered named parsers needed to parse the query. * @@ -345,4 +377,9 @@ public Iterable getAllFieldNames() { ? allFromMapping : () -> Iterators.concat(allFromMapping.iterator(), runtimeMappings.keySet().iterator()); } + + public Set getModelsForField(String fieldName) { + Set models = modelsForFields.get(fieldName); + return models != null ? models : Collections.emptySet(); + } } From fa67339568d9a9a1127366cd38da851e1783f476 Mon Sep 17 00:00:00 2001 From: Mike Pellegrini Date: Tue, 20 Feb 2024 17:02:14 -0500 Subject: [PATCH 096/250] Updated IndicesService to create the modelsForFields map --- .../query/TransportValidateQueryAction.java | 2 +- .../action/explain/TransportExplainAction.java | 2 +- .../action/search/TransportSearchAction.java | 6 +++++- .../search/TransportSearchShardsAction.java | 2 +- .../org/elasticsearch/index/IndexService.java | 3 ++- .../index/query/CoordinatorRewriteContext.java | 1 + .../index/query/SearchExecutionContext.java | 3 ++- .../elasticsearch/indices/IndicesService.java | 18 ++++++++++++++++-- .../elasticsearch/search/SearchService.java | 5 +++-- .../search/TransportSearchActionTests.java | 3 ++- .../test/AbstractBuilderTestCase.java | 3 ++- 11 files changed, 36 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/TransportValidateQueryAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/TransportValidateQueryAction.java index d4832fa0d14e1..64c1faf0401c0 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/TransportValidateQueryAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/validate/query/TransportValidateQueryAction.java @@ -107,7 +107,7 @@ protected void doExecute(Task task, ValidateQueryRequest request, ActionListener if (request.query() == null) { rewriteListener.onResponse(request.query()); } else { - Rewriteable.rewriteAndFetch(request.query(), searchService.getRewriteContext(timeProvider), rewriteListener); + Rewriteable.rewriteAndFetch(request.query(), searchService.getRewriteContext(timeProvider, request), rewriteListener); } } diff --git a/server/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java b/server/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java index d2d7a945520c1..6af5ac813cd43 100644 --- a/server/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java +++ b/server/src/main/java/org/elasticsearch/action/explain/TransportExplainAction.java @@ -84,7 +84,7 @@ protected void doExecute(Task task, ExplainRequest request, ActionListener request.nowInMillis; - Rewriteable.rewriteAndFetch(request.query(), searchService.getRewriteContext(timeProvider), rewriteListener); + Rewriteable.rewriteAndFetch(request.query(), searchService.getRewriteContext(timeProvider, request), rewriteListener); } @Override diff --git a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java index d80322b2954c6..74c554aa9bdc6 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -452,7 +452,11 @@ void executeRequest( } } }); - Rewriteable.rewriteAndFetch(original, searchService.getRewriteContext(timeProvider::absoluteStartMillis), rewriteListener); + Rewriteable.rewriteAndFetch( + original, + searchService.getRewriteContext(timeProvider::absoluteStartMillis, original), + rewriteListener + ); } static void adjustSearchType(SearchRequest searchRequest, boolean singleShard) { diff --git a/server/src/main/java/org/elasticsearch/action/search/TransportSearchShardsAction.java b/server/src/main/java/org/elasticsearch/action/search/TransportSearchShardsAction.java index 60efb910a5269..068a5caac237a 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchShardsAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchShardsAction.java @@ -104,7 +104,7 @@ protected void doExecute(Task task, SearchShardsRequest searchShardsRequest, Act ClusterState clusterState = clusterService.state(); Rewriteable.rewriteAndFetch( original, - searchService.getRewriteContext(timeProvider::absoluteStartMillis), + searchService.getRewriteContext(timeProvider::absoluteStartMillis, original), listener.delegateFailureAndWrap((delegate, searchRequest) -> { Map groupedIndices = remoteClusterService.groupIndices( searchRequest.indicesOptions(), diff --git a/server/src/main/java/org/elasticsearch/index/IndexService.java b/server/src/main/java/org/elasticsearch/index/IndexService.java index 16a5d153a3c19..21d3ea932c28d 100644 --- a/server/src/main/java/org/elasticsearch/index/IndexService.java +++ b/server/src/main/java/org/elasticsearch/index/IndexService.java @@ -725,7 +725,8 @@ public QueryRewriteContext newQueryRewriteContext( namedWriteableRegistry, valuesSourceRegistry, allowExpensiveQueries, - scriptService + scriptService, + null ); } 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 2a1062f8876d2..ac6512b0839e6 100644 --- a/server/src/main/java/org/elasticsearch/index/query/CoordinatorRewriteContext.java +++ b/server/src/main/java/org/elasticsearch/index/query/CoordinatorRewriteContext.java @@ -51,6 +51,7 @@ public CoordinatorRewriteContext( null, null, null, + null, null ); this.indexLongFieldRange = indexLongFieldRange; 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 86af6d21b7a09..be175dee804b1 100644 --- a/server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java +++ b/server/src/main/java/org/elasticsearch/index/query/SearchExecutionContext.java @@ -265,7 +265,8 @@ private SearchExecutionContext( namedWriteableRegistry, valuesSourceRegistry, allowExpensiveQueries, - scriptService + scriptService, + null ); this.shardId = shardId; this.shardRequestIndex = shardRequestIndex; diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesService.java b/server/src/main/java/org/elasticsearch/indices/IndicesService.java index b47d10882a5c1..43e294d9a2658 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesService.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesService.java @@ -18,6 +18,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ResourceAlreadyExistsException; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.action.admin.indices.mapping.put.TransportAutoPutMappingAction; import org.elasticsearch.action.admin.indices.mapping.put.TransportPutMappingAction; @@ -151,6 +152,7 @@ import java.util.Collection; import java.util.EnumMap; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -1695,8 +1697,20 @@ public AliasFilter buildAliasFilter(ClusterState state, String index, Set> modelsForFields = new HashMap<>(); + for (Index index : indices) { + Map> fieldsForModels = indexService(index).getMetadata().getFieldsForModels(); + for (Map.Entry> entry : fieldsForModels.entrySet()) { + for (String fieldName : entry.getValue()) { + Set models = modelsForFields.computeIfAbsent(fieldName, v -> new HashSet<>()); + models.add(entry.getKey()); + } + } + } + + return new QueryRewriteContext(parserConfig, client, nowInMillis, modelsForFields); } public DataRewriteContext getDataRewriteContext(LongSupplier nowInMillis) { diff --git a/server/src/main/java/org/elasticsearch/search/SearchService.java b/server/src/main/java/org/elasticsearch/search/SearchService.java index 70a002d676235..129022b96c451 100644 --- a/server/src/main/java/org/elasticsearch/search/SearchService.java +++ b/server/src/main/java/org/elasticsearch/search/SearchService.java @@ -19,6 +19,7 @@ import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRunnable; +import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.search.CanMatchNodeRequest; import org.elasticsearch.action.search.CanMatchNodeResponse; import org.elasticsearch.action.search.SearchRequest; @@ -1759,8 +1760,8 @@ private void rewriteAndFetchShardRequest(IndexShard shard, ShardSearchRequest re /** * Returns a new {@link QueryRewriteContext} with the given {@code now} provider */ - public QueryRewriteContext getRewriteContext(LongSupplier nowInMillis) { - return indicesService.getRewriteContext(nowInMillis); + public QueryRewriteContext getRewriteContext(LongSupplier nowInMillis, IndicesRequest indicesRequest) { + return indicesService.getRewriteContext(nowInMillis, indicesRequest); } public CoordinatorRewriteContextProvider getCoordinatorRewriteContextProvider(LongSupplier nowInMillis) { 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 604d404c2f519..4b4f1490179e4 100644 --- a/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java @@ -124,6 +124,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -1717,7 +1718,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { NodeClient client = new NodeClient(settings, threadPool); SearchService searchService = mock(SearchService.class); - when(searchService.getRewriteContext(any())).thenReturn(new QueryRewriteContext(null, null, null)); + when(searchService.getRewriteContext(any(), eq(searchRequest))).thenReturn(new QueryRewriteContext(null, null, null)); ClusterService clusterService = new ClusterService( settings, new ClusterSettings(settings, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), 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 76b836ba7e2a7..1d163b2ee7d33 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractBuilderTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractBuilderTestCase.java @@ -601,7 +601,8 @@ QueryRewriteContext createQueryRewriteContext() { namedWriteableRegistry, null, () -> true, - scriptService + scriptService, + null ); } From f65f8ecd29aaf0c5e5962f31bafe394b0ca005e5 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Wed, 21 Feb 2024 08:30:44 +0100 Subject: [PATCH 097/250] Remove unused field (#105640) This change removes a field in TransportPutShutdownNodeAction that is no longer used after the refactoring --- .../xpack/shutdown/TransportPutShutdownNodeAction.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/TransportPutShutdownNodeAction.java b/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/TransportPutShutdownNodeAction.java index fcd70d5c215f1..750bb9227cff6 100644 --- a/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/TransportPutShutdownNodeAction.java +++ b/x-pack/plugin/shutdown/src/main/java/org/elasticsearch/xpack/shutdown/TransportPutShutdownNodeAction.java @@ -45,8 +45,6 @@ public class TransportPutShutdownNodeAction extends AcknowledgedTransportMasterN private final AllocationService allocationService; private final MasterServiceTaskQueue taskQueue; - private final PutShutdownNodeExecutor executor = new PutShutdownNodeExecutor(); - private static boolean putShutdownNodeState( Map shutdownMetadata, Predicate nodeExists, From 4c21c96b7031302d4c5b2878aa0951a3c133c18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Wed, 21 Feb 2024 08:37:28 +0100 Subject: [PATCH 098/250] Set disk watermarks to low values to prevent tests from failing on nodes without enough disk space (#105663) --- .../src/test/java/org/elasticsearch/node/NodeTests.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/node/NodeTests.java b/server/src/test/java/org/elasticsearch/node/NodeTests.java index 986ed9184f3e7..b36cafd694378 100644 --- a/server/src/test/java/org/elasticsearch/node/NodeTests.java +++ b/server/src/test/java/org/elasticsearch/node/NodeTests.java @@ -8,11 +8,11 @@ package org.elasticsearch.node; import org.apache.lucene.tests.util.LuceneTestCase; -import org.apache.lucene.util.Constants; import org.apache.lucene.util.SetOnce; import org.elasticsearch.bootstrap.BootstrapCheck; import org.elasticsearch.bootstrap.BootstrapContext; import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.routing.allocation.DiskThresholdSettings; import org.elasticsearch.common.ReferenceDocs; import org.elasticsearch.common.breaker.CircuitBreaker; import org.elasticsearch.common.bytes.BytesReference; @@ -189,6 +189,10 @@ private static Settings.Builder baseSettings() { .put(ClusterName.CLUSTER_NAME_SETTING.getKey(), InternalTestCluster.clusterName("single-node-cluster", randomLong())) .put(Environment.PATH_HOME_SETTING.getKey(), tempDir) .put(NetworkModule.TRANSPORT_TYPE_KEY, getTestTransportType()) + // default the watermarks low values to prevent tests from failing on nodes without enough disk space + .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_LOW_DISK_WATERMARK_SETTING.getKey(), "1b") + .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_HIGH_DISK_WATERMARK_SETTING.getKey(), "1b") + .put(DiskThresholdSettings.CLUSTER_ROUTING_ALLOCATION_DISK_FLOOD_STAGE_WATERMARK_SETTING.getKey(), "1b") .put(dataNode()); } @@ -304,7 +308,6 @@ public void testCloseOnInterruptibleTask() throws Exception { } public void testCloseOnLeakedIndexReaderReference() throws Exception { - assumeFalse("AwaitsFix https://github.com/elastic/elasticsearch/issues/105236", Constants.MAC_OS_X); Node node = new MockNode(baseSettings().build(), basePlugins()); node.start(); IndicesService indicesService = node.injector().getInstance(IndicesService.class); @@ -320,7 +323,6 @@ public void testCloseOnLeakedIndexReaderReference() throws Exception { } public void testCloseOnLeakedStoreReference() throws Exception { - assumeFalse("AwaitsFix https://github.com/elastic/elasticsearch/issues/105236", Constants.MAC_OS_X); Node node = new MockNode(baseSettings().build(), basePlugins()); node.start(); IndicesService indicesService = node.injector().getInstance(IndicesService.class); From 3cde13cae0c256bbc913ad107c5a209f38062d60 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 21 Feb 2024 07:57:35 +0000 Subject: [PATCH 099/250] Distinguish different snapshot failures by log level (#105622) Today all snapshot failures are reported in the logs at `WARN`, including a stack trace, but most of them are in fact benign or expected and do not need any further action. To make it easier to track actionable problems, this commit downgrades the non-actionable ones to `INFO` level and suppresses their stack traces. --- docs/changelog/105622.yaml | 5 + .../snapshots/SnapshotsService.java | 46 ++++- .../snapshots/SnapshotResiliencyTests.java | 178 +++++++++++++++++- 3 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 docs/changelog/105622.yaml diff --git a/docs/changelog/105622.yaml b/docs/changelog/105622.yaml new file mode 100644 index 0000000000000..33093f5ffceb5 --- /dev/null +++ b/docs/changelog/105622.yaml @@ -0,0 +1,5 @@ +pr: 105622 +summary: Distinguish different snapshot failures by log level +area: Snapshot/Restore +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java index 3f8b19d72070b..3b2868298cf65 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java @@ -76,6 +76,7 @@ import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.shard.ShardId; @@ -3835,14 +3836,51 @@ private record CreateSnapshotTask( @Override public void onFailure(Exception e) { - logger.warn( - () -> format("[%s][%s] failed to create snapshot", snapshot.getRepository(), snapshot.getSnapshotId().getName()), - e - ); + final var logLevel = snapshotFailureLogLevel(e); + if (logLevel == Level.INFO && logger.isDebugEnabled() == false) { + // suppress stack trace at INFO unless extra verbosity is configured + logger.info( + format( + "[%s][%s] failed to create snapshot: %s", + snapshot.getRepository(), + snapshot.getSnapshotId().getName(), + e.getMessage() + ) + ); + } else { + logger.log( + logLevel, + () -> format("[%s][%s] failed to create snapshot", snapshot.getRepository(), snapshot.getSnapshotId().getName()), + e + ); + } listener.onFailure(e); } } + private static Level snapshotFailureLogLevel(Exception e) { + if (MasterService.isPublishFailureException(e)) { + // no action needed, the new master will take things from here + return Level.INFO; + } else if (e instanceof InvalidSnapshotNameException) { + // no action needed, typically ILM-related, or a user error + return Level.INFO; + } else if (e instanceof IndexNotFoundException) { + // not worrying, most likely a user error + return Level.INFO; + } else if (e instanceof SnapshotException) { + if (e.getMessage().contains(ReferenceDocs.UNASSIGNED_SHARDS.toString())) { + // non-partial snapshot requested but cluster health is not yellow or green; the health is tracked elsewhere so no need to + // make more noise here + return Level.INFO; + } + } else if (e instanceof IllegalArgumentException) { + // some other user error + return Level.INFO; + } + return Level.WARN; + } + private class SnapshotTaskExecutor implements ClusterStateTaskExecutor { @Override public ClusterState execute(BatchExecutionContext batchExecutionContext) throws Exception { diff --git a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java index 54c97ea8dc1a3..edde9f0164a6e 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/SnapshotResiliencyTests.java @@ -8,6 +8,7 @@ package org.elasticsearch.snapshots; +import org.apache.logging.log4j.Level; import org.apache.lucene.util.SetOnce; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionListener; @@ -140,6 +141,7 @@ import org.elasticsearch.gateway.MetaStateService; import org.elasticsearch.gateway.TransportNodesListGatewayStartedShards; import org.elasticsearch.index.Index; +import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.IndexSettingProviders; import org.elasticsearch.index.IndexingPressure; import org.elasticsearch.index.analysis.AnalysisRegistry; @@ -185,6 +187,7 @@ import org.elasticsearch.telemetry.tracing.Tracer; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.MockLogAppender; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.BytesRefRecycler; import org.elasticsearch.transport.DisruptableMockTransport; @@ -232,6 +235,7 @@ import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.iterableWithSize; import static org.hamcrest.Matchers.lessThanOrEqualTo; @@ -1391,7 +1395,6 @@ public void testFullSnapshotUnassignedShards() { final var indices = IntStream.range(0, between(1, 4)).mapToObj(i -> "index-" + i).sorted().toList(); final var repoName = "repo"; - final var originalSnapshotName = "original-snapshot"; var testListener = SubscribableListener @@ -1423,11 +1426,11 @@ public void testFullSnapshotUnassignedShards() { } }) - // Take a full snapshot for use as the source for future clones + // Take the snapshot to check the reaction to having unassigned shards .andThen( (l, ignored) -> client().admin() .cluster() - .prepareCreateSnapshot(repoName, originalSnapshotName) + .prepareCreateSnapshot(repoName, randomIdentifier()) .setWaitForCompletion(randomBoolean()) .execute(new ActionListener<>() { @Override @@ -1451,9 +1454,172 @@ public void onFailure(Exception e) { }) ); - deterministicTaskQueue.runAllRunnableTasks(); - assertTrue("executed all runnable tasks but test steps are still incomplete", testListener.isDone()); - safeAwait(testListener); // shouldn't throw + MockLogAppender.assertThatLogger(() -> { + deterministicTaskQueue.runAllRunnableTasks(); + assertTrue("executed all runnable tasks but test steps are still incomplete", testListener.isDone()); + safeAwait(testListener); // shouldn't throw + }, + SnapshotsService.class, + new MockLogAppender.SeenEventExpectation( + "INFO log", + SnapshotsService.class.getCanonicalName(), + Level.INFO, + "*failed to create snapshot*the following indices have unassigned primary shards*" + ) + ); + } + + public void testSnapshotNameAlreadyInUseExceptionLogging() { + setupTestCluster(1, 1); + + final var repoName = "repo"; + final var snapshotName = "test-snapshot"; + + final var testListener = createRepoAndIndex(repoName, "index", between(1, 2)) + // take snapshot once + .andThen( + (l, ignored) -> client().admin() + .cluster() + .prepareCreateSnapshot(repoName, snapshotName) + .setWaitForCompletion(true) + .execute(l) + ) + // take snapshot again + .andThen( + (l, ignored) -> client().admin() + .cluster() + .prepareCreateSnapshot(repoName, snapshotName) + .setWaitForCompletion(randomBoolean()) + .execute(new ActionListener<>() { + @Override + public void onResponse(CreateSnapshotResponse createSnapshotResponse) { + fail("snapshot should not have started"); + } + + @Override + public void onFailure(Exception e) { + assertThat(ExceptionsHelper.unwrapCause(e), instanceOf(SnapshotNameAlreadyInUseException.class)); + l.onResponse(null); + } + }) + ); + + MockLogAppender.assertThatLogger(() -> { + deterministicTaskQueue.runAllRunnableTasks(); + assertTrue("executed all runnable tasks but test steps are still incomplete", testListener.isDone()); + safeAwait(testListener); // shouldn't throw + }, + SnapshotsService.class, + new MockLogAppender.SeenEventExpectation( + "INFO log", + SnapshotsService.class.getCanonicalName(), + Level.INFO, + Strings.format("*failed to create snapshot*Invalid snapshot name [%s]*", snapshotName) + ) + ); + } + + public void testIndexNotFoundExceptionLogging() { + setupTestCluster(1, 0); // no need for data nodes here + + final var repoName = "repo"; + final var indexName = "does-not-exist"; + + final var testListener = SubscribableListener + // create repo + .newForked( + l -> client().admin() + .cluster() + .preparePutRepository(repoName) + .setType(FsRepository.TYPE) + .setSettings(Settings.builder().put("location", randomAlphaOfLength(10))) + .execute(l) + ) + // take snapshot of index that does not exist + .andThen( + (l, ignored) -> client().admin() + .cluster() + .prepareCreateSnapshot(repoName, randomIdentifier()) + .setIndices(indexName) + .setWaitForCompletion(randomBoolean()) + .execute(new ActionListener<>() { + @Override + public void onResponse(CreateSnapshotResponse createSnapshotResponse) { + fail("snapshot should not have started"); + } + + @Override + public void onFailure(Exception e) { + assertThat(ExceptionsHelper.unwrapCause(e), instanceOf(IndexNotFoundException.class)); + l.onResponse(null); + } + }) + ); + + MockLogAppender.assertThatLogger(() -> { + deterministicTaskQueue.runAllRunnableTasks(); + assertTrue("executed all runnable tasks but test steps are still incomplete", testListener.isDone()); + safeAwait(testListener); // shouldn't throw + }, + SnapshotsService.class, + new MockLogAppender.SeenEventExpectation( + "INFO log", + SnapshotsService.class.getCanonicalName(), + Level.INFO, + Strings.format("failed to create snapshot: no such index [%s]", indexName) + ) + ); + } + + public void testIllegalArgumentExceptionLogging() { + setupTestCluster(1, 0); // no need for data nodes here + + final var repoName = "repo"; + + final var testListener = SubscribableListener + // create repo + .newForked( + l -> client().admin() + .cluster() + .preparePutRepository(repoName) + .setType(FsRepository.TYPE) + .setSettings(Settings.builder().put("location", randomAlphaOfLength(10))) + .execute(l) + ) + // attempt to take snapshot with illegal config ('none' is allowed as a feature state iff it's the only one in the list) + .andThen( + (l, ignored) -> client().admin() + .cluster() + .prepareCreateSnapshot(repoName, randomIdentifier()) + .setFeatureStates("none", "none") + .setWaitForCompletion(randomBoolean()) + .execute(new ActionListener<>() { + @Override + public void onResponse(CreateSnapshotResponse createSnapshotResponse) { + fail("snapshot should not have started"); + } + + @Override + public void onFailure(Exception e) { + assertThat(ExceptionsHelper.unwrapCause(e), instanceOf(IllegalArgumentException.class)); + l.onResponse(null); + } + }) + ); + + MockLogAppender.assertThatLogger(() -> { + deterministicTaskQueue.runAllRunnableTasks(); + assertTrue("executed all runnable tasks but test steps are still incomplete", testListener.isDone()); + safeAwait(testListener); // shouldn't throw + }, + SnapshotsService.class, + new MockLogAppender.SeenEventExpectation( + "INFO log", + SnapshotsService.class.getCanonicalName(), + Level.INFO, + Strings.format("*failed to create snapshot*other feature states were requested: [none, none]", "") + ) + ); } private RepositoryData getRepositoryData(Repository repository) { From 7cbdb6cc197541da584256034ede895d0311a44e Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 21 Feb 2024 07:57:50 +0000 Subject: [PATCH 100/250] Drop dead code from get-snapshots request & response (#105608) Removes all the now-dead code related to reading pre-7.16 get-snapshots requests and responses, and also moves the `XContent` response parsing out of production and into the only test suite that uses it. --- .../http/snapshots/RestGetSnapshotsIT.java | 37 ++++++- .../snapshots/get/GetSnapshotsRequest.java | 101 ++++-------------- .../snapshots/get/GetSnapshotsResponse.java | 75 ++----------- .../elasticsearch/snapshots/SnapshotInfo.java | 13 +-- .../get/GetSnapshotsResponseTests.java | 44 -------- 5 files changed, 64 insertions(+), 206 deletions(-) diff --git a/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/snapshots/RestGetSnapshotsIT.java b/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/snapshots/RestGetSnapshotsIT.java index 5993978f9bd60..e9f4106433771 100644 --- a/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/snapshots/RestGetSnapshotsIT.java +++ b/qa/smoke-test-http/src/javaRestTest/java/org/elasticsearch/http/snapshots/RestGetSnapshotsIT.java @@ -9,6 +9,7 @@ package org.elasticsearch.http.snapshots; import org.apache.http.client.methods.HttpGet; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionFuture; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; @@ -23,6 +24,8 @@ import org.elasticsearch.snapshots.SnapshotInfo; import org.elasticsearch.snapshots.SnapshotsService; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.json.JsonXContent; @@ -31,6 +34,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -444,7 +448,7 @@ private static GetSnapshotsResponse readSnapshotInfos(Response response) throws InputStream input = response.getEntity().getContent(); XContentParser parser = JsonXContent.jsonXContent.createParser(XContentParserConfiguration.EMPTY, input) ) { - return GetSnapshotsResponse.fromXContent(parser); + return GET_SNAPSHOT_PARSER.parse(parser, null); } } @@ -501,4 +505,35 @@ private static GetSnapshotsResponse sortedWithLimit( final Response response = getRestClient().performRequest(request); return readSnapshotInfos(response); } + + private static final int UNKNOWN_COUNT = -1; + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser GET_SNAPSHOT_PARSER = new ConstructingObjectParser<>( + GetSnapshotsResponse.class.getName(), + true, + (args) -> new GetSnapshotsResponse( + (List) args[0], + (Map) args[1], + (String) args[2], + args[3] == null ? UNKNOWN_COUNT : (int) args[3], + args[4] == null ? UNKNOWN_COUNT : (int) args[4] + ) + ); + + static { + GET_SNAPSHOT_PARSER.declareObjectArray( + ConstructingObjectParser.constructorArg(), + (p, c) -> SnapshotInfo.SNAPSHOT_INFO_PARSER.apply(p, c).build(), + new ParseField("snapshots") + ); + GET_SNAPSHOT_PARSER.declareObject( + ConstructingObjectParser.optionalConstructorArg(), + (p, c) -> p.map(HashMap::new, ElasticsearchException::fromXContent), + new ParseField("failures") + ); + GET_SNAPSHOT_PARSER.declareStringOrNull(ConstructingObjectParser.optionalConstructorArg(), new ParseField("next")); + GET_SNAPSHOT_PARSER.declareIntOrNull(ConstructingObjectParser.optionalConstructorArg(), UNKNOWN_COUNT, new ParseField("total")); + GET_SNAPSHOT_PARSER.declareIntOrNull(ConstructingObjectParser.optionalConstructorArg(), UNKNOWN_COUNT, new ParseField("remaining")); + } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java index c3e2dd6e3b536..fda371f9364f9 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsRequest.java @@ -41,18 +41,6 @@ public class GetSnapshotsRequest extends MasterNodeRequest public static final String NO_POLICY_PATTERN = "_none"; public static final boolean DEFAULT_VERBOSE_MODE = true; - public static final TransportVersion SLM_POLICY_FILTERING_VERSION = TransportVersions.V_7_16_0; - - public static final TransportVersion FROM_SORT_VALUE_VERSION = TransportVersions.V_7_16_0; - - public static final TransportVersion MULTIPLE_REPOSITORIES_SUPPORT_ADDED = TransportVersions.V_7_14_0; - - public static final TransportVersion PAGINATED_GET_SNAPSHOTS_VERSION = TransportVersions.V_7_14_0; - - public static final TransportVersion NUMERIC_PAGINATION_VERSION = TransportVersions.V_7_15_0; - - private static final TransportVersion SORT_BY_SHARDS_OR_REPO_VERSION = TransportVersions.V_7_16_0; - private static final TransportVersion INDICES_FLAG_VERSION = TransportVersions.V_8_3_0; public static final int NO_LIMIT = -1; @@ -113,89 +101,36 @@ public GetSnapshotsRequest(String... repositories) { public GetSnapshotsRequest(StreamInput in) throws IOException { super(in); - if (in.getTransportVersion().onOrAfter(MULTIPLE_REPOSITORIES_SUPPORT_ADDED)) { - repositories = in.readStringArray(); - } else { - repositories = new String[] { in.readString() }; - } + repositories = in.readStringArray(); snapshots = in.readStringArray(); ignoreUnavailable = in.readBoolean(); verbose = in.readBoolean(); - if (in.getTransportVersion().onOrAfter(PAGINATED_GET_SNAPSHOTS_VERSION)) { - after = in.readOptionalWriteable(After::new); - sort = in.readEnum(SortBy.class); - size = in.readVInt(); - order = SortOrder.readFromStream(in); - if (in.getTransportVersion().onOrAfter(NUMERIC_PAGINATION_VERSION)) { - offset = in.readVInt(); - } - if (in.getTransportVersion().onOrAfter(SLM_POLICY_FILTERING_VERSION)) { - policies = in.readStringArray(); - } - if (in.getTransportVersion().onOrAfter(FROM_SORT_VALUE_VERSION)) { - fromSortValue = in.readOptionalString(); - } - if (in.getTransportVersion().onOrAfter(INDICES_FLAG_VERSION)) { - includeIndexNames = in.readBoolean(); - } + after = in.readOptionalWriteable(After::new); + sort = in.readEnum(SortBy.class); + size = in.readVInt(); + order = SortOrder.readFromStream(in); + offset = in.readVInt(); + policies = in.readStringArray(); + fromSortValue = in.readOptionalString(); + if (in.getTransportVersion().onOrAfter(INDICES_FLAG_VERSION)) { + includeIndexNames = in.readBoolean(); } } @Override public void writeTo(StreamOutput out) throws IOException { super.writeTo(out); - if (out.getTransportVersion().onOrAfter(MULTIPLE_REPOSITORIES_SUPPORT_ADDED)) { - out.writeStringArray(repositories); - } else { - if (repositories.length != 1) { - throw new IllegalArgumentException( - "Requesting snapshots from multiple repositories is not supported in versions prior " - + "to " - + MULTIPLE_REPOSITORIES_SUPPORT_ADDED.toString() - ); - } - out.writeString(repositories[0]); - } + out.writeStringArray(repositories); out.writeStringArray(snapshots); out.writeBoolean(ignoreUnavailable); out.writeBoolean(verbose); - if (out.getTransportVersion().onOrAfter(PAGINATED_GET_SNAPSHOTS_VERSION)) { - out.writeOptionalWriteable(after); - if ((sort == SortBy.SHARDS || sort == SortBy.FAILED_SHARDS || sort == SortBy.REPOSITORY) - && out.getTransportVersion().before(SORT_BY_SHARDS_OR_REPO_VERSION)) { - throw new IllegalArgumentException( - "can't use sort by shard count or repository name in transport version [" + out.getTransportVersion() + "]" - ); - } - out.writeEnum(sort); - out.writeVInt(size); - order.writeTo(out); - if (out.getTransportVersion().onOrAfter(NUMERIC_PAGINATION_VERSION)) { - out.writeVInt(offset); - } else if (offset != 0) { - throw new IllegalArgumentException( - "can't use numeric offset in get snapshots request in transport version [" + out.getTransportVersion() + "]" - ); - } - } else if (sort != SortBy.START_TIME || size != NO_LIMIT || after != null || order != SortOrder.ASC) { - throw new IllegalArgumentException( - "can't use paginated get snapshots request in transport version [" + out.getTransportVersion() + "]" - ); - } - if (out.getTransportVersion().onOrAfter(SLM_POLICY_FILTERING_VERSION)) { - out.writeStringArray(policies); - } else if (policies.length > 0) { - throw new IllegalArgumentException( - "can't use slm policy filter in snapshots request in transport version [" + out.getTransportVersion() + "]" - ); - } - if (out.getTransportVersion().onOrAfter(FROM_SORT_VALUE_VERSION)) { - out.writeOptionalString(fromSortValue); - } else if (fromSortValue != null) { - throw new IllegalArgumentException( - "can't use after-value in snapshot request in transport version [" + out.getTransportVersion() + "]" - ); - } + out.writeOptionalWriteable(after); + out.writeEnum(sort); + out.writeVInt(size); + order.writeTo(out); + out.writeVInt(offset); + out.writeStringArray(policies); + out.writeOptionalString(fromSortValue); if (out.getTransportVersion().onOrAfter(INDICES_FLAG_VERSION)) { out.writeBoolean(includeIndexNames); } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java index 3257ed1b986c3..85c2ff2806ace 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponse.java @@ -17,14 +17,10 @@ import org.elasticsearch.common.xcontent.ChunkedToXContentObject; import org.elasticsearch.core.Nullable; import org.elasticsearch.snapshots.SnapshotInfo; -import org.elasticsearch.xcontent.ConstructingObjectParser; -import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -35,37 +31,6 @@ */ public class GetSnapshotsResponse extends ActionResponse implements ChunkedToXContentObject { - private static final int UNKNOWN_COUNT = -1; - - @SuppressWarnings("unchecked") - private static final ConstructingObjectParser GET_SNAPSHOT_PARSER = new ConstructingObjectParser<>( - GetSnapshotsResponse.class.getName(), - true, - (args) -> new GetSnapshotsResponse( - (List) args[0], - (Map) args[1], - (String) args[2], - args[3] == null ? UNKNOWN_COUNT : (int) args[3], - args[4] == null ? UNKNOWN_COUNT : (int) args[4] - ) - ); - - static { - GET_SNAPSHOT_PARSER.declareObjectArray( - ConstructingObjectParser.constructorArg(), - (p, c) -> SnapshotInfo.SNAPSHOT_INFO_PARSER.apply(p, c).build(), - new ParseField("snapshots") - ); - GET_SNAPSHOT_PARSER.declareObject( - ConstructingObjectParser.optionalConstructorArg(), - (p, c) -> p.map(HashMap::new, ElasticsearchException::fromXContent), - new ParseField("failures") - ); - GET_SNAPSHOT_PARSER.declareStringOrNull(ConstructingObjectParser.optionalConstructorArg(), new ParseField("next")); - GET_SNAPSHOT_PARSER.declareIntOrNull(ConstructingObjectParser.optionalConstructorArg(), UNKNOWN_COUNT, new ParseField("total")); - GET_SNAPSHOT_PARSER.declareIntOrNull(ConstructingObjectParser.optionalConstructorArg(), UNKNOWN_COUNT, new ParseField("remaining")); - } - private final List snapshots; private final Map failures; @@ -93,21 +58,10 @@ public GetSnapshotsResponse( public GetSnapshotsResponse(StreamInput in) throws IOException { this.snapshots = in.readCollectionAsImmutableList(SnapshotInfo::readFrom); - if (in.getTransportVersion().onOrAfter(GetSnapshotsRequest.MULTIPLE_REPOSITORIES_SUPPORT_ADDED)) { - final Map failedResponses = in.readMap(StreamInput::readException); - this.failures = Collections.unmodifiableMap(failedResponses); - this.next = in.readOptionalString(); - } else { - this.failures = Collections.emptyMap(); - this.next = null; - } - if (in.getTransportVersion().onOrAfter(GetSnapshotsRequest.NUMERIC_PAGINATION_VERSION)) { - this.total = in.readVInt(); - this.remaining = in.readVInt(); - } else { - this.total = UNKNOWN_COUNT; - this.remaining = UNKNOWN_COUNT; - } + this.failures = Collections.unmodifiableMap(in.readMap(StreamInput::readException)); + this.next = in.readOptionalString(); + this.total = in.readVInt(); + this.remaining = in.readVInt(); } /** @@ -149,19 +103,10 @@ public int remaining() { @Override public void writeTo(StreamOutput out) throws IOException { out.writeCollection(snapshots); - if (out.getTransportVersion().onOrAfter(GetSnapshotsRequest.MULTIPLE_REPOSITORIES_SUPPORT_ADDED)) { - out.writeMap(failures, StreamOutput::writeException); - out.writeOptionalString(next); - } else { - if (failures.isEmpty() == false) { - assert false : "transport action should have thrown directly for old version but saw " + failures; - throw failures.values().iterator().next(); - } - } - if (out.getTransportVersion().onOrAfter(GetSnapshotsRequest.NUMERIC_PAGINATION_VERSION)) { - out.writeVInt(total); - out.writeVInt(remaining); - } + out.writeMap(failures, StreamOutput::writeException); + out.writeOptionalString(next); + out.writeVInt(total); + out.writeVInt(remaining); } @Override @@ -198,10 +143,6 @@ public Iterator toXContentChunked(ToXContent.Params params) { })); } - public static GetSnapshotsResponse fromXContent(XContentParser parser) throws IOException { - return GET_SNAPSHOT_PARSER.parse(parser, null); - } - @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java index c09719ec48039..243df88cfab00 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java @@ -496,12 +496,7 @@ public SnapshotInfo maybeWithoutIndices(boolean retainIndices) { * Constructs snapshot information from stream input */ public static SnapshotInfo readFrom(final StreamInput in) throws IOException { - final Snapshot snapshot; - if (in.getTransportVersion().onOrAfter(GetSnapshotsRequest.PAGINATED_GET_SNAPSHOTS_VERSION)) { - snapshot = new Snapshot(in); - } else { - snapshot = new Snapshot(UNKNOWN_REPO_NAME, new SnapshotId(in)); - } + final Snapshot snapshot = new Snapshot(in); final List indices = in.readStringCollectionAsImmutableList(); final SnapshotState state = in.readBoolean() ? SnapshotState.fromValue(in.readByte()) : null; final String reason = in.readOptionalString(); @@ -1015,11 +1010,7 @@ public static SnapshotInfo fromXContentInternal(final String repoName, final XCo @Override public void writeTo(final StreamOutput out) throws IOException { - if (out.getTransportVersion().onOrAfter(GetSnapshotsRequest.PAGINATED_GET_SNAPSHOTS_VERSION)) { - snapshot.writeTo(out); - } else { - snapshot.getSnapshotId().writeTo(out); - } + snapshot.writeTo(out); out.writeStringCollection(indices); if (state != null) { out.writeBoolean(true); diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java index f64f6a5d8275b..32a72bd0f7a76 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/snapshots/get/GetSnapshotsResponseTests.java @@ -10,7 +10,6 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.TransportVersion; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamOutput; @@ -24,9 +23,6 @@ import org.elasticsearch.snapshots.SnapshotShardFailure; import org.elasticsearch.test.AbstractChunkedSerializingTestCase; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xcontent.ToXContent; -import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xcontent.XContentType; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -39,11 +35,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Predicate; -import java.util.regex.Pattern; -import static org.elasticsearch.snapshots.SnapshotInfo.INDEX_DETAILS_XCONTENT_PARAM; -import static org.elasticsearch.test.AbstractXContentTestCase.chunkedXContentTester; import static org.hamcrest.CoreMatchers.containsString; public class GetSnapshotsResponseTests extends ESTestCase { @@ -53,10 +45,6 @@ public class GetSnapshotsResponseTests extends ESTestCase { // It does not override equals and hashCode, because it // contains ElasticsearchException, which does not override equals and hashCode. - private GetSnapshotsResponse doParseInstance(XContentParser parser) throws IOException { - return GetSnapshotsResponse.fromXContent(parser); - } - private GetSnapshotsResponse copyInstance(GetSnapshotsResponse instance) throws IOException { return copyInstance( instance, @@ -146,38 +134,6 @@ public void testSerialization() throws IOException { assertEqualInstances(testInstance, deserializedInstance); } - public void testFromXContent() throws IOException { - // Explicitly include the index details, excluded by default, since this is required for a faithful round-trip - final ToXContent.MapParams params = new ToXContent.MapParams(Map.of(INDEX_DETAILS_XCONTENT_PARAM, "true")); - - // Don't inject random fields into the custom snapshot metadata, because the metadata map is equality-checked after doing a - // round-trip through xContent serialization/deserialization. Even though the rest of the object ignores unknown fields, - // `metadata` doesn't ignore unknown fields (it just includes them in the parsed object, because the keys are arbitrary), - // so any new fields added to the metadata before it gets deserialized that weren't in the serialized version will - // cause the equality check to fail. - // - // Also don't inject random junk into the index details section, since this is keyed by index name but the values - // are required to be a valid IndexSnapshotDetails - // - // The actual fields are nested in an array, so this regex matches fields with names of the form - // `snapshots.3.metadata` - final Predicate predicate = Pattern.compile("snapshots\\.\\d+\\.metadata.*") - .asMatchPredicate() - .or(Pattern.compile("snapshots\\.\\d+\\.index_details").asMatchPredicate()) - .or(Pattern.compile("failures\\.*").asMatchPredicate()); - chunkedXContentTester(this::createParser, (XContentType t) -> createTestInstance(), params, this::doParseInstance).numberOfTestRuns( - 1 - ) - .supportsUnknownFields(true) - .shuffleFieldsExceptions(Strings.EMPTY_ARRAY) - .randomFieldsExcludeFilter(predicate) - .assertEqualsConsumer(this::assertEqualInstances) - // We set it to false, because GetSnapshotsResponse contains - // ElasticsearchException, whose xContent creation/parsing are not stable. - .assertToXContentEquivalence(false) - .test(); - } - public void testChunking() { AbstractChunkedSerializingTestCase.assertChunkCount(createTestInstance(), response -> response.getSnapshots().size() + 2); } From f06a580eb7a84144707d6ef761218dc44604dba4 Mon Sep 17 00:00:00 2001 From: Luigi Dell'Aquila Date: Wed, 21 Feb 2024 09:25:25 +0100 Subject: [PATCH 101/250] ES|QL: Set default query LIMIT to 1000 (#105618) --- docs/reference/esql/esql-get-started.asciidoc | 8 +- docs/reference/esql/esql-kibana.asciidoc | 2 +- docs/reference/esql/esql-limitations.asciidoc | 2 +- .../esql/source-commands/from.asciidoc | 4 +- .../xpack/esql/qa/rest/RestEsqlTestCase.java | 5 +- .../xpack/esql/EsqlTestUtils.java | 2 +- .../xpack/esql/action/EsqlActionTaskIT.java | 2 +- .../xpack/esql/plugin/EsqlPlugin.java | 2 +- .../LocalPhysicalPlanOptimizerTests.java | 36 +++---- .../optimizer/LogicalPlanOptimizerTests.java | 90 ++++++++-------- .../optimizer/PhysicalPlanOptimizerTests.java | 54 +++++----- .../rest-api-spec/test/esql/100_bug_fix.yml | 15 +-- .../rest-api-spec/test/esql/20_aggs.yml | 98 ++++++++--------- .../rest-api-spec/test/esql/30_types.yml | 102 +++++++++--------- .../test/esql/90_non_indexed.yml | 9 +- 15 files changed, 218 insertions(+), 213 deletions(-) diff --git a/docs/reference/esql/esql-get-started.asciidoc b/docs/reference/esql/esql-get-started.asciidoc index 631a961b023ab..4dae9ffcddd7f 100644 --- a/docs/reference/esql/esql-get-started.asciidoc +++ b/docs/reference/esql/esql-get-started.asciidoc @@ -37,7 +37,7 @@ image::images/esql/source-command.svg[A source command producing a table from {e The <> source command returns a table with documents from a data stream, index, or alias. Each row in the resulting table represents a document. -This query returns up to 500 documents from the `sample_data` index: +This query returns up to 1000 documents from the `sample_data` index: [source,esql] ---- @@ -237,7 +237,7 @@ include::{esql-specs}/eval.csv-spec[tag=gs-eval-stats-backticks] To track statistics over time, {esql} enables you to create histograms using the <> function. `AUTO_BUCKET` creates human-friendly bucket sizes and returns a value for each row that corresponds to the resulting bucket the -row falls into. +row falls into. For example, to create hourly buckets for the data on October 23rd: @@ -272,7 +272,7 @@ image::images/esql/esql-enrich.png[align="center"] Before you can use `ENRICH`, you first need to <> and <> -an <>. +an <>. include::{es-repo-dir}/tab-widgets/esql/esql-getting-started-widget-enrich-policy.asciidoc[] @@ -344,4 +344,4 @@ For more about data processing with {esql}, refer to [[esql-getting-learn-more]] === Learn more -To learn more about {esql}, refer to <> and <>. \ No newline at end of file +To learn more about {esql}, refer to <> and <>. diff --git a/docs/reference/esql/esql-kibana.asciidoc b/docs/reference/esql/esql-kibana.asciidoc index 07502add5a620..67827d32ce29c 100644 --- a/docs/reference/esql/esql-kibana.asciidoc +++ b/docs/reference/esql/esql-kibana.asciidoc @@ -103,7 +103,7 @@ detailed warning, expand the query bar, and click *warnings*. === The results table For the example query, the results table shows 10 rows. Omitting the `LIMIT` -command, the results table defaults to up to 500 rows. Using `LIMIT`, you can +command, the results table defaults to up to 1000 rows. Using `LIMIT`, you can increase the limit to up to 10,000 rows. NOTE: the 10,000 row limit only applies to the number of rows that are retrieved diff --git a/docs/reference/esql/esql-limitations.asciidoc b/docs/reference/esql/esql-limitations.asciidoc index 94bd38cd0ec28..788177df64dc9 100644 --- a/docs/reference/esql/esql-limitations.asciidoc +++ b/docs/reference/esql/esql-limitations.asciidoc @@ -9,7 +9,7 @@ [[esql-max-rows]] === Result set size limit -By default, an {esql} query returns up to 500 rows. You can increase the number +By default, an {esql} query returns up to 1000 rows. You can increase the number of rows up to 10,000 using the <> command. include::processing-commands/limit.asciidoc[tag=limitation] diff --git a/docs/reference/esql/source-commands/from.asciidoc b/docs/reference/esql/source-commands/from.asciidoc index 5263a17b48df9..dbb5010060257 100644 --- a/docs/reference/esql/source-commands/from.asciidoc +++ b/docs/reference/esql/source-commands/from.asciidoc @@ -26,7 +26,7 @@ corresponds to a field, and can be accessed by the name of that field. [NOTE] ==== By default, an {esql} query without an explicit <> uses an implicit -limit of 500. This applies to `FROM` too. A `FROM` command without `LIMIT`: +limit of 1000. This applies to `FROM` too. A `FROM` command without `LIMIT`: [source,esql] ---- @@ -38,7 +38,7 @@ is executed as: [source,esql] ---- FROM employees -| LIMIT 500 +| LIMIT 1000 ---- ==== diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java index 6dea60476c3d1..cccd1a3f8854b 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RestEsqlTestCase.java @@ -817,7 +817,10 @@ private static HttpEntity assertWarnings(Response response, List allowed } private static Set mutedWarnings() { - return Set.of("No limit defined, adding default limit of [500]"); + return Set.of( + "No limit defined, adding default limit of [1000]", + "No limit defined, adding default limit of [500]" // this is for bwc tests, the limit in v 8.12.x is 500 + ); } private static void bulkLoadTestData(int count) throws IOException { diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java index bcfe5ce9787ad..8c5c79b98767e 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/EsqlTestUtils.java @@ -185,7 +185,7 @@ public static List> getValuesList(Iterator> values public static List withDefaultLimitWarning(List warnings) { List result = warnings == null ? new ArrayList<>() : new ArrayList<>(warnings); - result.add("No limit defined, adding default limit of [500]"); + result.add("No limit defined, adding default limit of [1000]"); return result; } diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionTaskIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionTaskIT.java index 276539a5bbeac..60f174773a1b8 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionTaskIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/EsqlActionTaskIT.java @@ -71,7 +71,7 @@ public void setup() { \\_ExchangeSourceOperator[] \\_AggregationOperator[mode = FINAL, aggs = sum of longs] \\_ProjectOperator[projection = [0]] - \\_LimitOperator[limit = 500] + \\_LimitOperator[limit = 1000] \\_OutputOperator[columns = [sum(pause_me)]]"""; } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java index 17dad71401119..14ebf3da2cd7e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java @@ -79,7 +79,7 @@ public class EsqlPlugin extends Plugin implements ActionPlugin { public static final Setting QUERY_RESULT_TRUNCATION_DEFAULT_SIZE = Setting.intSetting( "esql.query.result_truncation_default_size", - 500, + 1000, 1, 10000, Setting.Property.NodeScope, diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index 80cc7d9a52a4b..55320cfbeca32 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -152,7 +152,7 @@ private Analyzer makeAnalyzer(String mappingFileName, EnrichResolution enrichRes /** * Expects - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[COUNT([2a][KEYWORD]) AS c],FINAL,null] * \_ExchangeExec[[count{r}#24, seen{r}#25],true] * \_EsStatsQueryExec[test], stats[Stat[name=*, type=COUNT, query=null]]], query[{"esql_single_value":{"field":"emp_no","next": @@ -171,7 +171,7 @@ public void testCountAllWithEval() { /** * Expects - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[COUNT([2a][KEYWORD]) AS c],FINAL,null] * \_ExchangeExec[[count{r}#14, seen{r}#15],true] * \_EsStatsQueryExec[test], stats[Stat[name=*, type=COUNT, query=null]]], @@ -187,7 +187,7 @@ public void testCountAllWithFilter() { /** * Expects - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[COUNT(emp_no{f}#5) AS c],FINAL,null] * \_ExchangeExec[[count{r}#15, seen{r}#16],true] * \_EsStatsQueryExec[test], stats[Stat[name=emp_no, type=COUNT, query={ @@ -207,7 +207,7 @@ public void testCountFieldWithFilter() { /** * Expects - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[COUNT(salary{f}#20) AS c],FINAL,null] * \_ExchangeExec[[count{r}#25, seen{r}#26],true] * \_EsStatsQueryExec[test], stats[Stat[name=salary, type=COUNT, query={ @@ -302,7 +302,7 @@ public void testAnotherCountAllWithFilter() { /** * Expected * ProjectExec[[c{r}#3, c{r}#3 AS call, c_literal{r}#7]] - * \_LimitExec[500[INTEGER]] + * \_LimitExec[1000[INTEGER]] * \_AggregateExec[[],[COUNT([2a][KEYWORD]) AS c, COUNT(1[INTEGER]) AS c_literal],FINAL,null] * \_ExchangeExec[[count{r}#18, seen{r}#19, count{r}#20, seen{r}#21],true] * \_EsStatsQueryExec[test], stats[Stat[name=*, type=COUNT, query=null], Stat[name=*, type=COUNT, query=null]]], @@ -346,7 +346,7 @@ public void testCountFieldsAndAllWithFilter() { /** * Expecting - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[COUNT([2a][KEYWORD]) AS c],FINAL,null] * \_ExchangeExec[[count{r}#14, seen{r}#15],true] * \_LocalSourceExec[[c{r}#3],[LongVectorBlock[vector=ConstantLongVector[positions=1, value=0]]]] @@ -377,12 +377,12 @@ public boolean exists(String field) { /** * Expects - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_ExchangeExec[[],false] * \_ProjectExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, job{f}#10, job.raw{f}#11, languages{f}#6, last_n * ame{f}#7, long_noidx{f}#12, salary{f}#8]] * \_FieldExtractExec[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gen..] - * \_EsQueryExec[test], query[{"exists":{"field":"emp_no","boost":1.0}}][_doc{f}#13], limit[500], sort[] estimatedRowSize[324] + * \_EsQueryExec[test], query[{"exists":{"field":"emp_no","boost":1.0}}][_doc{f}#13], limit[1000], sort[] estimatedRowSize[324] */ public void testIsNotNullPushdownFilter() { var plan = plan("from test | where emp_no is not null"); @@ -392,20 +392,20 @@ public void testIsNotNullPushdownFilter() { var project = as(exchange.child(), ProjectExec.class); var field = as(project.child(), FieldExtractExec.class); var query = as(field.child(), EsQueryExec.class); - assertThat(query.limit().fold(), is(500)); + assertThat(query.limit().fold(), is(1000)); var expected = QueryBuilders.existsQuery("emp_no"); assertThat(query.query().toString(), is(expected.toString())); } /** * Expects - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_ExchangeExec[[],false] * \_ProjectExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, job{f}#10, job.raw{f}#11, languages{f}#6, last_n * ame{f}#7, long_noidx{f}#12, salary{f}#8]] * \_FieldExtractExec[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gen..] * \_EsQueryExec[test], query[{"bool":{"must_not":[{"exists":{"field":"emp_no","boost":1.0}}],"boost":1.0}}][_doc{f}#13], - * limit[500], sort[] estimatedRowSize[324] + * limit[1000], sort[] estimatedRowSize[324] */ public void testIsNullPushdownFilter() { var plan = plan("from test | where emp_no is null"); @@ -415,21 +415,21 @@ public void testIsNullPushdownFilter() { var project = as(exchange.child(), ProjectExec.class); var field = as(project.child(), FieldExtractExec.class); var query = as(field.child(), EsQueryExec.class); - assertThat(query.limit().fold(), is(500)); + assertThat(query.limit().fold(), is(1000)); var expected = QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery("emp_no")); assertThat(query.query().toString(), is(expected.toString())); } /** * Expects - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_ExchangeExec[[],false] * \_ProjectExec[[!alias_integer, boolean{f}#4, byte{f}#5, constant_keyword-foo{f}#6, date{f}#7, double{f}#8, float{f}#9, * half_float{f}#10, integer{f}#12, ip{f}#13, keyword{f}#14, long{f}#15, scaled_float{f}#11, short{f}#17, text{f}#18, * unsigned_long{f}#16, version{f}#19, wildcard{f}#20]] * \_FieldExtractExec[!alias_integer, boolean{f}#4, byte{f}#5, constant_k..][] * \_EsQueryExec[test], query[{"esql_single_value":{"field":"ip","next":{"terms":{"ip":["127.0.0.0/24"],"boost":1.0}},"source": - * "cidr_match(ip, \"127.0.0.0/24\")@1:19"}}][_doc{f}#21], limit[500], sort[] estimatedRowSize[389] + * "cidr_match(ip, \"127.0.0.0/24\")@1:19"}}][_doc{f}#21], limit[1000], sort[] estimatedRowSize[389] */ public void testCidrMatchPushdownFilter() { var allTypeMappingAnalyzer = makeAnalyzer("mapping-ip.json", new EnrichResolution()); @@ -451,7 +451,7 @@ public void testCidrMatchPushdownFilter() { var project = as(exchange.child(), ProjectExec.class); var field = as(project.child(), FieldExtractExec.class); var queryExec = as(field.child(), EsQueryExec.class); - assertThat(queryExec.limit().fold(), is(500)); + assertThat(queryExec.limit().fold(), is(1000)); var expectedInnerQuery = QueryBuilders.termsQuery(fieldName, cidrBlocks); var expectedQuery = wrapWithSingleQuery(expectedInnerQuery, fieldName, new Source(1, 18, cidrMatch)); @@ -549,7 +549,7 @@ public void testOutOfRangeFilterPushdown() { /** * Expects e.g. - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_ExchangeExec[[],false] * \_ProjectExec[[!alias_integer, boolean{f}#190, byte{f}#191, constant_keyword-foo{f}#192, date{f}#193, double{f}#194, ...]] * \_FieldExtractExec[!alias_integer, boolean{f}#190, byte{f}#191, consta..][] @@ -569,13 +569,13 @@ private EsQueryExec doTestOutOfRangeFilterPushdown(String query, Analyzer analyz /** * Expects - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_ExchangeExec[[],false] * \_ProjectExec[[_meta_field{f}#8, emp_no{r}#2, first_name{r}#3, gender{f}#4, job{f}#9, job.raw{f}#10, languages{f}#5, first_n * ame{r}#3 AS last_name, long_noidx{f}#11, emp_no{r}#2 AS salary]] * \_FieldExtractExec[_meta_field{f}#8, gender{f}#4, job{f}#9, job.raw{f}..] * \_EvalExec[[null[INTEGER] AS emp_no, null[KEYWORD] AS first_name]] - * \_EsQueryExec[test], query[][_doc{f}#12], limit[500], sort[] estimatedRowSize[270] + * \_EsQueryExec[test], query[][_doc{f}#12], limit[1000], sort[] estimatedRowSize[270] */ public void testMissingFieldsDoNotGetExtracted() { var stats = EsqlTestUtils.statsForMissingField("first_name", "last_name", "emp_no", "salary"); 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 e04344ca86732..9dfcffbf48e6e 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 @@ -238,7 +238,7 @@ public void testCombineProjectionWithAggregation() { /** * Project[[s{r}#4 AS d, s{r}#4, last_name{f}#21, first_name{f}#18]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_Aggregate[[last_name{f}#21, first_name{f}#18],[SUM(salary{f}#22) AS s, last_name{f}#21, first_name{f}#18]] * \_EsRelation[test][_meta_field{f}#23, emp_no{f}#17, first_name{f}#18, ..] */ @@ -297,7 +297,7 @@ public void testCombineProjectionWithPruning() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[f{r}#7],[SUM(emp_no{f}#15) AS s, COUNT(first_name{f}#16) AS c, first_name{f}#16 AS f]] * \_EsRelation[test][_meta_field{f}#21, emp_no{f}#15, first_name{f}#16, ..] */ @@ -327,7 +327,7 @@ public void testCombineProjectionWithAggregationFirstAndAliasedGroupingUsedInAgg /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[f{r}#7],[SUM(emp_no{f}#15) AS s, first_name{f}#16 AS f]] * \_EsRelation[test][_meta_field{f}#21, emp_no{f}#15, first_name{f}#16, ..] */ @@ -987,9 +987,9 @@ public void testCombineOrderByThroughFilter() { /** * Expected - * TopN[[Order[first_name{f}#170,ASC,LAST]],500[INTEGER]] + * TopN[[Order[first_name{f}#170,ASC,LAST]],1000[INTEGER]] * \_MvExpand[first_name{f}#170] - * \_TopN[[Order[emp_no{f}#169,ASC,LAST]],500[INTEGER]] + * \_TopN[[Order[emp_no{f}#169,ASC,LAST]],1000[INTEGER]] * \_EsRelation[test][avg_worked_seconds{f}#167, birth_date{f}#168, emp_n..] */ public void testDontCombineOrderByThroughMvExpand() { @@ -1009,10 +1009,10 @@ public void testDontCombineOrderByThroughMvExpand() { /** * Expected - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_MvExpand[x{r}#159] * \_EsqlProject[[first_name{f}#162 AS x]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_EsRelation[test][first_name{f}#162] */ public void testCopyDefaultLimitPastMvExpand() { @@ -1969,7 +1969,7 @@ public void testPruneChainedEval() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[],[COUNT(salary{f}#1345) AS c]] * \_EsRelation[test][_meta_field{f}#1346, emp_no{f}#1340, first_name{f}#..] */ @@ -2008,7 +2008,7 @@ public void testPruneUnusedAggSimple() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[],[COUNT(salary{f}#19) AS x]] * \_EsRelation[test][_meta_field{f}#20, emp_no{f}#14, first_name{f}#15, ..] */ @@ -2053,7 +2053,7 @@ public void testPruneUnusedAggsChainedAgg() { /** * Expects * Project[[c{r}#342]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_Filter[min{r}#348 > 10[INTEGER]] * \_Aggregate[[],[COUNT(salary{f}#367) AS c, MIN(salary{f}#367) AS min]] * \_EsRelation[test][_meta_field{f}#368, emp_no{f}#362, first_name{f}#36..] @@ -2084,7 +2084,7 @@ public void testPruneMixedAggInsideUnusedEval() { /** * Expects * Eval[[max{r}#6 + min{r}#9 + c{r}#3 AS x, min{r}#9 AS y, c{r}#3 AS z]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_Aggregate[[],[COUNT(salary{f}#26) AS c, MAX(salary{f}#26) AS max, MIN(salary{f}#26) AS min]] * \_EsRelation[test][_meta_field{f}#27, emp_no{f}#21, first_name{f}#22, ..] */ @@ -2106,7 +2106,7 @@ public void testNoPruningWhenDealingJustWithEvals() { * Expects * Project[[y{r}#6 AS z]] * \_Eval[[emp_no{f}#11 + 1[INTEGER] AS y]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_EsRelation[test][_meta_field{f}#17, emp_no{f}#11, first_name{f}#12, ..] */ public void testNoPruningWhenChainedEvals() { @@ -2127,7 +2127,7 @@ public void testNoPruningWhenChainedEvals() { /** * Expects * Project[[salary{f}#20 AS x, emp_no{f}#15 AS y]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_EsRelation[test][_meta_field{f}#21, emp_no{f}#15, first_name{f}#16, ..] */ public void testPruningDuplicateEvals() { @@ -2153,7 +2153,7 @@ public void testPruningDuplicateEvals() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[],[COUNT(salary{f}#24) AS cx, COUNT(emp_no{f}#19) AS cy]] * \_EsRelation[test][_meta_field{f}#25, emp_no{f}#19, first_name{f}#20, ..] */ @@ -2177,7 +2177,7 @@ public void testPruneEvalAliasOnAggUngrouped() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[x{r}#6],[COUNT(emp_no{f}#17) AS cy, salary{f}#22 AS x]] * \_EsRelation[test][_meta_field{f}#23, emp_no{f}#17, first_name{f}#18, ..] */ @@ -2202,7 +2202,7 @@ public void testPruneEvalAliasOnAggGroupedByAlias() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[gender{f}#22],[COUNT(emp_no{f}#20) AS cy, MIN(salary{f}#25) AS cx, gender{f}#22]] * \_EsRelation[test][_meta_field{f}#26, emp_no{f}#20, first_name{f}#21, ..] */ @@ -2228,7 +2228,7 @@ public void testPruneEvalAliasOnAggGrouped() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[gender{f}#21],[COUNT(emp_no{f}#19) AS cy, MIN(salary{f}#24) AS cx, gender{f}#21]] * \_EsRelation[test][_meta_field{f}#25, emp_no{f}#19, first_name{f}#20, ..] */ @@ -2254,7 +2254,7 @@ public void testPruneEvalAliasMixedWithRenameOnAggGrouped() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[gender{f}#19],[COUNT(x{r}#3) AS cy, MIN(x{r}#3) AS cx, gender{f}#19]] * \_Eval[[emp_no{f}#17 + 1[INTEGER] AS x]] * \_EsRelation[test][_meta_field{f}#23, emp_no{f}#17, first_name{f}#18, ..] @@ -2283,7 +2283,7 @@ public void testEvalAliasingAcrossCommands() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[gender{f}#19],[COUNT(x{r}#3) AS cy, MIN(x{r}#3) AS cx, gender{f}#19]] * \_Eval[[emp_no{f}#17 + 1[INTEGER] AS x]] * \_EsRelation[test][_meta_field{f}#23, emp_no{f}#17, first_name{f}#18, ..] @@ -2310,7 +2310,7 @@ public void testEvalAliasingInsideSameCommand() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[gender{f}#22],[COUNT(z{r}#9) AS cy, MIN(x{r}#3) AS cx, gender{f}#22]] * \_Eval[[emp_no{f}#20 + 1[INTEGER] AS x, x{r}#3 + 1[INTEGER] AS z]] * \_EsRelation[test][_meta_field{f}#26, emp_no{f}#20, first_name{f}#21, ..] @@ -2355,7 +2355,7 @@ public void testPruneRenameOnAgg() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[gender{f}#14],[COUNT(salary{f}#17) AS cy, MIN(emp_no{f}#12) AS cx, gender{f}#14]] * \_EsRelation[test][_meta_field{f}#18, emp_no{f}#12, first_name{f}#13, ..] */ @@ -2383,7 +2383,7 @@ public void testPruneRenameOnAggBy() { * Expects * Project[[c1{r}#2, c2{r}#4, cs{r}#6, cm{r}#8, cexp{r}#10]] * \_Eval[[c1{r}#2 AS c2, c1{r}#2 AS cs, c1{r}#2 AS cm, c1{r}#2 AS cexp]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_Aggregate[[],[COUNT([2a][KEYWORD]) AS c1]] * \_EsRelation[test][_meta_field{f}#17, emp_no{f}#11, first_name{f}#12, ..] */ @@ -2414,7 +2414,7 @@ public void testEliminateDuplicateAggsCountAll() { * Expects * Project[[c1{r}#7, cx{r}#10, cs{r}#12, cy{r}#15]] * \_Eval[[c1{r}#7 AS cx, c1{r}#7 AS cs, c1{r}#7 AS cy]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_Aggregate[[],[COUNT([2a][KEYWORD]) AS c1]] * \_EsRelation[test][_meta_field{f}#22, emp_no{f}#16, first_name{f}#17, ..] */ @@ -2446,7 +2446,7 @@ public void testEliminateDuplicateAggsWithAliasedFields() { /** * Expects * Project[[min{r}#1385, max{r}#1388, min{r}#1385 AS min2, max{r}#1388 AS max2, gender{f}#1398]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_Aggregate[[gender{f}#1398],[MIN(salary{f}#1401) AS min, MAX(salary{f}#1401) AS max, gender{f}#1398]] * \_EsRelation[test][_meta_field{f}#1402, emp_no{f}#1396, first_name{f}#..] */ @@ -2492,7 +2492,7 @@ public void testEliminateDuplicateAggWithNull() { /** * Expects * Project[[max(x){r}#11, max(x){r}#11 AS max(y), max(x){r}#11 AS max(z)]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_Aggregate[[],[MAX(salary{f}#21) AS max(x)]] * \_EsRelation[test][_meta_field{f}#22, emp_no{f}#16, first_name{f}#17, ..] */ @@ -2546,7 +2546,7 @@ public void testMvExpandFoldable() { /** * Expected - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[a{r}#2],[COUNT([2a][KEYWORD]) AS bar]] * \_Row[[1[INTEGER] AS a]] */ @@ -2565,7 +2565,7 @@ public void testRenameStatsDropGroup() { /** * Expected - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[a{r}#2, bar{r}#8],[COUNT([2a][KEYWORD]) AS baz, b{r}#4 AS bar]] * \_Row[[1[INTEGER] AS a, 2[INTEGER] AS b]] */ @@ -2584,7 +2584,7 @@ public void testMultipleRenameStatsDropGroup() { /** * Expected - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[emp_no{f}#11, bar{r}#4],[MAX(salary{f}#16) AS baz, gender{f}#13 AS bar]] * \_EsRelation[test][_meta_field{f}#17, emp_no{f}#11, first_name{f}#12, ..] */ @@ -2626,7 +2626,7 @@ private void aggFieldName(Expression exp, Class /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[],[SUM(emp_no{f}#4) AS sum(emp_no)]] * \_EsRelation[test][_meta_field{f}#10, emp_no{f}#4, first_name{f}#5, ge..] */ @@ -2658,7 +2658,7 @@ public void testIsNotNullConstraintForStatsWithGrouping() { /** * Expected - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[salary{f}#1185],[SUM(salary{f}#1185) AS sum(salary), salary{f}#1185]] * \_EsRelation[test][_meta_field{f}#1186, emp_no{f}#1180, first_name{f}#..] */ @@ -2677,7 +2677,7 @@ public void testIsNotNullConstraintForStatsWithAndOnGrouping() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[x{r}#4],[SUM(salary{f}#13) AS sum(salary), salary{f}#13 AS x]] * \_EsRelation[test][_meta_field{f}#14, emp_no{f}#8, first_name{f}#9, ge..] */ @@ -2697,7 +2697,7 @@ public void testIsNotNullConstraintForStatsWithAndOnGroupingAlias() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[salary{f}#13],[SUM(emp_no{f}#8) AS sum(x), salary{f}#13]] * \_EsRelation[test][_meta_field{f}#14, emp_no{f}#8, first_name{f}#9, ge..] */ @@ -2719,7 +2719,7 @@ public void testIsNotNullConstraintSkippedForStatsWithAlias() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[],[SUM(emp_no{f}#8) AS a, MIN(salary{f}#13) AS b]] * \_EsRelation[test][_meta_field{f}#14, emp_no{f}#8, first_name{f}#9, ge..] */ @@ -2738,7 +2738,7 @@ public void testIsNotNullConstraintForStatsWithMultiAggWithoutGrouping() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[gender{f}#11],[SUM(emp_no{f}#9) AS a, MIN(salary{f}#14) AS b, gender{f}#11]] * \_EsRelation[test][_meta_field{f}#15, emp_no{f}#9, first_name{f}#10, g..] */ @@ -2757,7 +2757,7 @@ public void testIsNotNullConstraintForStatsWithMultiAggWithGrouping() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[emp_no{f}#9],[SUM(emp_no{f}#9) AS a, MIN(salary{f}#14) AS b, emp_no{f}#9]] * \_EsRelation[test][_meta_field{f}#15, emp_no{f}#9, first_name{f}#10, g..] */ @@ -2776,7 +2776,7 @@ public void testIsNotNullConstraintForStatsWithMultiAggWithAndOnGrouping() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[w{r}#14, g{r}#16],[COUNT(b{r}#24) AS c, w{r}#14, gender{f}#32 AS g]] * \_Eval[[emp_no{f}#30 / 10[INTEGER] AS x, x{r}#4 + salary{f}#35 AS y, y{r}#8 / 4[INTEGER] AS z, z{r}#11 * 2[INTEGER] + * 3[INTEGER] AS w, salary{f}#35 + 4[INTEGER] / 2[INTEGER] AS a, a{r}#21 + 3[INTEGER] AS b]] @@ -2804,7 +2804,7 @@ public void testIsNotNullConstraintForAliasedExpressions() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[],[SPATIALCENTROID(location{f}#9) AS centroid]] * \_EsRelation[airports][abbrev{f}#5, location{f}#9, name{f}#6, scalerank{f}..] */ @@ -2829,7 +2829,7 @@ public void testSpatialTypesAndStatsUseDocValues() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[emp_no%2{r}#6],[COUNT(salary{f}#12) AS c, emp_no%2{r}#6]] * \_Eval[[emp_no{f}#7 % 2[INTEGER] AS emp_no%2]] * \_EsRelation[test][_meta_field{f}#13, emp_no{f}#7, first_name{f}#8, ge..] @@ -2854,7 +2854,7 @@ public void testNestedExpressionsInGroups() { /** * Expects - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[emp_no{f}#6],[COUNT(__c_COUNT@1bd45f36{r}#16) AS c, emp_no{f}#6]] * \_Eval[[salary{f}#11 + 1[INTEGER] AS __c_COUNT@1bd45f36]] * \_EsRelation[test][_meta_field{f}#12, emp_no{f}#6, first_name{f}#7, ge..] @@ -2879,7 +2879,7 @@ public void testNestedExpressionsInAggs() { } /** - * Limit[500[INTEGER]] + * Limit[1000[INTEGER]] * \_Aggregate[[emp_no%2{r}#7],[COUNT(__c_COUNT@fb7855b0{r}#18) AS c, emp_no%2{r}#7]] * \_Eval[[emp_no{f}#8 % 2[INTEGER] AS emp_no%2, 100[INTEGER] / languages{f}#11 + salary{f}#13 + 1[INTEGER] AS __c_COUNT * @fb7855b0]] @@ -2952,7 +2952,7 @@ public void testLogicalPlanOptimizerVerificationException() { * Project[[x{r}#5]] * \_Eval[[____x_AVG@9efc3cf3_SUM@daf9f221{r}#18 / ____x_AVG@9efc3cf3_COUNT@53cd08ed{r}#19 AS __x_AVG@9efc3cf3, __x_AVG@ * 9efc3cf3{r}#16 / 2[INTEGER] + __x_MAX@475d0e4d{r}#17 AS x]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_Aggregate[[],[SUM(salary{f}#11) AS ____x_AVG@9efc3cf3_SUM@daf9f221, COUNT(salary{f}#11) AS ____x_AVG@9efc3cf3_COUNT@53cd0 * 8ed, MAX(salary{f}#11) AS __x_MAX@475d0e4d]] * \_EsRelation[test][_meta_field{f}#12, emp_no{f}#6, first_name{f}#7, ge..] @@ -2990,7 +2990,7 @@ public void testStatsExpOverAggs() { * \_Eval[[$$SUM$$$AVG$avg(salary_%_3)>$0$0{r}#29 / $$COUNT$$$AVG$avg(salary_%_3)>$0$1{r}#30 AS $$AVG$avg(salary_%_3)>$0, * $$AVG$avg(salary_%_3)>$0{r}#23 + $$MAX$avg(salary_%_3)>$1{r}#24 AS x, * $$MIN$min(emp_no_/_3)>$2{r}#25 + 10[INTEGER] - $$MEDIAN$min(emp_no_/_3)>$3{r}#26 AS y]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_Aggregate[[z{r}#12],[SUM($$salary_%_3$AVG$0{r}#27) AS $$SUM$$$AVG$avg(salary_%_3)>$0$0, * COUNT($$salary_%_3$AVG$0{r}#27) AS $$COUNT$$$AVG$avg(salary_%_3)>$0$1, * MAX(emp_no{f}#13) AS $$MAX$avg(salary_%_3)>$1, @@ -3047,7 +3047,7 @@ public void testStatsExpOverAggsMulti() { * CONCAT(TOSTRING($$AVG$CONCAT(TO_STRIN>$0{r}#23),TOSTRING($$MAX$CONCAT(TO_STRIN>$1{r}#24)) AS x, * $$MIN$(MIN(emp_no_/_3>$2{r}#25 + 3.141592653589793[DOUBLE] - $$MEDIAN$(MIN(emp_no_/_3>$3{r}#26 / 2.718281828459045[DOUBLE] * AS y]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_Aggregate[[z{r}#12],[SUM($$salary_%_3$AVG$0{r}#27) AS $$SUM$$$AVG$CONCAT(TO_STRIN>$0$0, * COUNT($$salary_%_3$AVG$0{r}#27) AS $$COUNT$$$AVG$CONCAT(TO_STRIN>$0$1, * MAX(emp_no{f}#13) AS $$MAX$CONCAT(TO_STRIN>$1, @@ -3109,7 +3109,7 @@ public void testStatsExpOverAggsWithScalars() { * \_Eval[[$$$$avg(salary)_+_m>$AVG$0$SUM$0{r}#48 / $$max(salary)_+_3>$COUNT$2{r}#46 AS $$avg(salary)_+_m>$AVG$0, $$avg( * salary)_+_m>$AVG$0{r}#44 + $$avg(salary)_+_m>$MAX$1{r}#45 AS a, $$avg(salary)_+_m>$MAX$1{r}#45 + 3[INTEGER] + * 3.141592653589793[DOUBLE] + $$max(salary)_+_3>$COUNT$2{r}#46 AS b]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_Aggregate[[w{r}#28],[SUM(salary{f}#39) AS $$$$avg(salary)_+_m>$AVG$0$SUM$0, MAX(salary{f}#39) AS $$avg(salary)_+_m>$MAX$1 * , COUNT(salary{f}#39) AS $$max(salary)_+_3>$COUNT$2, MIN(salary{f}#39) AS $$count(salary)_->$MIN$3]] * \_Eval[[languages{f}#37 % 2[INTEGER] AS w]] @@ -3176,7 +3176,7 @@ public void testStatsExpOverAggsWithScalarAndDuplicateAggs() { /** * Expects * Project[[a{r}#5, a{r}#5 AS b, w{r}#12]] - * \_Limit[500[INTEGER]] + * \_Limit[1000[INTEGER]] * \_Aggregate[[w{r}#12],[SUM($$salary_/_2_+_la>$SUM$0{r}#26) AS a, w{r}#12]] * \_Eval[[emp_no{f}#16 % 2[INTEGER] AS w, salary{f}#21 / 2[INTEGER] + languages{f}#19 AS $$salary_/_2_+_la>$SUM$0]] * \_EsRelation[test][_meta_field{f}#22, emp_no{f}#16, first_name{f}#17, ..] 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 4030e7e0bcbef..0c87db5e5c6db 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 @@ -349,7 +349,7 @@ public void testExactlyOneExtractorPerFieldWithPruning() { /** * Expects - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SUM(salary{f}#882) AS x],FINAL,null] * \_ExchangeExec[[sum{r}#887, seen{r}#888],true] * \_FragmentExec[filter=null, estimatedRowSize=0, fragment=[ @@ -615,7 +615,7 @@ public void testExtractGroupingFieldsIfAggdWithEval() { /** * Expects * EvalExec[[agg_emp{r}#4 + 7[INTEGER] AS x]] - * \_LimitExec[500[INTEGER]] + * \_LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SUM(emp_no{f}#8) AS agg_emp],FINAL,16] * \_ExchangeExec[[sum{r}#18, seen{r}#19],true] * \_AggregateExec[[],[SUM(emp_no{f}#8) AS agg_emp],PARTIAL,8] @@ -647,7 +647,7 @@ public void testQueryWithAggregation() { /** * Expects * EvalExec[[agg_emp{r}#4 + 7[INTEGER] AS x]] - * \_LimitExec[500[INTEGER]] + * \_LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SUM(emp_no{f}#8) AS agg_emp],FINAL,16] * \_ExchangeExec[[sum{r}#18, seen{r}#19],true] * \_AggregateExec[[],[SUM(emp_no{f}#8) AS agg_emp],PARTIAL,8] @@ -1562,14 +1562,14 @@ public void testPushDownRLike() { } /** - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_ExchangeExec[[],false] * \_ProjectExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, job{f}#10, job.raw{f}#11, languages{f}#6, last_n * ame{f}#7, long_noidx{f}#12, salary{f}#8]] * \_FieldExtractExec[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gen..] * \_EsQueryExec[test], query[{"esql_single_value":{"field":"first_name","next": * {"term":{"first_name":{"value":"foo","case_insensitive":true}}},"source":"first_name =~ \"foo\"@2:9"}}] - * [_doc{f}#23], limit[500], sort[] estimatedRowSize[324] + * [_doc{f}#23], limit[1000], sort[] estimatedRowSize[324] */ @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/103599") public void testPushDownEqualsIgnoreCase() { @@ -1591,12 +1591,12 @@ public void testPushDownEqualsIgnoreCase() { } /** - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_ExchangeExec[[],false] * \_ProjectExec[[_meta_field{f}#12, emp_no{f}#6, first_name{f}#7, gender{f}#8, job{f}#13, job.raw{f}#14, languages{f}#9, last_ * name{f}#10, long_noidx{f}#15, salary{f}#11, x{r}#4]] * \_FieldExtractExec[_meta_field{f}#12, emp_no{f}#6, gender{f}#8, job{f}..] - * \_LimitExec[500[INTEGER]] + * \_LimitExec[1000[INTEGER]] * \_FilterExec[x{r}#4 =~ [66 6f 6f][KEYWORD]] * \_EvalExec[[CONCAT(first_name{f}#7,[66 6f 6f][KEYWORD]) AS x]] * \_FieldExtractExec[first_name{f}#7] @@ -2081,7 +2081,7 @@ public void testAvgSurrogateFunctionAfterRenameAndLimit() { /** * Expects - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[languages{f}#9],[MIN(salary{f}#11) AS m, languages{f}#9],FINAL,8] * \_ExchangeExec[[languages{f}#9, min{r}#16, seen{r}#17],true] * \_LocalSourceExec[[languages{f}#9, min{r}#16, seen{r}#17],EMPTY] @@ -2113,7 +2113,7 @@ public boolean exists(String field) { /** * Expects * intermediate plan - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[COUNT(emp_no{f}#6) AS c],FINAL,null] * \_ExchangeExec[[count{r}#16, seen{r}#17],true] * \_FragmentExec[filter=null, estimatedRowSize=0, fragment=[ @@ -2122,7 +2122,7 @@ public boolean exists(String field) { * \_EsRelation[test][_meta_field{f}#12, emp_no{f}#6, first_name{f}#7, ge..]]] * * and final plan is - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[COUNT(emp_no{f}#6) AS c],FINAL,8] * \_ExchangeExec[[count{r}#16, seen{r}#17],true] * \_LocalSourceExec[[count{r}#16, seen{r}#17],[LongVectorBlock[vector=ConstantLongVector[positions=1, value=0]]]] @@ -2184,7 +2184,7 @@ public void testGlobalAggFoldingOutput() { * Expects * ProjectExec[[a{r}#5]] * \_EvalExec[[__a_SUM@734e2841{r}#16 / __a_COUNT@12536eab{r}#17 AS a]] - * \_LimitExec[500[INTEGER]] + * \_LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SUM(emp_no{f}#6) AS __a_SUM@734e2841, COUNT(emp_no{f}#6) AS __a_COUNT@12536eab],FINAL,24] * \_ExchangeExec[[sum{r}#18, seen{r}#19, count{r}#20, seen{r}#21],true] * \_LocalSourceExec[[sum{r}#18, seen{r}#19, count{r}#20, seen{r}#21],[LongArrayBlock[positions=1, mvOrdering=UNORDERED, @@ -2217,7 +2217,7 @@ public void testPartialAggFoldingOutputForSyntheticAgg() { /** * Before local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(location{f}#9) AS centroid],FINAL,null] * \_ExchangeExec[[xVal{r}#10, xDel{r}#11, yVal{r}#12, yDel{r}#13, count{r}#14],true] * \_FragmentExec[filter=null, estimatedRowSize=0, fragment=[ @@ -2226,7 +2226,7 @@ public void testPartialAggFoldingOutputForSyntheticAgg() { * * After local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(location{f}#9) AS centroid],FINAL,50] * \_ExchangeExec[[xVal{r}#10, xDel{r}#11, yVal{r}#12, yDel{r}#13, count{r}#14],true] * \_AggregateExec[[],[SPATIALCENTROID(location{f}#9) AS centroid],PARTIAL,50] @@ -2274,7 +2274,7 @@ public void testSpatialTypesAndStatsUseDocValues() { /** * Before local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(__centroid_SPATIALCENTROID@b54a93a7{r}#10) AS centroid],FINAL,null] * \_ExchangeExec[[xVal{r}#11, xDel{r}#12, yVal{r}#13, yDel{r}#14, count{r}#15],true] * \_FragmentExec[filter=null, estimatedRowSize=0, fragment=[ @@ -2284,7 +2284,7 @@ public void testSpatialTypesAndStatsUseDocValues() { * * After local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(__centroid_SPATIALCENTROID@ad2847b6{r}#10) AS centroid],FINAL,50] * \_ExchangeExec[[xVal{r}#11, xDel{r}#12, yVal{r}#13, yDel{r}#14, count{r}#15],true] * \_AggregateExec[[],[SPATIALCENTROID(__centroid_SPATIALCENTROID@ad2847b6{r}#10) AS centroid],PARTIAL,50] @@ -2337,7 +2337,7 @@ public void testSpatialTypesAndStatsUseDocValuesNested() { /** * Before local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(__centroid_SPATIALCENTROID@ec8dd77e{r}#7) AS centroid],FINAL,null] * \_AggregateExec[[],[SPATIALCENTROID(__centroid_SPATIALCENTROID@ec8dd77e{r}#7) AS centroid],PARTIAL,null] * \_EvalExec[[[1 1 0 0 0 0 0 30 e2 4c 7c 45 40 0 0 e0 92 b0 82 2d 40][GEO_POINT] AS __centroid_SPATIALCENTROID@ec8dd77e]] @@ -2346,7 +2346,7 @@ public void testSpatialTypesAndStatsUseDocValuesNested() { * * After local optimizations we expect no changes because field is extracted: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(__centroid_SPATIALCENTROID@7ff910a{r}#7) AS centroid],FINAL,50] * \_AggregateExec[[],[SPATIALCENTROID(__centroid_SPATIALCENTROID@7ff910a{r}#7) AS centroid],PARTIAL,50] * \_EvalExec[[[1 1 0 0 0 0 0 30 e2 4c 7c 45 40 0 0 e0 92 b0 82 2d 40][GEO_POINT] AS __centroid_SPATIALCENTROID@7ff910a]] @@ -2389,7 +2389,7 @@ public void testSpatialTypesAndStatsUseDocValuesNestedLiteral() { /** * Before local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(location{f}#11) AS centroid, COUNT([2a][KEYWORD]) AS count],FINAL,null] * \_ExchangeExec[[xVal{r}#12, xDel{r}#13, yVal{r}#14, yDel{r}#15, count{r}#16, count{r}#17, seen{r}#18],true] * \_FragmentExec[filter=null, estimatedRowSize=0, fragment=[ @@ -2398,7 +2398,7 @@ public void testSpatialTypesAndStatsUseDocValuesNestedLiteral() { * * After local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(location{f}#11) AS centroid, COUNT([2a][KEYWORD]) AS count],FINAL,58] * \_ExchangeExec[[xVal{r}#12, xDel{r}#13, yVal{r}#14, yDel{r}#15, count{r}#16, count{r}#17, seen{r}#18],true] * \_AggregateExec[[],[COUNT([2a][KEYWORD]) AS count, SPATIALCENTROID(location{f}#11) AS centroid],PARTIAL,58] @@ -2449,7 +2449,7 @@ public void testSpatialTypesAndStatsUseDocValuesMultiAggregations() { /** * Before local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(location{f}#14) AS airports, SPATIALCENTROID(city_location{f}#17) AS cities, COUNT([2a][KEY * WORD]) AS count],FINAL,null] * \_ExchangeExec[[xVal{r}#18, xDel{r}#19, yVal{r}#20, yDel{r}#21, count{r}#22, xVal{r}#23, xDel{r}#24, yVal{r}#25, yDel{r}#26, @@ -2461,7 +2461,7 @@ public void testSpatialTypesAndStatsUseDocValuesMultiAggregations() { * * After local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(location{f}#14) AS airports, SPATIALCENTROID(city_location{f}#17) AS cities, COUNT([2a][KEY * WORD]) AS count],FINAL,108] * \_ExchangeExec[[xVal{r}#18, xDel{r}#19, yVal{r}#20, yDel{r}#21, count{r}#22, xVal{r}#23, xDel{r}#24, yVal{r}#25, yDel{r}#26, @@ -2518,7 +2518,7 @@ public void testSpatialTypesAndStatsUseDocValuesMultiSpatialAggregations() { /** * Before local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(location{f}#12) AS centroid, COUNT([2a][KEYWORD]) AS count],FINAL,null] * \_ExchangeExec[[xVal{r}#13, xDel{r}#14, yVal{r}#15, yDel{r}#16, count{r}#17, count{r}#18, seen{r}#19],true] * \_FragmentExec[filter=null, estimatedRowSize=0, fragment=[ @@ -2528,7 +2528,7 @@ public void testSpatialTypesAndStatsUseDocValuesMultiSpatialAggregations() { * * After local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(location{f}#11) AS centroid, COUNT([2a][KEYWORD]) AS count],FINAL,58] * \_ExchangeExec[[xVal{r}#12, xDel{r}#13, yVal{r}#14, yDel{r}#15, count{r}#16, count{r}#17, seen{r}#18],true] * \_AggregateExec[[],[COUNT([2a][KEYWORD]) AS count, SPATIALCENTROID(location{f}#11) AS centroid],PARTIAL,58] @@ -2585,7 +2585,7 @@ public void testSpatialTypesAndStatsUseDocValuesMultiAggregationsFiltered() { /** * Before local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[scalerank{f}#10],[SPATIALCENTROID(location{f}#12) AS centroid, COUNT([2a][KEYWORD]) AS count, scalerank{f}#10], * FINAL,null] * \_ExchangeExec[[scalerank{f}#10, xVal{r}#13, xDel{r}#14, yVal{r}#15, yDel{r}#16, count{r}#17, count{r}#18, seen{r}#19],true] @@ -2595,7 +2595,7 @@ public void testSpatialTypesAndStatsUseDocValuesMultiAggregationsFiltered() { * * After local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[scalerank{f}#10],[SPATIALCENTROID(location{f}#12) AS centroid, COUNT([2a][KEYWORD]) AS count, scalerank{f}#10], * FINAL,62] * \_ExchangeExec[[scalerank{f}#10, xVal{r}#13, xDel{r}#14, yVal{r}#15, yDel{r}#16, count{r}#17, count{r}#18, seen{r}#19],true] @@ -2654,7 +2654,7 @@ public void testSpatialTypesAndStatsUseDocValuesMultiAggregationsGrouped() { /** * Before local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(centroid{r}#4) AS centroid, SUM(count{r}#6) AS count],FINAL,null] * \_AggregateExec[[],[SPATIALCENTROID(centroid{r}#4) AS centroid, SUM(count{r}#6) AS count],PARTIAL,null] * \_AggregateExec[[scalerank{f}#16],[SPATIALCENTROID(location{f}#18) AS centroid, COUNT([2a][KEYWORD]) AS count],FINAL,null] @@ -2665,7 +2665,7 @@ public void testSpatialTypesAndStatsUseDocValuesMultiAggregationsGrouped() { * * After local optimizations: * - * LimitExec[500[INTEGER]] + * LimitExec[1000[INTEGER]] * \_AggregateExec[[],[SPATIALCENTROID(centroid{r}#4) AS centroid, SUM(count{r}#6) AS count],FINAL,58] * \_AggregateExec[[],[SPATIALCENTROID(centroid{r}#4) AS centroid, SUM(count{r}#6) AS count],PARTIAL,58] * \_AggregateExec[[scalerank{f}#16],[SPATIALCENTROID(location{f}#18) AS centroid, COUNT([2a][KEYWORD]) AS count],FINAL,58] diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/100_bug_fix.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/100_bug_fix.yml index f44b45a8be1d2..44d7290cbc002 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/100_bug_fix.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/100_bug_fix.yml @@ -3,7 +3,7 @@ - skip: version: " - 8.11.99" reason: "fixes in 8.12 or later" - features: warnings + features: allowed_warnings_regex - do: bulk: index: test @@ -14,10 +14,11 @@ - { "index": { } } - { "emp_no": 20 } - do: - warnings: - - "Line 1:37: evaluation of [to_ip(coalesce(ip1.keyword, \"255.255.255.255\"))] failed, treating result as null. Only first 20 failures recorded." + allowed_warnings_regex: + - "Line 1:37: evaluation of \\[to_ip\\(coalesce\\(ip1.keyword, \\\\\"255.255.255.255\\\\\"\\)\\)\\] failed, treating result as null. Only first 20 failures recorded." - "Line 1:37: java.lang.IllegalArgumentException: '127.0' is not an IP string literal." - - "No limit defined, adding default limit of [500]" + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: body: query: 'FROM test | sort emp_no | eval ip = to_ip(coalesce(ip1.keyword, "255.255.255.255")) | keep emp_no, ip' @@ -33,10 +34,10 @@ - do: - warnings: - - "Line 1:98: evaluation of [to_ip(x2)] failed, treating result as null. Only first 20 failures recorded." + allowed_warnings_regex: + - "Line 1:98: evaluation of \\[to_ip\\(x2\\)\\] failed, treating result as null. Only first 20 failures recorded." - "Line 1:98: java.lang.IllegalArgumentException: '127.00.1' is not an IP string literal." - - "No limit defined, adding default limit of [500]" + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'FROM test | sort emp_no | eval x1 = concat(ip1, ip2), x2 = coalesce(x1, "255.255.255.255"), x3 = to_ip(x2) | keep emp_no, x*' diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/20_aggs.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/20_aggs.yml index 4019b3a303345..820df8a3ff066 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/20_aggs.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/20_aggs.yml @@ -3,7 +3,7 @@ setup: - skip: version: " - 8.10.99" reason: "ESQL is available in 8.11+" - features: warnings + features: allowed_warnings_regex - do: indices.create: index: test @@ -115,8 +115,8 @@ setup: --- "Test From": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -140,8 +140,8 @@ setup: --- "Test simple grouping avg": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | where color == "red" | stats avg(data) by color' @@ -156,8 +156,8 @@ setup: --- "Test From Stats Avg": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats avg(count)' @@ -170,8 +170,8 @@ setup: --- "Test From Stats Avg With Alias": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats f1 = avg(count)' @@ -184,8 +184,8 @@ setup: --- "Test From Stats Count": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats count(data)' @@ -198,8 +198,8 @@ setup: --- "Test From Stats Count With Alias": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats dataCount = count(data)' @@ -212,8 +212,8 @@ setup: --- "Test From Stats Min": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats min(count)' @@ -226,8 +226,8 @@ setup: --- "Test From Stats Min With Alias": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats minCount=min(count)' @@ -240,8 +240,8 @@ setup: --- "Test From Stats Max": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats max(count)' @@ -254,8 +254,8 @@ setup: --- "Test From Stats Max With Alias": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats maxCount=max(count)' @@ -283,8 +283,8 @@ setup: --- "Test Median On Long": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats med=median(count)' @@ -297,8 +297,8 @@ setup: --- "Test Median On Double": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats med=median(count_d)' @@ -311,8 +311,8 @@ setup: --- "Test Grouping Median On Long": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats med=median(count) by color | sort med' @@ -328,8 +328,8 @@ setup: --- "Test Grouping Median On Double": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats med=median(count_d) by color | sort med' @@ -345,8 +345,8 @@ setup: --- "Test Median Absolute Deviation On Long": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats med=median_absolute_deviation(count)' @@ -359,8 +359,8 @@ setup: --- "Test Median Absolute Deviation On Double": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats med=median_absolute_deviation(count_d)' @@ -373,8 +373,8 @@ setup: --- "Test Grouping Median Absolute Deviation On Long": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats med=median_absolute_deviation(count) by color | sort color' @@ -390,8 +390,8 @@ setup: --- "Test Grouping Median Absolute Deviation On Double": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats med=median_absolute_deviation(count_d) by color | sort color' @@ -407,8 +407,8 @@ setup: --- "Test From Stats Eval": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats avg_count = avg(count) | eval x = avg_count + 7' @@ -420,8 +420,8 @@ setup: --- "Test Stats Where": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats x = avg(count) | where x > 100' @@ -444,8 +444,8 @@ setup: --- "Test Eval Row With Null": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'row a = 1, b = 2, c = null | eval z = c + b + a' @@ -469,8 +469,8 @@ setup: --- "Test Eval With Null And Count": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | eval nullsum = count_d + null | stats count(nullsum)' @@ -485,8 +485,8 @@ setup: --- "Test Eval With Multiple Expressions": - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'row l=1, d=1.0, ln=1 + null, dn=1.0 + null | stats sum(l), sum(d), sum(ln), sum(dn)' @@ -510,8 +510,8 @@ setup: --- grouping on text: - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'FROM test | STATS med=median(count) BY text | SORT med' diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/30_types.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/30_types.yml index 41e6d6b2cca77..8f1d64e169fde 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/30_types.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/30_types.yml @@ -3,7 +3,7 @@ setup: - skip: version: " - 8.11.99" reason: "more field loading added in 8.12+" - features: warnings + features: allowed_warnings_regex --- constant_keyword: @@ -30,8 +30,8 @@ constant_keyword: - { "color": "red" } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -44,8 +44,8 @@ constant_keyword: - match: { values.0.1: wow such constant } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | eval l=length(kind) | keep l' @@ -108,8 +108,8 @@ multivalued keyword: - { "card": ["jack", "of", "diamonds"] } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -139,8 +139,8 @@ keyword no doc_values: - { "card": ["jack", "of", "diamonds"] } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -169,8 +169,8 @@ wildcard: - { "card": "jack of diamonds" } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -180,8 +180,8 @@ wildcard: - match: {values.0.0: jack of diamonds} - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | eval l=length(card) | keep l' @@ -220,8 +220,8 @@ numbers: - { i: 123, l: -1234567891011121131, d: 1.234567891234568, mv_i: [123456, -123456], mv_l: [1234567891011121131, -1234567891011121131], mv_d: [1.234567891234568, -1.234567891234568] } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -271,8 +271,8 @@ small_numbers: - { b: 1, s: 1245, hf: 12.01, f: 112.0 } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -291,8 +291,8 @@ small_numbers: - match: {values.0.3: 1245} - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | eval sum_d = b + f + hf + s, sum_i = b + s | keep sum_d, sum_i' @@ -305,8 +305,8 @@ small_numbers: - match: {values.0.1: 1246} - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | eval r_f = round(f), r_hf = round(hf) | keep r_f, r_hf' @@ -341,8 +341,8 @@ scaled_float: - { f: 112.01, d: 1.0 } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -355,8 +355,8 @@ scaled_float: - match: {values.0.1: 112.01} - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | eval sum = d + f | keep sum' @@ -385,8 +385,8 @@ multivalued boolean: - { "booleans": [ true, false, false, false ] } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -417,8 +417,8 @@ ip: - { "ip": "127.0.0.1", "keyword": "127.0.0.2" } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -431,8 +431,8 @@ ip: - match: { values.0.1: "127.0.0.2" } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | where keyword == "127.0.0.2" | rename ip as IP | drop keyword' @@ -487,8 +487,8 @@ alias: - { "foo": "def", "level1": {"level2": 50}, "some_long": 15, "some_date": "2015-01-01T12:00:00.000Z" } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | keep foo, bar, level1.level2, level2_alias, some_long, some_long_alias, some_long_alias2, some_date, some_date_alias | sort level2_alias' @@ -531,8 +531,8 @@ alias: - match: { values.1.8: 2015-01-01T12:00:00.000Z } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | where bar == "abc" | keep foo, bar, level1.level2, level2_alias' @@ -551,8 +551,8 @@ alias: - match: { values.0.3: 10 } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | where level2_alias == 10 | keep foo, bar, level1.level2, level2_alias' @@ -571,16 +571,16 @@ alias: - match: { values.0.3: 10 } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | where level2_alias == 20' - length: { values: 0 } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test | stats x = max(level2_alias)' @@ -609,8 +609,8 @@ version: - { "version": [ "1.2.3", "4.5.6-SNOOPY" ] } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -642,8 +642,8 @@ id: - { "kw": "keyword1" } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test metadata _id | keep _id, kw' @@ -673,8 +673,8 @@ unsigned_long: - { "number": [ "1", "9223372036854775808", "0", "18446744073709551615" ] } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -892,8 +892,8 @@ geo_point: - { "location": "POINT(1 -1)" } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' @@ -925,8 +925,8 @@ cartesian_point: - { "location": "POINT(4321 -1234)" } - do: - warnings: - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" esql.query: body: query: 'from test' diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/90_non_indexed.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/90_non_indexed.yml index 80f15b9cb7414..c10554cebf300 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/90_non_indexed.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/90_non_indexed.yml @@ -2,7 +2,7 @@ setup: - skip: version: " - 8.11.99" reason: "extracting non-indexed fields available in 8.12+" - features: allowed_warnings + features: allowed_warnings_regex - do: indices.create: index: test @@ -95,9 +95,10 @@ setup: --- fetch: - do: - allowed_warnings: - - "Field [ip_noidx] cannot be retrieved, it is unsupported or not indexed; returning null" - - "No limit defined, adding default limit of [500]" + allowed_warnings_regex: + - "Field \\[ip_noidx\\] cannot be retrieved, it is unsupported or not indexed; returning null" + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: body: query: 'from test' From 16bdbe4be15fc474348636b8ee977f4147f11b46 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 21 Feb 2024 09:44:00 +0100 Subject: [PATCH 102/250] Remove needless allocations of ReducedRequestInfo from TransportBulkAction (#105646) These things accounted for a couple of GB in needless allocations during bulk indexing in the TSDB benchmark. We could represent the logic here cleaner by chaning the algorithm to not require collecting a map etc. but for a 10 min fix this is fine and saves non-trivial GC. --- .../action/bulk/TransportBulkAction.java | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java b/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java index 32566b559410d..3e661c2efe72f 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/TransportBulkAction.java @@ -353,8 +353,11 @@ protected void doInternalExecute(Task task, BulkRequest bulkRequest, String exec .collect( Collectors.toMap( DocWriteRequest::index, - request -> new ReducedRequestInfo(request.isRequireAlias(), request.isRequireDataStream()), - ReducedRequestInfo::merge + request -> ReducedRequestInfo.of(request.isRequireAlias(), request.isRequireDataStream()), + (existing, updated) -> ReducedRequestInfo.of( + existing.isRequireAlias || updated.isRequireAlias, + existing.isRequireDataStream || updated.isRequireDataStream + ) ) ); @@ -601,13 +604,29 @@ protected long buildTookInMillis(long startTimeNanos) { return TimeUnit.NANOSECONDS.toMillis(relativeTime() - startTimeNanos); } - private record ReducedRequestInfo(boolean isRequireAlias, boolean isRequireDataStream) { - private ReducedRequestInfo merge(ReducedRequestInfo other) { - return new ReducedRequestInfo( - this.isRequireAlias || other.isRequireAlias, - this.isRequireDataStream || other.isRequireDataStream - ); + private enum ReducedRequestInfo { + + REQUIRE_ALIAS_AND_DATA_STREAM(true, true), + REQUIRE_ALIAS_NOT_DATA_STREAM(true, false), + + REQUIRE_DATA_STREAM_NOT_ALIAS(false, true), + REQUIRE_NOTHING(false, false); + + private final boolean isRequireAlias; + private final boolean isRequireDataStream; + + ReducedRequestInfo(boolean isRequireAlias, boolean isRequireDataStream) { + this.isRequireAlias = isRequireAlias; + this.isRequireDataStream = isRequireDataStream; } + + static ReducedRequestInfo of(boolean isRequireAlias, boolean isRequireDataStream) { + if (isRequireAlias) { + return isRequireDataStream ? REQUIRE_ALIAS_AND_DATA_STREAM : REQUIRE_ALIAS_NOT_DATA_STREAM; + } + return isRequireDataStream ? REQUIRE_DATA_STREAM_NOT_ALIAS : REQUIRE_NOTHING; + } + } void executeBulk( From 954c428cde5f3dcab7b1dc99c24ee0ea00a724fa Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Wed, 21 Feb 2024 09:47:48 +0100 Subject: [PATCH 103/250] Fix EsAbortPolicy to not force execution if executor is already shutting down (#105666) Submitting a task during shutdown is highly unreliable and in almost all cases the task will be rejected (removed) anyways. Not forcing execution if the executor is already shutting down leads to more deterministic behavior and fixes EsExecutorsTests.testFixedBoundedRejectOnShutdown. --- .../org/elasticsearch/common/util/concurrent/EsAbortPolicy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/common/util/concurrent/EsAbortPolicy.java b/server/src/main/java/org/elasticsearch/common/util/concurrent/EsAbortPolicy.java index 52bd736f2bcf4..5dbacbb16aeea 100644 --- a/server/src/main/java/org/elasticsearch/common/util/concurrent/EsAbortPolicy.java +++ b/server/src/main/java/org/elasticsearch/common/util/concurrent/EsAbortPolicy.java @@ -14,7 +14,7 @@ public class EsAbortPolicy extends EsRejectedExecutionHandler { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { - if (r instanceof AbstractRunnable abstractRunnable) { + if (executor.isShutdown() == false && r instanceof AbstractRunnable abstractRunnable) { if (abstractRunnable.isForceExecution()) { if (executor.getQueue() instanceof SizeBlockingQueue sizeBlockingQueue) { try { From 0dca71eff96a4def23a3cc349e4399c6317565c7 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 21 Feb 2024 09:49:37 +0100 Subject: [PATCH 104/250] Avoid allocations in security's Automaton cache (#105654) We can just cast here instead of capturing the key in the lambda. --- .../xpack/core/security/support/Automatons.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java index a364b9cdbb227..5d7a4b279298c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java @@ -80,6 +80,7 @@ public static Automaton patterns(String... patterns) { /** * Builds and returns an automaton that will represent the union of all the given patterns. */ + @SuppressWarnings("unchecked") public static Automaton patterns(Collection patterns) { if (patterns.isEmpty()) { return EMPTY; @@ -88,7 +89,7 @@ public static Automaton patterns(Collection patterns) { return buildAutomaton(patterns); } else { try { - return cache.computeIfAbsent(Sets.newHashSet(patterns), ignore -> buildAutomaton(patterns)); + return cache.computeIfAbsent(Sets.newHashSet(patterns), p -> buildAutomaton((Set) p)); } catch (ExecutionException e) { throw unwrapCacheException(e); } @@ -184,7 +185,7 @@ static Automaton pattern(String pattern) { return buildAutomaton(pattern); } else { try { - return cache.computeIfAbsent(pattern, ignore -> buildAutomaton(pattern)); + return cache.computeIfAbsent(pattern, p -> buildAutomaton((String) p)); } catch (ExecutionException e) { throw unwrapCacheException(e); } From d1ec0d2544e8cfeb9b57ccb208d6080465ee818c Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Wed, 21 Feb 2024 11:02:36 +0100 Subject: [PATCH 105/250] Add protected method to allow overriding the computation of the size of a cache file region (#105570) This change introduces a protected method in SharedBlobCacheService that allows to initialize all cache file regions with the full region size. It causes the underlying SparseFileTracker to always track a full region, and therefore it makes it possible to write more bytes to a region that has its initial cache file length changed. --- .../blobcache/BlobCacheUtils.java | 12 +++++ .../shared/SharedBlobCacheService.java | 11 ++++- .../shared/SharedBlobCacheServiceTests.java | 45 ++++++++++++++++++- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/BlobCacheUtils.java b/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/BlobCacheUtils.java index c4dff2cb4457b..be2971bfa319a 100644 --- a/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/BlobCacheUtils.java +++ b/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/BlobCacheUtils.java @@ -9,6 +9,7 @@ import org.apache.lucene.store.IndexInput; import org.elasticsearch.blobcache.common.ByteRange; +import org.elasticsearch.blobcache.shared.SharedBytes; import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.core.Streams; @@ -31,6 +32,17 @@ public static int toIntBytes(long l) { return ByteSizeUnit.BYTES.toIntBytes(l); } + /** + * Rounds the length up so that it is aligned on the next page size (defined by SharedBytes.PAGE_SIZE). For example + */ + public static long toPageAlignedSize(long length) { + int remainder = (int) length % SharedBytes.PAGE_SIZE; + if (remainder > 0L) { + return length + (SharedBytes.PAGE_SIZE - remainder); + } + return length; + } + public static void throwEOF(long channelPos, long len) throws EOFException { throw new EOFException(format("unexpected EOF reading [%d-%d]", channelPos, channelPos + len)); } diff --git a/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/shared/SharedBlobCacheService.java b/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/shared/SharedBlobCacheService.java index 2c5997e479209..f2ebe61906258 100644 --- a/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/shared/SharedBlobCacheService.java +++ b/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/shared/SharedBlobCacheService.java @@ -435,7 +435,14 @@ private ByteRange mapSubRangeToRegion(ByteRange range, int region) { ); } - private int getRegionSize(long fileLength, int region) { + /** + * Compute the size of a cache file region. + * + * @param fileLength the length of the file/blob to cache + * @param region the region number + * @return a size in bytes of the cache file region + */ + protected int computeCacheFileRegionSize(long fileLength, int region) { assert fileLength > 0; final int maxRegion = getEndingRegion(fileLength); assert region >= 0 && region <= maxRegion : region + " - " + maxRegion; @@ -1209,7 +1216,7 @@ public LFUCacheEntry get(KeyType cacheKey, long fileLength, int region) { // if we did not find an entry var entry = keyMapping.get(regionKey); if (entry == null) { - final int effectiveRegionSize = getRegionSize(fileLength, region); + final int effectiveRegionSize = computeCacheFileRegionSize(fileLength, region); entry = keyMapping.computeIfAbsent(regionKey, key -> new LFUCacheEntry(new CacheFileRegion(key, effectiveRegionSize), now)); } // io is volatile, double locking is fine, as long as we assign it last. diff --git a/x-pack/plugin/blob-cache/src/test/java/org/elasticsearch/blobcache/shared/SharedBlobCacheServiceTests.java b/x-pack/plugin/blob-cache/src/test/java/org/elasticsearch/blobcache/shared/SharedBlobCacheServiceTests.java index 049197edd97df..5cdd44ad86332 100644 --- a/x-pack/plugin/blob-cache/src/test/java/org/elasticsearch/blobcache/shared/SharedBlobCacheServiceTests.java +++ b/x-pack/plugin/blob-cache/src/test/java/org/elasticsearch/blobcache/shared/SharedBlobCacheServiceTests.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.support.GroupedActionListener; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.blobcache.BlobCacheMetrics; +import org.elasticsearch.blobcache.BlobCacheUtils; import org.elasticsearch.blobcache.common.ByteRange; import org.elasticsearch.cluster.node.DiscoveryNodeRole; import org.elasticsearch.common.settings.Setting; @@ -1052,7 +1053,6 @@ public void testPopulate() throws Exception { .put("path.home", createTempDir()) .build(); - final AtomicLong relativeTimeInMillis = new AtomicLong(0L); final DeterministicTaskQueue taskQueue = new DeterministicTaskQueue(); try ( NodeEnvironment environment = new NodeEnvironment(settings, TestEnvironment.newEnvironment(settings)); @@ -1136,4 +1136,47 @@ public void testNonPositiveRecoveryRangeSizeRejected() { assertThatNonPositiveRecoveryRangeSizeRejected(SharedBlobCacheService.SHARED_CACHE_RECOVERY_RANGE_SIZE_SETTING); } + public void testUseFullRegionSize() throws IOException { + final long regionSize = size(randomIntBetween(1, 100)); + final long cacheSize = regionSize * randomIntBetween(1, 10); + + Settings settings = Settings.builder() + .put(NODE_NAME_SETTING.getKey(), "node") + .put(SharedBlobCacheService.SHARED_CACHE_REGION_SIZE_SETTING.getKey(), ByteSizeValue.ofBytes(regionSize).getStringRep()) + .put(SharedBlobCacheService.SHARED_CACHE_SIZE_SETTING.getKey(), ByteSizeValue.ofBytes(cacheSize).getStringRep()) + .put("path.home", createTempDir()) + .build(); + final DeterministicTaskQueue taskQueue = new DeterministicTaskQueue(); + try ( + NodeEnvironment environment = new NodeEnvironment(settings, TestEnvironment.newEnvironment(settings)); + var cacheService = new SharedBlobCacheService<>( + environment, + settings, + taskQueue.getThreadPool(), + ThreadPool.Names.GENERIC, + ThreadPool.Names.GENERIC, + BlobCacheMetrics.NOOP + ) { + @Override + protected int computeCacheFileRegionSize(long fileLength, int region) { + // use full region + return super.getRegionSize(); + } + } + ) { + final var cacheKey = generateCacheKey(); + final var blobLength = randomLongBetween(1L, cacheSize); + + int regions = Math.toIntExact(blobLength / regionSize); + regions += (blobLength % regionSize == 0L ? 0L : 1L); + assertThat( + cacheService.computeCacheFileRegionSize(blobLength, randomFrom(regions)), + equalTo(BlobCacheUtils.toIntBytes(regionSize)) + ); + for (int region = 0; region < regions; region++) { + var cacheFileRegion = cacheService.get(cacheKey, blobLength, region); + assertThat(cacheFileRegion.tracker.getLength(), equalTo(regionSize)); + } + } + } } From 76cf92718660a886b6e25b9eec02e19f1af6c019 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Wed, 21 Feb 2024 11:29:24 +0100 Subject: [PATCH 106/250] Fix Setting.exists if key doesn't equal key in settings keys. (#105652) Fix `exists` for affix settings and list settings if using the index syntax. In these cases the equality check fails. Additionally, this fixes inconsistencies in different implementations of `exists` and `existsOrFallbackExists` to make sure secure setting keys are consistently excluded unless using `SecureSetting.exists`. --- .../common/settings/SecureSetting.java | 9 ++- .../common/settings/Setting.java | 68 +++++++++++++------ .../common/settings/SettingTests.java | 52 +++++++++++++- 3 files changed, 106 insertions(+), 23 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/settings/SecureSetting.java b/server/src/main/java/org/elasticsearch/common/settings/SecureSetting.java index b69f05ea62fcb..6fe2c71c15e00 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/SecureSetting.java +++ b/server/src/main/java/org/elasticsearch/common/settings/SecureSetting.java @@ -13,6 +13,7 @@ import java.io.InputStream; import java.security.GeneralSecurityException; +import java.util.Collections; import java.util.EnumSet; import java.util.Set; @@ -67,7 +68,13 @@ String innerGetRaw(final Settings settings) { @Override public boolean exists(Settings settings) { final SecureSettings secureSettings = settings.getSecureSettings(); - return secureSettings != null && secureSettings.getSettingNames().contains(getKey()); + return secureSettings != null && getRawKey().exists(secureSettings.getSettingNames(), Collections.emptySet()); + } + + @Override + public boolean exists(Settings.Builder builder) { + final SecureSettings secureSettings = builder.getSecureSettings(); + return secureSettings != null && getRawKey().exists(secureSettings.getSettingNames(), Collections.emptySet()); } @Override diff --git a/server/src/main/java/org/elasticsearch/common/settings/Setting.java b/server/src/main/java/org/elasticsearch/common/settings/Setting.java index f6dd5532a3aea..aa1c25a3f1952 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/Setting.java +++ b/server/src/main/java/org/elasticsearch/common/settings/Setting.java @@ -15,7 +15,6 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.VersionId; import org.elasticsearch.common.logging.DeprecationCategory; -import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.MemorySizeValue; import org.elasticsearch.common.xcontent.XContentParserUtils; @@ -49,6 +48,7 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.IntFunction; +import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -503,16 +503,13 @@ public T getDefault(Settings settings) { * @return true if the setting is present in the given settings instance, otherwise false */ public boolean exists(final Settings settings) { - return exists(settings.keySet(), settings.getSecureSettings()); + SecureSettings secureSettings = settings.getSecureSettings(); + return key.exists(settings.keySet(), secureSettings == null ? Collections.emptySet() : secureSettings.getSettingNames()); } public boolean exists(final Settings.Builder builder) { - return exists(builder.keys(), builder.getSecureSettings()); - } - - private boolean exists(final Set keys, final SecureSettings secureSettings) { - final String key = getKey(); - return keys.contains(key) && (secureSettings == null || secureSettings.getSettingNames().contains(key) == false); + SecureSettings secureSettings = builder.getSecureSettings(); + return key.exists(builder.keys(), secureSettings == null ? Collections.emptySet() : secureSettings.getSettingNames()); } /** @@ -522,7 +519,7 @@ private boolean exists(final Set keys, final SecureSettings secureSettin * @return true if the setting including fallback settings is present in the given settings instance, otherwise false */ public boolean existsOrFallbackExists(final Settings settings) { - return settings.keySet().contains(getKey()) || (fallbackSetting != null && fallbackSetting.existsOrFallbackExists(settings)); + return exists(settings) || (fallbackSetting != null && fallbackSetting.existsOrFallbackExists(settings)); } /** @@ -1164,21 +1161,12 @@ public String innerGetRaw(final Settings settings) { @Override public Settings get(Settings settings) { + // TODO should we be checking for deprecations here? Settings byPrefix = settings.getByPrefix(getKey()); validator.accept(byPrefix); return byPrefix; } - @Override - public boolean exists(Settings settings) { - for (String settingsKey : settings.keySet()) { - if (settingsKey.startsWith(key)) { - return true; - } - } - return false; - } - @Override public void diff(Settings.Builder builder, Settings source, Settings defaultSettings) { Set leftGroup = get(source).keySet(); @@ -2108,6 +2096,13 @@ private static AffixSetting affixKeySetting( public interface Key { boolean match(String key); + + /** + * Returns true if and only if this key is present in the given settings instance (ignoring given exclusions). + * @param keys keys to check + * @param exclusions exclusions to ignore + */ + boolean exists(Set keys, Set exclusions); } public static class SimpleKey implements Key { @@ -2139,9 +2134,15 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(key); } + + @Override + public boolean exists(Set keys, Set exclusions) { + return keys.contains(key) && exclusions.contains(key) == false; + } } public static final class GroupKey extends SimpleKey { + public GroupKey(String key) { super(key); if (key.endsWith(".") == false) { @@ -2151,7 +2152,15 @@ public GroupKey(String key) { @Override public boolean match(String toTest) { - return Regex.simpleMatch(key + "*", toTest); + return toTest != null && toTest.startsWith(key); + } + + @Override + public boolean exists(Set keys, Set exclusions) { + if (exclusions.isEmpty()) { + return keys.stream().anyMatch(this::match); + } + return keys.stream().filter(Predicate.not(exclusions::contains)).anyMatch(this::match); } } @@ -2167,6 +2176,17 @@ public ListKey(String key) { public boolean match(String toTest) { return pattern.matcher(toTest).matches(); } + + @Override + public boolean exists(Set keys, Set exclusions) { + if (keys.contains(key)) { + return exclusions.contains(key) == false; + } + if (exclusions.isEmpty()) { + return keys.stream().anyMatch(this::match); + } + return keys.stream().filter(Predicate.not(exclusions::contains)).anyMatch(this::match); + } } /** @@ -2224,6 +2244,14 @@ public boolean match(String key) { return pattern.matcher(key).matches(); } + @Override + public boolean exists(Set keys, Set exclusions) { + if (exclusions.isEmpty()) { + return keys.stream().anyMatch(this::match); + } + return keys.stream().filter(Predicate.not(exclusions::contains)).anyMatch(this::match); + } + /** * Does this key have a fallback prefix? */ diff --git a/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java b/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java index 1b3d741a6ea44..13f789a8b5fae 100644 --- a/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java +++ b/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java @@ -552,6 +552,13 @@ public void testGroups() { } } + public void testGroupKeyExists() { + Setting setting = Setting.groupSetting("foo.deprecated.", Property.NodeScope); + + assertFalse(setting.exists(Settings.EMPTY)); + assertTrue(setting.exists(Settings.builder().put("foo.deprecated.1.value", "1").build())); + } + public void testFilteredGroups() { AtomicReference ref = new AtomicReference<>(null); Setting setting = Setting.groupSetting("foo.bar.", Property.Filtered, Property.Dynamic); @@ -659,6 +666,22 @@ public void testCompositeValidator() { } + public void testListKeyExists() { + final Setting> listSetting = Setting.listSetting( + "foo", + Collections.singletonList("bar"), + Function.identity(), + Property.NodeScope + ); + Settings settings = Settings.builder().put("foo", "bar1,bar2").build(); + assertFalse(listSetting.exists(Settings.EMPTY)); + assertTrue(listSetting.exists(settings)); + + settings = Settings.builder().put("foo.0", "foo1").put("foo.1", "foo2").build(); + assertFalse(listSetting.exists(Settings.EMPTY)); + assertTrue(listSetting.exists(settings)); + } + public void testListSettingsDeprecated() { final Setting> deprecatedListSetting = Setting.listSetting( "foo.deprecated", @@ -673,9 +696,19 @@ public void testListSettingsDeprecated() { Function.identity(), Property.NodeScope ); - final Settings settings = Settings.builder() + Settings settings = Settings.builder() .put("foo.deprecated", "foo.deprecated1,foo.deprecated2") - .put("foo.deprecated", "foo.non_deprecated1,foo.non_deprecated2") + .put("foo.non_deprecated", "foo.non_deprecated1,foo.non_deprecated2") + .build(); + deprecatedListSetting.get(settings); + nonDeprecatedListSetting.get(settings); + assertSettingDeprecationsAndWarnings(new Setting[] { deprecatedListSetting }); + + settings = Settings.builder() + .put("foo.deprecated.0", "foo.deprecated1") + .put("foo.deprecated.1", "foo.deprecated2") + .put("foo.non_deprecated.0", "foo.non_deprecated1") + .put("foo.non_deprecated.1", "foo.non_deprecated2") .build(); deprecatedListSetting.get(settings); nonDeprecatedListSetting.get(settings); @@ -881,6 +914,21 @@ public void testAffixKeySetting() { assertFalse(listAffixSetting.match("foo")); } + public void testAffixKeyExists() { + Setting setting = Setting.affixKeySetting("foo.", "enable", (key) -> Setting.boolSetting(key, false, Property.NodeScope)); + + assertFalse(setting.exists(Settings.EMPTY)); + assertTrue(setting.exists(Settings.builder().put("foo.test.enable", "true").build())); + } + + public void testAffixKeyExistsWithSecure() { + Setting setting = Setting.affixKeySetting("foo.", "enable", (key) -> Setting.boolSetting(key, false, Property.NodeScope)); + + final MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("foo.test.enabled", "true"); + assertFalse(setting.exists(Settings.builder().setSecureSettings(secureSettings).build())); + } + public void testAffixSettingNamespaces() { Setting.AffixSetting setting = Setting.affixKeySetting( "foo.", From 550b5bb76f98ca35fed5a8cb76870f9a4be4d499 Mon Sep 17 00:00:00 2001 From: Tim Grein Date: Wed, 21 Feb 2024 11:29:44 +0100 Subject: [PATCH 107/250] [Connectors API] Unify enum error messages and add more tests (#105569) --- .../connector/ConnectorStatus.java | 2 +- .../connector/ConnectorSyncStatus.java | 4 +-- .../ConfigurationDisplayType.java | 2 +- .../configuration/ConfigurationFieldType.java | 2 +- .../ConfigurationValidationType.java | 2 +- .../connector/filtering/FilteringPolicy.java | 2 +- .../filtering/FilteringRuleCondition.java | 2 +- .../filtering/FilteringValidationState.java | 2 +- .../ConnectorSyncJobTriggerMethod.java | 4 ++- .../syncjob/ConnectorSyncJobType.java | 2 +- .../connector/ConnectorStatusTests.java | 26 ++++++++++++++++ .../connector/ConnectorSyncStatusTests.java | 26 ++++++++++++++++ .../connector/ConnectorTestUtils.java | 16 +++++----- .../ConfigurationDisplayTypeTests.java | 26 ++++++++++++++++ .../ConfigurationFieldTypeTests.java | 27 +++++++++++++++++ .../ConfigurationValidationTypeTests.java | 27 +++++++++++++++++ .../filtering/FilteringPolicyTests.java | 27 +++++++++++++++++ .../FilteringRuleConditionTests.java | 30 +++++++++++++++++++ .../FilteringValidationStateTests.java | 30 +++++++++++++++++++ 19 files changed, 240 insertions(+), 19 deletions(-) create mode 100644 x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorStatusTests.java create mode 100644 x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorSyncStatusTests.java create mode 100644 x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationDisplayTypeTests.java create mode 100644 x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationFieldTypeTests.java create mode 100644 x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationValidationTypeTests.java create mode 100644 x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/filtering/FilteringPolicyTests.java create mode 100644 x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/filtering/FilteringRuleConditionTests.java create mode 100644 x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/filtering/FilteringValidationStateTests.java diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorStatus.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorStatus.java index 5ebbab668890b..b64da63adcd50 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorStatus.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorStatus.java @@ -37,6 +37,6 @@ public static ConnectorStatus connectorStatus(String status) { return connectorStatus; } } - throw new IllegalArgumentException("Unknown ConnectorStatus: " + status); + throw new IllegalArgumentException("Unknown " + ConnectorStatus.class.getSimpleName() + " [" + status + "]."); } } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorSyncStatus.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorSyncStatus.java index 30fca79f78876..eedb585e3ad6c 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorSyncStatus.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorSyncStatus.java @@ -37,7 +37,7 @@ public static ConnectorSyncStatus fromString(String syncStatusString) { } } - throw new IllegalArgumentException("Unknown sync status '" + syncStatusString + "'."); + throw new IllegalArgumentException("Unknown " + ConnectorSyncStatus.class.getSimpleName() + " [" + syncStatusString + "]."); } @Override @@ -51,6 +51,6 @@ public static ConnectorSyncStatus connectorSyncStatus(String status) { return connectorSyncStatus; } } - throw new IllegalArgumentException("Unknown ConnectorSyncStatus: " + status); + throw new IllegalArgumentException("Unknown " + ConnectorSyncStatus.class.getSimpleName() + " [" + status + "]."); } } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationDisplayType.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationDisplayType.java index df8dee04d61b9..c6c87d18a4939 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationDisplayType.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationDisplayType.java @@ -28,6 +28,6 @@ public static ConfigurationDisplayType displayType(String type) { return displayType; } } - throw new IllegalArgumentException("Unknown DisplayType: " + type); + throw new IllegalArgumentException("Unknown " + ConfigurationDisplayType.class.getSimpleName() + " [" + type + "]."); } } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationFieldType.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationFieldType.java index 20162735985c6..2d59f23e7aec8 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationFieldType.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationFieldType.java @@ -30,6 +30,6 @@ public static ConfigurationFieldType fieldType(String type) { return fieldType; } } - throw new IllegalArgumentException("Unknown FieldType: " + type); + throw new IllegalArgumentException("Unknown " + ConfigurationFieldType.class.getSimpleName() + " [" + type + "]."); } } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationValidationType.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationValidationType.java index 7c064014a95ba..182be36a473f7 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationValidationType.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationValidationType.java @@ -27,6 +27,6 @@ public static ConfigurationValidationType validationType(String type) { return displayType; } } - throw new IllegalArgumentException("Unknown ValidationType: " + type); + throw new IllegalArgumentException("Unknown " + ConfigurationValidationType.class.getSimpleName() + " [" + type + "]."); } } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/filtering/FilteringPolicy.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/filtering/FilteringPolicy.java index 48170cfc8fae4..a59a7e3fd4831 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/filtering/FilteringPolicy.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/filtering/FilteringPolicy.java @@ -24,6 +24,6 @@ public static FilteringPolicy filteringPolicy(String policy) { return filteringPolicy; } } - throw new IllegalArgumentException("Unknown FilteringPolicy: " + policy); + throw new IllegalArgumentException("Unknown " + FilteringPolicy.class.getSimpleName() + " [" + policy + "]."); } } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/filtering/FilteringRuleCondition.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/filtering/FilteringRuleCondition.java index 967107961b0d4..2d640d58732dd 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/filtering/FilteringRuleCondition.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/filtering/FilteringRuleCondition.java @@ -33,6 +33,6 @@ public static FilteringRuleCondition filteringRuleCondition(String condition) { return filteringRuleCondition; } } - throw new IllegalArgumentException("Unknown FilteringRuleCondition: " + condition); + throw new IllegalArgumentException("Unknown " + FilteringRuleCondition.class.getSimpleName() + " [" + condition + "]."); } } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/filtering/FilteringValidationState.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/filtering/FilteringValidationState.java index e2d370e3b9ed8..d033a1189ae00 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/filtering/FilteringValidationState.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/filtering/FilteringValidationState.java @@ -25,6 +25,6 @@ public static FilteringValidationState filteringValidationState(String validatio return filteringValidationState; } } - throw new IllegalArgumentException("Unknown FilteringValidationState: " + validationState); + throw new IllegalArgumentException("Unknown " + FilteringValidationState.class.getSimpleName() + " [" + validationState + "]."); } } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTriggerMethod.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTriggerMethod.java index 110748795fb77..890c8db018cdf 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTriggerMethod.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobTriggerMethod.java @@ -20,7 +20,9 @@ public static ConnectorSyncJobTriggerMethod fromString(String triggerMethodStrin } } - throw new IllegalArgumentException("Unknown trigger method '" + triggerMethodString + "'."); + throw new IllegalArgumentException( + "Unknown " + ConnectorSyncJobTriggerMethod.class.getSimpleName() + " [" + triggerMethodString + "]." + ); } @Override diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobType.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobType.java index 2d0a18da6fec5..7a6dc22f409cd 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobType.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobType.java @@ -21,7 +21,7 @@ public static ConnectorSyncJobType fromString(String syncJobTypeString) { } } - throw new IllegalArgumentException("Unknown sync job type '" + syncJobTypeString + "'."); + throw new IllegalArgumentException("Unknown " + ConnectorSyncJobType.class.getSimpleName() + " [" + syncJobTypeString + "]."); } @Override diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorStatusTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorStatusTests.java new file mode 100644 index 0000000000000..a08a015158198 --- /dev/null +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorStatusTests.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.application.connector; + +import org.elasticsearch.test.ESTestCase; + +import static org.hamcrest.Matchers.equalTo; + +public class ConnectorStatusTests extends ESTestCase { + + public void testConnectorStatus_WithValidConnectorStatusString() { + ConnectorStatus connectorStatus = ConnectorTestUtils.getRandomConnectorStatus(); + + assertThat(ConnectorStatus.connectorStatus(connectorStatus.toString()), equalTo(connectorStatus)); + } + + public void testConnectorStatus_WithInvalidConnectorStatusString_ExpectIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> ConnectorStatus.connectorStatus("invalid connector status")); + } + +} diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorSyncStatusTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorSyncStatusTests.java new file mode 100644 index 0000000000000..ae341eb1f8d2f --- /dev/null +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorSyncStatusTests.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.application.connector; + +import org.elasticsearch.test.ESTestCase; + +import static org.hamcrest.Matchers.equalTo; + +public class ConnectorSyncStatusTests extends ESTestCase { + + public void testConnectorSyncStatus_WithValidConnectorSyncStatusString() { + ConnectorSyncStatus connectorSyncStatus = ConnectorTestUtils.getRandomSyncStatus(); + + assertThat(ConnectorSyncStatus.connectorSyncStatus(connectorSyncStatus.toString()), equalTo(connectorSyncStatus)); + } + + public void testConnectorSyncStatus_WithInvalidConnectorSyncStatusString_ExpectIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> ConnectorSyncStatus.connectorSyncStatus("invalid connector sync status")); + } + +} diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTestUtils.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTestUtils.java index 3e17c33834989..6d94cdc3ebe35 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTestUtils.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorTestUtils.java @@ -152,7 +152,7 @@ public static ConnectorFiltering getRandomConnectorFiltering() { .setId(randomAlphaOfLength(10)) .setOrder(randomInt()) .setPolicy(getRandomFilteringPolicy()) - .setRule(getRandomFilteringRule()) + .setRule(getRandomFilteringRuleCondition()) .setUpdatedAt(currentTimestamp) .setValue(randomAlphaOfLength(10)) .build() @@ -180,7 +180,7 @@ public static ConnectorFiltering getRandomConnectorFiltering() { .setId(randomAlphaOfLength(10)) .setOrder(randomInt()) .setPolicy(getRandomFilteringPolicy()) - .setRule(getRandomFilteringRule()) + .setRule(getRandomFilteringRuleCondition()) .setUpdatedAt(currentTimestamp) .setValue(randomAlphaOfLength(10)) .build() @@ -378,32 +378,32 @@ public static ConnectorStatus getRandomConnectorStatus() { return values[randomInt(values.length - 1)]; } - private static FilteringPolicy getRandomFilteringPolicy() { + public static FilteringPolicy getRandomFilteringPolicy() { FilteringPolicy[] values = FilteringPolicy.values(); return values[randomInt(values.length - 1)]; } - private static FilteringRuleCondition getRandomFilteringRule() { + public static FilteringRuleCondition getRandomFilteringRuleCondition() { FilteringRuleCondition[] values = FilteringRuleCondition.values(); return values[randomInt(values.length - 1)]; } - private static FilteringValidationState getRandomFilteringValidationState() { + public static FilteringValidationState getRandomFilteringValidationState() { FilteringValidationState[] values = FilteringValidationState.values(); return values[randomInt(values.length - 1)]; } - private static ConfigurationDisplayType getRandomConfigurationDisplayType() { + public static ConfigurationDisplayType getRandomConfigurationDisplayType() { ConfigurationDisplayType[] values = ConfigurationDisplayType.values(); return values[randomInt(values.length - 1)]; } - private static ConfigurationFieldType getRandomConfigurationFieldType() { + public static ConfigurationFieldType getRandomConfigurationFieldType() { ConfigurationFieldType[] values = ConfigurationFieldType.values(); return values[randomInt(values.length - 1)]; } - private static ConfigurationValidationType getRandomConfigurationValidationType() { + public static ConfigurationValidationType getRandomConfigurationValidationType() { ConfigurationValidationType[] values = ConfigurationValidationType.values(); return values[randomInt(values.length - 1)]; } diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationDisplayTypeTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationDisplayTypeTests.java new file mode 100644 index 0000000000000..e9fb15ba33082 --- /dev/null +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationDisplayTypeTests.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.application.connector.configuration; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.application.connector.ConnectorTestUtils; + +import static org.hamcrest.Matchers.equalTo; + +public class ConfigurationDisplayTypeTests extends ESTestCase { + + public void testDisplayType_WithValidConfigurationDisplayTypeString() { + ConfigurationDisplayType displayType = ConnectorTestUtils.getRandomConfigurationDisplayType(); + + assertThat(ConfigurationDisplayType.displayType(displayType.toString()), equalTo(displayType)); + } + + public void testDisplayType_WithInvalidConfigurationDisplayTypeString_ExpectIllegalArgumentException() { + expectThrows(IllegalArgumentException.class, () -> ConfigurationDisplayType.displayType("invalid configuration display type")); + } +} diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationFieldTypeTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationFieldTypeTests.java new file mode 100644 index 0000000000000..eeab9e77c586f --- /dev/null +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationFieldTypeTests.java @@ -0,0 +1,27 @@ +/* + * 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.application.connector.configuration; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.application.connector.ConnectorTestUtils; + +import static org.hamcrest.Matchers.equalTo; + +public class ConfigurationFieldTypeTests extends ESTestCase { + + public void testFieldType_WithValidConfigurationFieldTypeString() { + ConfigurationFieldType fieldType = ConnectorTestUtils.getRandomConfigurationFieldType(); + + assertThat(ConfigurationFieldType.fieldType(fieldType.toString()), equalTo(fieldType)); + } + + public void testFieldType_WithInvalidConfigurationFieldTypeString_ExpectIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> ConfigurationFieldType.fieldType("invalid field type")); + } + +} diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationValidationTypeTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationValidationTypeTests.java new file mode 100644 index 0000000000000..69b845d0c99d8 --- /dev/null +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/configuration/ConfigurationValidationTypeTests.java @@ -0,0 +1,27 @@ +/* + * 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.application.connector.configuration; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.application.connector.ConnectorTestUtils; + +import static org.hamcrest.Matchers.equalTo; + +public class ConfigurationValidationTypeTests extends ESTestCase { + + public void testValidationType_WithValidConfigurationValidationTypeString() { + ConfigurationValidationType validationType = ConnectorTestUtils.getRandomConfigurationValidationType(); + + assertThat(ConfigurationValidationType.validationType(validationType.toString()), equalTo(validationType)); + } + + public void testValidationType_WithInvalidConfigurationValidationTypeString_ExpectIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> ConfigurationValidationType.validationType("invalid validation type")); + } + +} diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/filtering/FilteringPolicyTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/filtering/FilteringPolicyTests.java new file mode 100644 index 0000000000000..4bf53661caf95 --- /dev/null +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/filtering/FilteringPolicyTests.java @@ -0,0 +1,27 @@ +/* + * 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.application.connector.filtering; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.application.connector.ConnectorTestUtils; + +import static org.hamcrest.Matchers.equalTo; + +public class FilteringPolicyTests extends ESTestCase { + + public void testFilteringPolicy_WithValidFilteringPolicyString() { + FilteringPolicy filteringPolicy = ConnectorTestUtils.getRandomFilteringPolicy(); + + assertThat(FilteringPolicy.filteringPolicy(filteringPolicy.toString()), equalTo(filteringPolicy)); + } + + public void testFilteringPolicy_WithInvalidFilteringPolicyString_ExpectIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> FilteringPolicy.filteringPolicy("invalid filtering policy")); + } + +} diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/filtering/FilteringRuleConditionTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/filtering/FilteringRuleConditionTests.java new file mode 100644 index 0000000000000..8d8ffaf4fe02c --- /dev/null +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/filtering/FilteringRuleConditionTests.java @@ -0,0 +1,30 @@ +/* + * 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.application.connector.filtering; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.application.connector.ConnectorTestUtils; + +import static org.hamcrest.Matchers.equalTo; + +public class FilteringRuleConditionTests extends ESTestCase { + + public void testFilteringRuleCondition_WithValidFilteringRuleConditionString() { + FilteringRuleCondition ruleCondition = ConnectorTestUtils.getRandomFilteringRuleCondition(); + + assertThat(FilteringRuleCondition.filteringRuleCondition(ruleCondition.toString()), equalTo(ruleCondition)); + } + + public void testFilteringRuleCondition_WithInvalidFilteringRuleConditionString_ExpectIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> FilteringRuleCondition.filteringRuleCondition("invalid filtering rule condition") + ); + } + +} diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/filtering/FilteringValidationStateTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/filtering/FilteringValidationStateTests.java new file mode 100644 index 0000000000000..67cd86b2b8aef --- /dev/null +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/filtering/FilteringValidationStateTests.java @@ -0,0 +1,30 @@ +/* + * 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.application.connector.filtering; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.application.connector.ConnectorTestUtils; + +import static org.hamcrest.Matchers.equalTo; + +public class FilteringValidationStateTests extends ESTestCase { + + public void testFilteringValidationState_WithValidFilteringValidationStateString() { + FilteringValidationState validationState = ConnectorTestUtils.getRandomFilteringValidationState(); + + assertThat(FilteringValidationState.filteringValidationState(validationState.toString()), equalTo(validationState)); + } + + public void testFilteringValidationState_WithInvalidFilteringValidationStateString_ExpectIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> FilteringValidationState.filteringValidationState("invalid filtering validation state") + ); + } + +} From 5d4bb6ef46523b5c0e914a2157ec30db23bb3b25 Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 21 Feb 2024 10:36:26 +0000 Subject: [PATCH 108/250] Slight readability improvement in EsAbortPolicy (#105680) Reviewing #105666 I was tripped up by how the check for force execution is split into two nested `if` statements, the first of which now has another condition. By grouping the conditions like this it makes it look like `AbstractRunnable` is somehow not special on a shut-down executor. It is still special, but that specialness is implemented elsewhere in `EsThreadPoolExecutor#execute`. This commit regroups the conditions and extracts a method to limit the scope of the `AbstractRunnable`. --- .../common/util/concurrent/EsAbortPolicy.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/util/concurrent/EsAbortPolicy.java b/server/src/main/java/org/elasticsearch/common/util/concurrent/EsAbortPolicy.java index 5dbacbb16aeea..0f77326967ebb 100644 --- a/server/src/main/java/org/elasticsearch/common/util/concurrent/EsAbortPolicy.java +++ b/server/src/main/java/org/elasticsearch/common/util/concurrent/EsAbortPolicy.java @@ -14,24 +14,26 @@ public class EsAbortPolicy extends EsRejectedExecutionHandler { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { - if (executor.isShutdown() == false && r instanceof AbstractRunnable abstractRunnable) { - if (abstractRunnable.isForceExecution()) { - if (executor.getQueue() instanceof SizeBlockingQueue sizeBlockingQueue) { - try { - sizeBlockingQueue.forcePut(r); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("forced execution, but got interrupted", e); - } - if ((executor.isShutdown() && sizeBlockingQueue.remove(r)) == false) { - return; - } // else fall through and reject the task since the executor is shut down - } else { - throw new IllegalStateException("expected but did not find SizeBlockingQueue: " + executor); + if (executor.isShutdown() == false && isForceExecution(r)) { + if (executor.getQueue() instanceof SizeBlockingQueue sizeBlockingQueue) { + try { + sizeBlockingQueue.forcePut(r); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("forced execution, but got interrupted", e); } + if ((executor.isShutdown() && sizeBlockingQueue.remove(r)) == false) { + return; + } // else fall through and reject the task since the executor is shut down + } else { + throw new IllegalStateException("expected but did not find SizeBlockingQueue: " + executor); } } incrementRejections(); throw newRejectedException(r, executor, executor.isShutdown()); } + + private static boolean isForceExecution(Runnable r) { + return r instanceof AbstractRunnable abstractRunnable && abstractRunnable.isForceExecution(); + } } From 48ceed06616eaaa0bd999ae845bdf63733a5ccec Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Wed, 21 Feb 2024 11:54:43 +0100 Subject: [PATCH 109/250] Additional roles and privileges APIs customization (#105503) This PR folds together the following: * Support fetching native-only roles, i.e., excluding reserved roles for the Get Roles API * Switch the Delete Roles API to public protection scope * Support injecting a response translator for the Get Builtin Privileges API Depends on: https://github.com/elastic/elasticsearch/pull/105336 Relates: ES-7826, ES-7828, ES-7845 --- .../GetBuiltinPrivilegesRequest.java | 7 -- .../GetBuiltinPrivilegesResponse.java | 28 +++--- ...etBuiltinPrivilegesResponseTranslator.java | 20 +++++ .../security/action/role/GetRolesRequest.java | 18 ++-- .../action/role/GetRolesRequestBuilder.java | 5 ++ .../action/role/GetRolesResponse.java | 18 +--- .../GetBuiltinPrivilegesResponseTests.java | 32 ------- .../xpack/security/Security.java | 46 +++++----- .../TransportGetBuiltinPrivilegesAction.java | 15 +--- .../action/role/TransportGetRolesAction.java | 54 +++++++---- .../RestGetBuiltinPrivilegesAction.java | 49 ++++++++-- .../action/role/RestDeleteRoleAction.java | 2 +- .../rest/action/role/RestGetRolesAction.java | 65 +++++++++----- .../role/TransportGetRolesActionTests.java | 89 +++++++++++++++++++ 14 files changed, 285 insertions(+), 163 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTranslator.java delete mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesRequest.java index 1fdf8ee35d1b6..bbcd2bbe255ce 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesRequest.java @@ -8,19 +8,12 @@ import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; -import org.elasticsearch.common.io.stream.StreamInput; - -import java.io.IOException; /** * Request to retrieve built-in (cluster/index) privileges. */ public final class GetBuiltinPrivilegesRequest extends ActionRequest { - public GetBuiltinPrivilegesRequest(StreamInput in) throws IOException { - super(in); - } - public GetBuiltinPrivilegesRequest() {} @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponse.java index d4d99d0b25b7d..328089a73b2f5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponse.java @@ -7,8 +7,8 @@ package org.elasticsearch.xpack.core.security.action.privilege; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.support.TransportAction; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import java.io.IOException; @@ -17,32 +17,25 @@ import java.util.Objects; /** - * Response containing one or more application privileges retrieved from the security index + * Response containing built-in (cluster/index) privileges */ public final class GetBuiltinPrivilegesResponse extends ActionResponse { - private String[] clusterPrivileges; - private String[] indexPrivileges; - - public GetBuiltinPrivilegesResponse(String[] clusterPrivileges, String[] indexPrivileges) { - this.clusterPrivileges = Objects.requireNonNull(clusterPrivileges, "Cluster privileges cannot be null"); - this.indexPrivileges = Objects.requireNonNull(indexPrivileges, "Index privileges cannot be null"); - } + private final String[] clusterPrivileges; + private final String[] indexPrivileges; public GetBuiltinPrivilegesResponse(Collection clusterPrivileges, Collection indexPrivileges) { - this(clusterPrivileges.toArray(Strings.EMPTY_ARRAY), indexPrivileges.toArray(Strings.EMPTY_ARRAY)); + this.clusterPrivileges = Objects.requireNonNull( + clusterPrivileges.toArray(Strings.EMPTY_ARRAY), + "Cluster privileges cannot be null" + ); + this.indexPrivileges = Objects.requireNonNull(indexPrivileges.toArray(Strings.EMPTY_ARRAY), "Index privileges cannot be null"); } public GetBuiltinPrivilegesResponse() { this(Collections.emptySet(), Collections.emptySet()); } - public GetBuiltinPrivilegesResponse(StreamInput in) throws IOException { - super(in); - this.clusterPrivileges = in.readStringArray(); - this.indexPrivileges = in.readStringArray(); - } - public String[] getClusterPrivileges() { return clusterPrivileges; } @@ -53,7 +46,6 @@ public String[] getIndexPrivileges() { @Override public void writeTo(StreamOutput out) throws IOException { - out.writeStringArray(clusterPrivileges); - out.writeStringArray(indexPrivileges); + TransportAction.localOnly(); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTranslator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTranslator.java new file mode 100644 index 0000000000000..2d018ae2f1b2f --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTranslator.java @@ -0,0 +1,20 @@ +/* + * 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.core.security.action.privilege; + +public interface GetBuiltinPrivilegesResponseTranslator { + + GetBuiltinPrivilegesResponse translate(GetBuiltinPrivilegesResponse response, boolean restrictResponse); + + class Default implements GetBuiltinPrivilegesResponseTranslator { + public GetBuiltinPrivilegesResponse translate(GetBuiltinPrivilegesResponse response, boolean restrictResponse) { + assert false == restrictResponse; + return response; + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesRequest.java index f5239f18c256a..310bd6c707796 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesRequest.java @@ -9,12 +9,12 @@ import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import java.io.IOException; import static org.elasticsearch.action.ValidateActions.addValidationError; +import static org.elasticsearch.action.support.TransportAction.localOnly; /** * Request to retrieve roles from the security index @@ -23,10 +23,7 @@ public class GetRolesRequest extends ActionRequest { private String[] names = Strings.EMPTY_ARRAY; - public GetRolesRequest(StreamInput in) throws IOException { - super(in); - names = in.readStringArray(); - } + private boolean nativeOnly = false; public GetRolesRequest() {} @@ -47,9 +44,16 @@ public String[] names() { return names; } + public void nativeOnly(boolean nativeOnly) { + this.nativeOnly = nativeOnly; + } + + public boolean nativeOnly() { + return this.nativeOnly; + } + @Override public void writeTo(StreamOutput out) throws IOException { - super.writeTo(out); - out.writeStringArray(names); + localOnly(); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesRequestBuilder.java index 693a497d05087..bd3b5784e5ba0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesRequestBuilder.java @@ -22,4 +22,9 @@ public GetRolesRequestBuilder names(String... names) { request.names(names); return this; } + + public GetRolesRequestBuilder nativeOnly(boolean nativeOnly) { + request.nativeOnly(nativeOnly); + return this; + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java index 86e74952c5956..e00c85749ca76 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java @@ -7,7 +7,7 @@ package org.elasticsearch.xpack.core.security.action.role; import org.elasticsearch.action.ActionResponse; -import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.action.support.TransportAction; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; @@ -18,16 +18,7 @@ */ public class GetRolesResponse extends ActionResponse { - private RoleDescriptor[] roles; - - public GetRolesResponse(StreamInput in) throws IOException { - super(in); - int size = in.readVInt(); - roles = new RoleDescriptor[size]; - for (int i = 0; i < size; i++) { - roles[i] = new RoleDescriptor(in); - } - } + private final RoleDescriptor[] roles; public GetRolesResponse(RoleDescriptor... roles) { this.roles = roles; @@ -43,9 +34,6 @@ public boolean hasRoles() { @Override public void writeTo(StreamOutput out) throws IOException { - out.writeVInt(roles.length); - for (RoleDescriptor role : roles) { - role.writeTo(out); - } + TransportAction.localOnly(); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTests.java deleted file mode 100644 index c8d14a4d71db1..0000000000000 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/privilege/GetBuiltinPrivilegesResponseTests.java +++ /dev/null @@ -1,32 +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.core.security.action.privilege; - -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.test.ESTestCase; -import org.hamcrest.Matchers; - -import java.io.IOException; - -public class GetBuiltinPrivilegesResponseTests extends ESTestCase { - - public void testSerialization() throws IOException { - final String[] cluster = generateRandomStringArray(8, randomIntBetween(3, 8), false, true); - final String[] index = generateRandomStringArray(8, randomIntBetween(3, 8), false, true); - final GetBuiltinPrivilegesResponse original = new GetBuiltinPrivilegesResponse(cluster, index); - - final BytesStreamOutput out = new BytesStreamOutput(); - original.writeTo(out); - - final GetBuiltinPrivilegesResponse copy = new GetBuiltinPrivilegesResponse(out.bytes().streamInput()); - - assertThat(copy.getClusterPrivileges(), Matchers.equalTo(cluster)); - assertThat(copy.getIndexPrivileges(), Matchers.equalTo(index)); - } - -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 763eb2616175c..3beff69849a58 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -135,6 +135,7 @@ import org.elasticsearch.xpack.core.security.action.privilege.ClearPrivilegesCacheAction; import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction; import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesResponseTranslator; import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction; import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction; import org.elasticsearch.xpack.core.security.action.profile.ActivateProfileAction; @@ -560,6 +561,7 @@ public class Security extends Plugin private final SetOnce scriptServiceReference = new SetOnce<>(); private final SetOnce operatorOnlyRegistry = new SetOnce<>(); private final SetOnce putRoleRequestBuilderFactory = new SetOnce<>(); + private final SetOnce getBuiltinPrivilegesResponseTranslator = new SetOnce<>(); private final SetOnce fileRolesStore = new SetOnce<>(); private final SetOnce operatorPrivilegesService = new SetOnce<>(); private final SetOnce reservedRoleMappingAction = new SetOnce<>(); @@ -820,6 +822,10 @@ Collection createComponents( putRoleRequestBuilderFactory.set(new PutRoleRequestBuilderFactory.Default()); } + if (getBuiltinPrivilegesResponseTranslator.get() == null) { + getBuiltinPrivilegesResponseTranslator.set(new GetBuiltinPrivilegesResponseTranslator.Default()); + } + final Map, ActionListener>>> customRoleProviders = new LinkedHashMap<>(); for (SecurityExtension extension : securityExtensions) { final List, ActionListener>> providers = extension.getRolesProviders( @@ -1446,7 +1452,7 @@ public List getRestHandlers( new RestOpenIdConnectPrepareAuthenticationAction(settings, getLicenseState()), new RestOpenIdConnectAuthenticateAction(settings, getLicenseState()), new RestOpenIdConnectLogoutAction(settings, getLicenseState()), - new RestGetBuiltinPrivilegesAction(settings, getLicenseState()), + new RestGetBuiltinPrivilegesAction(settings, getLicenseState(), getBuiltinPrivilegesResponseTranslator.get()), new RestGetPrivilegesAction(settings, getLicenseState()), new RestPutPrivilegesAction(settings, getLicenseState()), new RestDeletePrivilegesAction(settings, getLicenseState()), @@ -2030,33 +2036,21 @@ public void accept(DiscoveryNode node, ClusterState state) { @Override public void loadExtensions(ExtensionLoader loader) { securityExtensions.addAll(loader.loadExtensions(SecurityExtension.class)); + loadSingletonExtensionAndSetOnce(loader, operatorOnlyRegistry, OperatorOnlyRegistry.class); + loadSingletonExtensionAndSetOnce(loader, putRoleRequestBuilderFactory, PutRoleRequestBuilderFactory.class); + loadSingletonExtensionAndSetOnce(loader, getBuiltinPrivilegesResponseTranslator, GetBuiltinPrivilegesResponseTranslator.class); + } - // operator registry SPI - List operatorOnlyRegistries = loader.loadExtensions(OperatorOnlyRegistry.class); - if (operatorOnlyRegistries.size() > 1) { - throw new IllegalStateException(OperatorOnlyRegistry.class + " may not have multiple implementations"); - } else if (operatorOnlyRegistries.size() == 1) { - OperatorOnlyRegistry operatorOnlyRegistry = operatorOnlyRegistries.get(0); - this.operatorOnlyRegistry.set(operatorOnlyRegistry); - logger.debug( - "Loaded implementation [{}] for interface OperatorOnlyRegistry", - operatorOnlyRegistry.getClass().getCanonicalName() - ); - } - - List builderFactories = loader.loadExtensions(PutRoleRequestBuilderFactory.class); - if (builderFactories.size() > 1) { - throw new IllegalStateException(PutRoleRequestBuilderFactory.class + " may not have multiple implementations"); - } else if (builderFactories.size() == 1) { - PutRoleRequestBuilderFactory builderFactory = builderFactories.get(0); - this.putRoleRequestBuilderFactory.set(builderFactory); - logger.debug( - "Loaded implementation [{}] for interface [{}]", - builderFactory.getClass().getCanonicalName(), - PutRoleRequestBuilderFactory.class - ); + private void loadSingletonExtensionAndSetOnce(ExtensionLoader loader, SetOnce setOnce, Class clazz) { + final List loaded = loader.loadExtensions(clazz); + if (loaded.size() > 1) { + throw new IllegalStateException(clazz + " may not have multiple implementations"); + } else if (loaded.size() == 1) { + final T singleLoaded = loaded.get(0); + setOnce.set(singleLoaded); + logger.debug("Loaded implementation [{}] for interface [{}]", singleLoaded.getClass().getCanonicalName(), clazz); } else { - logger.debug("Will fall back on default implementation for interface [{}]", PutRoleRequestBuilderFactory.class); + logger.debug("Will fall back on default implementation for interface [{}]", clazz); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetBuiltinPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetBuiltinPrivilegesAction.java index 6494c5b7c9230..8ea8ec3e0dcd9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetBuiltinPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/privilege/TransportGetBuiltinPrivilegesAction.java @@ -8,9 +8,8 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.action.support.TransportAction; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesAction; @@ -22,19 +21,13 @@ import java.util.TreeSet; /** - * Transport action to retrieve one or more application privileges from the security index + * Transport action to retrieve built-in (cluster/index) privileges */ -public class TransportGetBuiltinPrivilegesAction extends HandledTransportAction { +public class TransportGetBuiltinPrivilegesAction extends TransportAction { @Inject public TransportGetBuiltinPrivilegesAction(ActionFilters actionFilters, TransportService transportService) { - super( - GetBuiltinPrivilegesAction.NAME, - transportService, - actionFilters, - GetBuiltinPrivilegesRequest::new, - EsExecutors.DIRECT_EXECUTOR_SERVICE - ); + super(GetBuiltinPrivilegesAction.NAME, actionFilters, transportService.getTaskManager()); } @Override diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesAction.java index 3d63364f85664..eadae3bfc0baf 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesAction.java @@ -8,9 +8,8 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; -import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.action.support.TransportAction; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.action.role.GetRolesAction; @@ -21,11 +20,14 @@ import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; -public class TransportGetRolesAction extends HandledTransportAction { +public class TransportGetRolesAction extends TransportAction { private final NativeRolesStore nativeRolesStore; private final ReservedRolesStore reservedRolesStore; @@ -37,7 +39,7 @@ public TransportGetRolesAction( TransportService transportService, ReservedRolesStore reservedRolesStore ) { - super(GetRolesAction.NAME, transportService, actionFilters, GetRolesRequest::new, EsExecutors.DIRECT_EXECUTOR_SERVICE); + super(GetRolesAction.NAME, actionFilters, transportService.getTaskManager()); this.nativeRolesStore = nativeRolesStore; this.reservedRolesStore = reservedRolesStore; } @@ -46,15 +48,23 @@ public TransportGetRolesAction( protected void doExecute(Task task, final GetRolesRequest request, final ActionListener listener) { final String[] requestedRoles = request.names(); final boolean specificRolesRequested = requestedRoles != null && requestedRoles.length > 0; - final Set rolesToSearchFor = new HashSet<>(); - final List roles = new ArrayList<>(); + if (request.nativeOnly()) { + final Set rolesToSearchFor = specificRolesRequested + ? Arrays.stream(requestedRoles).collect(Collectors.toSet()) + : Collections.emptySet(); + getNativeRoles(rolesToSearchFor, listener); + return; + } + + final Set rolesToSearchFor = new HashSet<>(); + final List reservedRoles = new ArrayList<>(); if (specificRolesRequested) { for (String role : requestedRoles) { if (ReservedRolesStore.isReserved(role)) { RoleDescriptor rd = ReservedRolesStore.roleDescriptor(role); if (rd != null) { - roles.add(rd); + reservedRoles.add(rd); } else { listener.onFailure(new IllegalStateException("unable to obtain reserved role [" + role + "]")); return; @@ -64,21 +74,29 @@ protected void doExecute(Task task, final GetRolesRequest request, final ActionL } } } else { - roles.addAll(ReservedRolesStore.roleDescriptors()); + reservedRoles.addAll(ReservedRolesStore.roleDescriptors()); } if (specificRolesRequested && rolesToSearchFor.isEmpty()) { - // specific roles were requested but they were built in only, no need to hit the store - listener.onResponse(new GetRolesResponse(roles.toArray(new RoleDescriptor[roles.size()]))); + // specific roles were requested, but they were built in only, no need to hit the store + listener.onResponse(new GetRolesResponse(reservedRoles.toArray(new RoleDescriptor[0]))); } else { - nativeRolesStore.getRoleDescriptors(rolesToSearchFor, ActionListener.wrap((retrievalResult) -> { - if (retrievalResult.isSuccess()) { - roles.addAll(retrievalResult.getDescriptors()); - listener.onResponse(new GetRolesResponse(roles.toArray(new RoleDescriptor[roles.size()]))); - } else { - listener.onFailure(retrievalResult.getFailure()); - } - }, listener::onFailure)); + getNativeRoles(rolesToSearchFor, reservedRoles, listener); } } + + private void getNativeRoles(Set rolesToSearchFor, ActionListener listener) { + getNativeRoles(rolesToSearchFor, new ArrayList<>(), listener); + } + + private void getNativeRoles(Set rolesToSearchFor, List foundRoles, ActionListener listener) { + nativeRolesStore.getRoleDescriptors(rolesToSearchFor, ActionListener.wrap((retrievalResult) -> { + if (retrievalResult.isSuccess()) { + foundRoles.addAll(retrievalResult.getDescriptors()); + listener.onResponse(new GetRolesResponse(foundRoles.toArray(new RoleDescriptor[0]))); + } else { + listener.onFailure(retrievalResult.getFailure()); + } + }, listener::onFailure)); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java index fe3b5cab38444..334e560312db1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/privilege/RestGetBuiltinPrivilegesAction.java @@ -6,7 +6,11 @@ */ package org.elasticsearch.xpack.security.rest.action.privilege; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestRequest; @@ -19,6 +23,8 @@ import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesAction; import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesRequest; import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesResponse; +import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesResponseTranslator; +import org.elasticsearch.xpack.security.authz.store.NativeRolesStore; import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import java.io.IOException; @@ -27,13 +33,21 @@ import static org.elasticsearch.rest.RestRequest.Method.GET; /** - * Rest action to retrieve an application privilege from the security index + * Rest action to retrieve built-in (cluster/index) privileges */ -@ServerlessScope(Scope.INTERNAL) +@ServerlessScope(Scope.PUBLIC) public class RestGetBuiltinPrivilegesAction extends SecurityBaseRestHandler { - public RestGetBuiltinPrivilegesAction(Settings settings, XPackLicenseState licenseState) { + private static final Logger logger = LogManager.getLogger(RestGetBuiltinPrivilegesAction.class); + private final GetBuiltinPrivilegesResponseTranslator responseTranslator; + + public RestGetBuiltinPrivilegesAction( + Settings settings, + XPackLicenseState licenseState, + GetBuiltinPrivilegesResponseTranslator responseTranslator + ) { super(settings, licenseState); + this.responseTranslator = responseTranslator; } @Override @@ -48,15 +62,17 @@ public String getName() { @Override public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + final boolean restrictResponse = request.hasParam(RestRequest.PATH_RESTRICTED); return channel -> client.execute( GetBuiltinPrivilegesAction.INSTANCE, new GetBuiltinPrivilegesRequest(), new RestBuilderListener<>(channel) { @Override public RestResponse buildResponse(GetBuiltinPrivilegesResponse response, XContentBuilder builder) throws Exception { + final var translatedResponse = responseTranslator.translate(response, restrictResponse); builder.startObject(); - builder.array("cluster", response.getClusterPrivileges()); - builder.array("index", response.getIndexPrivileges()); + builder.array("cluster", translatedResponse.getClusterPrivileges()); + builder.array("index", translatedResponse.getIndexPrivileges()); builder.endObject(); return new RestResponse(RestStatus.OK, builder); } @@ -64,4 +80,27 @@ public RestResponse buildResponse(GetBuiltinPrivilegesResponse response, XConten ); } + @Override + protected Exception innerCheckFeatureAvailable(RestRequest request) { + final boolean restrictPath = request.hasParam(RestRequest.PATH_RESTRICTED); + assert false == restrictPath || DiscoveryNode.isStateless(settings); + if (false == restrictPath) { + return super.innerCheckFeatureAvailable(request); + } + // This is a temporary hack: we are re-using the native roles setting as an overall feature flag for custom roles. + final Boolean nativeRolesEnabled = settings.getAsBoolean(NativeRolesStore.NATIVE_ROLES_ENABLED, true); + if (nativeRolesEnabled == false) { + logger.debug( + "Attempt to call [{} {}] but [{}] is [{}]", + request.method(), + request.rawPath(), + NativeRolesStore.NATIVE_ROLES_ENABLED, + settings.get(NativeRolesStore.NATIVE_ROLES_ENABLED) + ); + return new ElasticsearchStatusException("This API is not enabled on this Elasticsearch instance", RestStatus.GONE); + } else { + return null; + } + } + } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestDeleteRoleAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestDeleteRoleAction.java index 88f53c999dfb9..cf5e4d12e7b37 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestDeleteRoleAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestDeleteRoleAction.java @@ -28,7 +28,7 @@ /** * Rest endpoint to delete a Role from the security index */ -@ServerlessScope(Scope.INTERNAL) +@ServerlessScope(Scope.PUBLIC) public class RestDeleteRoleAction extends NativeRoleBaseRestHandler { public RestDeleteRoleAction(Settings settings, XPackLicenseState licenseState) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestGetRolesAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestGetRolesAction.java index 4b2660658a38f..232d74d16725d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestGetRolesAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/role/RestGetRolesAction.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.rest.action.role; import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.RestApiVersion; @@ -21,7 +22,6 @@ import org.elasticsearch.xpack.core.security.action.role.GetRolesRequestBuilder; import org.elasticsearch.xpack.core.security.action.role.GetRolesResponse; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import java.io.IOException; import java.util.List; @@ -30,12 +30,9 @@ /** * Rest endpoint to retrieve a Role from the security index - * - * Note: This class does not extend {@link NativeRoleBaseRestHandler} because it handles both reserved roles and native - * roles, and should still be available even if native role management is disabled. */ -@ServerlessScope(Scope.INTERNAL) -public class RestGetRolesAction extends SecurityBaseRestHandler { +@ServerlessScope(Scope.PUBLIC) +public class RestGetRolesAction extends NativeRoleBaseRestHandler { public RestGetRolesAction(Settings settings, XPackLicenseState licenseState) { super(settings, licenseState); @@ -57,25 +54,47 @@ public String getName() { @Override public RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { final String[] roles = request.paramAsStringArray("name", Strings.EMPTY_ARRAY); - return channel -> new GetRolesRequestBuilder(client).names(roles).execute(new RestBuilderListener<>(channel) { - @Override - public RestResponse buildResponse(GetRolesResponse response, XContentBuilder builder) throws Exception { - builder.startObject(); - for (RoleDescriptor role : response.roles()) { - builder.field(role.getName(), role); - } - builder.endObject(); + final boolean restrictRequest = isPathRestricted(request); + return channel -> new GetRolesRequestBuilder(client).names(roles) + .nativeOnly(restrictRequest) + .execute(new RestBuilderListener<>(channel) { + @Override + public RestResponse buildResponse(GetRolesResponse response, XContentBuilder builder) throws Exception { + builder.startObject(); + for (RoleDescriptor role : response.roles()) { + builder.field(role.getName(), role); + } + builder.endObject(); + + // if the user asked for specific roles, but none of them were found + // we'll return an empty result and 404 status code + if (roles.length != 0 && response.roles().length == 0) { + return new RestResponse(RestStatus.NOT_FOUND, builder); + } - // if the user asked for specific roles, but none of them were found - // we'll return an empty result and 404 status code - if (roles.length != 0 && response.roles().length == 0) { - return new RestResponse(RestStatus.NOT_FOUND, builder); + // either the user asked for all roles, or at least one of the roles + // the user asked for was found + return new RestResponse(RestStatus.OK, builder); } + }); + } + + @Override + protected Exception innerCheckFeatureAvailable(RestRequest request) { + // Note: For non-restricted requests this action handles both reserved roles and native + // roles, and should still be available even if native role management is disabled. + // For restricted requests it should only be available if native role management is enabled + final boolean restrictPath = isPathRestricted(request); + if (false == restrictPath) { + return null; + } else { + return super.innerCheckFeatureAvailable(request); + } + } - // either the user asked for all roles, or at least one of the roles - // the user asked for was found - return new RestResponse(RestStatus.OK, builder); - } - }); + private boolean isPathRestricted(RestRequest request) { + final boolean restrictRequest = request.hasParam(RestRequest.PATH_RESTRICTED); + assert false == restrictRequest || DiscoveryNode.isStateless(settings); + return restrictRequest; } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java index 2b7125a411d61..0348ff6df90b2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/role/TransportGetRolesActionTests.java @@ -276,6 +276,95 @@ public void onFailure(Exception e) { } } + public void testGetWithNativeOnly() { + final boolean all = randomBoolean(); + final List storeRoleDescriptors = randomRoleDescriptors(); + final List storeNames = storeRoleDescriptors.stream().map(RoleDescriptor::getName).collect(Collectors.toList()); + + final List requestedNames = new ArrayList<>(); + final List requestedStoreNames = new ArrayList<>(); + if (all == false) { + // Add some reserved roles; we don't expect these to be returned by the native role store + requestedNames.addAll(randomSubsetOf(randomIntBetween(1, ReservedRolesStore.names().size()), ReservedRolesStore.names())); + requestedStoreNames.addAll(randomSubsetOf(randomIntBetween(1, storeNames.size()), storeNames)); + requestedNames.addAll(requestedStoreNames); + } + + final NativeRolesStore rolesStore = mockNativeRolesStore(requestedNames, storeRoleDescriptors); + + final TransportService transportService = new TransportService( + Settings.EMPTY, + mock(Transport.class), + mock(ThreadPool.class), + TransportService.NOOP_TRANSPORT_INTERCEPTOR, + x -> null, + null, + Collections.emptySet() + ); + final TransportGetRolesAction action = new TransportGetRolesAction( + mock(ActionFilters.class), + rolesStore, + transportService, + new ReservedRolesStore() + ); + + final GetRolesRequest request = new GetRolesRequest(); + request.names(requestedNames.toArray(Strings.EMPTY_ARRAY)); + request.nativeOnly(true); + + final List actualRoleNames = doExecuteSuccessfully(action, request); + if (all) { + assertThat(actualRoleNames, containsInAnyOrder(storeNames.toArray(Strings.EMPTY_ARRAY))); + verify(rolesStore, times(1)).getRoleDescriptors(eq(new HashSet<>()), anyActionListener()); + } else { + assertThat(actualRoleNames, containsInAnyOrder(requestedStoreNames.toArray(Strings.EMPTY_ARRAY))); + verify(rolesStore, times(1)).getRoleDescriptors(eq(new HashSet<>(requestedNames)), anyActionListener()); + } + } + + private List doExecuteSuccessfully(TransportGetRolesAction action, GetRolesRequest request) { + final AtomicReference throwableRef = new AtomicReference<>(); + final AtomicReference responseRef = new AtomicReference<>(); + action.doExecute(mock(Task.class), request, new ActionListener<>() { + @Override + public void onResponse(GetRolesResponse response) { + responseRef.set(response); + } + + @Override + public void onFailure(Exception e) { + throwableRef.set(e); + } + }); + + assertThat(throwableRef.get(), is(nullValue())); + assertThat(responseRef.get(), is(notNullValue())); + return Arrays.stream(responseRef.get().roles()).map(RoleDescriptor::getName).collect(Collectors.toList()); + } + + private NativeRolesStore mockNativeRolesStore(List expectedStoreNames, List storeRoleDescriptors) { + NativeRolesStore rolesStore = mock(NativeRolesStore.class); + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + assert args.length == 2; + @SuppressWarnings("unchecked") + Set requestedNames = (Set) args[0]; + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) args[1]; + if (requestedNames.size() == 0) { + listener.onResponse(RoleRetrievalResult.success(new HashSet<>(storeRoleDescriptors))); + } else { + listener.onResponse( + RoleRetrievalResult.success( + storeRoleDescriptors.stream().filter(r -> requestedNames.contains(r.getName())).collect(Collectors.toSet()) + ) + ); + } + return null; + }).when(rolesStore).getRoleDescriptors(eq(new HashSet<>(expectedStoreNames)), anyActionListener()); + return rolesStore; + } + public void testException() { final Exception e = randomFrom(new ElasticsearchSecurityException(""), new IllegalStateException()); final List storeRoleDescriptors = randomRoleDescriptors(); From d4263c2d4eaa2049a0364039ca350ea1af7b1d3d Mon Sep 17 00:00:00 2001 From: David Turner Date: Wed, 21 Feb 2024 12:00:37 +0000 Subject: [PATCH 110/250] Accept `SocketException` in `Netty4HttpClient` (#105690) It's also possible to get a `Connection reset` if the server closes the channel while we're still sending requests. This commit handles that case in these tests. --- .../java/org/elasticsearch/http/netty4/Netty4HttpClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java index d6ee096b8dfd8..56ba3ae1958f7 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java @@ -40,6 +40,7 @@ import java.io.Closeable; import java.net.SocketAddress; +import java.net.SocketException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; @@ -190,7 +191,7 @@ protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - if (cause instanceof PrematureChannelClosureException) { + if (cause instanceof PrematureChannelClosureException || cause instanceof SocketException) { // no more requests coming, so fast-forward the latch fastForward(); } else { From 9c72157bb7b55e4ba2b7411f325a8b3d214a7370 Mon Sep 17 00:00:00 2001 From: Carlos Delgado <6339205+carlosdelest@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:50:29 +0100 Subject: [PATCH 111/250] Add dense vector inference mock service for testing (#105655) --- .../inference/InferenceBaseRestTest.java | 39 ++- .../xpack/inference/InferenceCrudIT.java | 12 +- .../MockDenseInferenceServiceIT.java | 65 +++++ ...java => MockSparseInferenceServiceIT.java} | 12 +- .../mock/AbstractTestInferenceService.java | 206 ++++++++++++++++ .../TestDenseInferenceServiceExtension.java | 224 ++++++++++++++++++ .../mock/TestInferenceServicePlugin.java | 23 +- ... TestSparseInferenceServiceExtension.java} | 184 +------------- ...search.inference.InferenceServiceExtension | 3 +- 9 files changed, 558 insertions(+), 210 deletions(-) create mode 100644 x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/MockDenseInferenceServiceIT.java rename x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/{MockInferenceServiceIT.java => MockSparseInferenceServiceIT.java} (88%) create mode 100644 x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/AbstractTestInferenceService.java create mode 100644 x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDenseInferenceServiceExtension.java rename x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/{TestInferenceServiceExtension.java => TestSparseInferenceServiceExtension.java} (56%) 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 11a5bdf045f21..a9096f9059c5b 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 @@ -50,11 +50,11 @@ protected Settings restClientSettings() { return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build(); } - static String mockServiceModelConfig() { - return mockServiceModelConfig(null); + static String mockSparseServiceModelConfig() { + return mockSparseServiceModelConfig(null); } - static String mockServiceModelConfig(@Nullable TaskType taskTypeInBody) { + static String mockSparseServiceModelConfig(@Nullable TaskType taskTypeInBody) { var taskType = taskTypeInBody == null ? "" : "\"task_type\": \"" + taskTypeInBody + "\","; return Strings.format(""" { @@ -72,7 +72,7 @@ static String mockServiceModelConfig(@Nullable TaskType taskTypeInBody) { """, taskType); } - static String mockServiceModelConfig(@Nullable TaskType taskTypeInBody, boolean shouldReturnHiddenField) { + static String mockSparseServiceModelConfig(@Nullable TaskType taskTypeInBody, boolean shouldReturnHiddenField) { var taskType = taskTypeInBody == null ? "" : "\"task_type\": \"" + taskTypeInBody + "\","; return Strings.format(""" { @@ -91,6 +91,22 @@ static String mockServiceModelConfig(@Nullable TaskType taskTypeInBody, boolean """, taskType, shouldReturnHiddenField); } + static String mockDenseServiceModelConfig() { + return """ + { + "task_type": "text_embedding", + "service": "text_embedding_test_service", + "service_settings": { + "model": "my_dense_vector_model", + "api_key": "abc64", + "dimensions": 246 + }, + "task_settings": { + } + } + """; + } + protected void deleteModel(String modelId) throws IOException { var request = new Request("DELETE", "_inference/" + modelId); var response = client().performRequest(request); @@ -200,11 +216,16 @@ private Map inferOnMockServiceInternal(String endpoint, List resultMap, int expectedNumberOfResults, TaskType taskType) { - if (taskType == TaskType.SPARSE_EMBEDDING) { - var results = (List>) resultMap.get(TaskType.SPARSE_EMBEDDING.toString()); - assertThat(results, hasSize(expectedNumberOfResults)); - } else { - fail("test with task type [" + taskType + "] are not supported yet"); + switch (taskType) { + case SPARSE_EMBEDDING -> { + var results = (List>) resultMap.get(TaskType.SPARSE_EMBEDDING.toString()); + assertThat(results, hasSize(expectedNumberOfResults)); + } + case TEXT_EMBEDDING -> { + var results = (List>) resultMap.get(TaskType.TEXT_EMBEDDING.toString()); + assertThat(results, hasSize(expectedNumberOfResults)); + } + default -> fail("test with task type [" + taskType + "] are not supported yet"); } } 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 f6718afd2f879..1ecc7980cea99 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 @@ -25,10 +25,10 @@ public class InferenceCrudIT extends InferenceBaseRestTest { @SuppressWarnings("unchecked") public void testGet() throws IOException { for (int i = 0; i < 5; i++) { - putModel("se_model_" + i, mockServiceModelConfig(), TaskType.SPARSE_EMBEDDING); + putModel("se_model_" + i, mockSparseServiceModelConfig(), TaskType.SPARSE_EMBEDDING); } for (int i = 0; i < 4; i++) { - putModel("te_model_" + i, mockServiceModelConfig(), TaskType.TEXT_EMBEDDING); + putModel("te_model_" + i, mockSparseServiceModelConfig(), TaskType.TEXT_EMBEDDING); } var getAllModels = (List>) getAllModels().get("models"); @@ -59,7 +59,7 @@ public void testGet() throws IOException { } public void testGetModelWithWrongTaskType() throws IOException { - putModel("sparse_embedding_model", mockServiceModelConfig(), TaskType.SPARSE_EMBEDDING); + putModel("sparse_embedding_model", mockSparseServiceModelConfig(), TaskType.SPARSE_EMBEDDING); var e = expectThrows(ResponseException.class, () -> getModels("sparse_embedding_model", TaskType.TEXT_EMBEDDING)); assertThat( e.getMessage(), @@ -68,7 +68,7 @@ public void testGetModelWithWrongTaskType() throws IOException { } public void testDeleteModelWithWrongTaskType() throws IOException { - putModel("sparse_embedding_model", mockServiceModelConfig(), TaskType.SPARSE_EMBEDDING); + putModel("sparse_embedding_model", mockSparseServiceModelConfig(), TaskType.SPARSE_EMBEDDING); var e = expectThrows(ResponseException.class, () -> deleteModel("sparse_embedding_model", TaskType.TEXT_EMBEDDING)); assertThat( e.getMessage(), @@ -79,7 +79,7 @@ public void testDeleteModelWithWrongTaskType() throws IOException { @SuppressWarnings("unchecked") public void testGetModelWithAnyTaskType() throws IOException { String inferenceEntityId = "sparse_embedding_model"; - putModel(inferenceEntityId, mockServiceModelConfig(), TaskType.SPARSE_EMBEDDING); + putModel(inferenceEntityId, mockSparseServiceModelConfig(), TaskType.SPARSE_EMBEDDING); var singleModel = (List>) getModels(inferenceEntityId, TaskType.ANY).get("models"); assertEquals(inferenceEntityId, singleModel.get(0).get("model_id")); assertEquals(TaskType.SPARSE_EMBEDDING.toString(), singleModel.get(0).get("task_type")); @@ -88,7 +88,7 @@ public void testGetModelWithAnyTaskType() throws IOException { @SuppressWarnings("unchecked") public void testApisWithoutTaskType() throws IOException { String modelId = "no_task_type_in_url"; - putModel(modelId, mockServiceModelConfig(TaskType.SPARSE_EMBEDDING)); + putModel(modelId, mockSparseServiceModelConfig(TaskType.SPARSE_EMBEDDING)); var singleModel = (List>) getModel(modelId).get("models"); assertEquals(modelId, singleModel.get(0).get("model_id")); assertEquals(TaskType.SPARSE_EMBEDDING.toString(), singleModel.get(0).get("task_type")); diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/MockDenseInferenceServiceIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/MockDenseInferenceServiceIT.java new file mode 100644 index 0000000000000..a8c0a45f3f9db --- /dev/null +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/MockDenseInferenceServiceIT.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.inference.TaskType; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public class MockDenseInferenceServiceIT extends InferenceBaseRestTest { + + @SuppressWarnings("unchecked") + public void testMockService() throws IOException { + String inferenceEntityId = "test-mock"; + var putModel = putModel(inferenceEntityId, mockDenseServiceModelConfig(), TaskType.TEXT_EMBEDDING); + var getModels = getModels(inferenceEntityId, TaskType.TEXT_EMBEDDING); + var model = ((List>) getModels.get("models")).get(0); + + for (var modelMap : List.of(putModel, model)) { + assertEquals(inferenceEntityId, modelMap.get("model_id")); + assertEquals(TaskType.TEXT_EMBEDDING, TaskType.fromString((String) modelMap.get("task_type"))); + assertEquals("text_embedding_test_service", modelMap.get("service")); + } + + // The response is randomly generated, the input can be anything + var inference = inferOnMockService(inferenceEntityId, List.of(randomAlphaOfLength(10))); + assertNonEmptyInferenceResults(inference, 1, TaskType.TEXT_EMBEDDING); + } + + public void testMockServiceWithMultipleInputs() throws IOException { + String inferenceEntityId = "test-mock-with-multi-inputs"; + putModel(inferenceEntityId, mockDenseServiceModelConfig(), TaskType.TEXT_EMBEDDING); + + // The response is randomly generated, the input can be anything + var inference = inferOnMockService( + inferenceEntityId, + TaskType.TEXT_EMBEDDING, + List.of(randomAlphaOfLength(5), randomAlphaOfLength(10), randomAlphaOfLength(15)) + ); + + assertNonEmptyInferenceResults(inference, 3, TaskType.TEXT_EMBEDDING); + } + + @SuppressWarnings("unchecked") + public void testMockService_DoesNotReturnSecretsInGetResponse() throws IOException { + String inferenceEntityId = "test-mock"; + var putModel = putModel(inferenceEntityId, mockDenseServiceModelConfig(), TaskType.TEXT_EMBEDDING); + var getModels = getModels(inferenceEntityId, TaskType.TEXT_EMBEDDING); + var model = ((List>) getModels.get("models")).get(0); + + var serviceSettings = (Map) model.get("service_settings"); + assertNull(serviceSettings.get("api_key")); + assertNotNull(serviceSettings.get("model")); + + var putServiceSettings = (Map) putModel.get("service_settings"); + assertNull(putServiceSettings.get("api_key")); + assertNotNull(putServiceSettings.get("model")); + } +} diff --git a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/MockInferenceServiceIT.java b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/MockSparseInferenceServiceIT.java similarity index 88% rename from x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/MockInferenceServiceIT.java rename to x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/MockSparseInferenceServiceIT.java index c226612d7a6e5..616947eae4d72 100644 --- a/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/MockInferenceServiceIT.java +++ b/x-pack/plugin/inference/qa/inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/inference/MockSparseInferenceServiceIT.java @@ -15,12 +15,12 @@ import static org.hamcrest.Matchers.is; -public class MockInferenceServiceIT extends InferenceBaseRestTest { +public class MockSparseInferenceServiceIT extends InferenceBaseRestTest { @SuppressWarnings("unchecked") public void testMockService() throws IOException { String inferenceEntityId = "test-mock"; - var putModel = putModel(inferenceEntityId, mockServiceModelConfig(), TaskType.SPARSE_EMBEDDING); + var putModel = putModel(inferenceEntityId, mockSparseServiceModelConfig(), TaskType.SPARSE_EMBEDDING); var getModels = getModels(inferenceEntityId, TaskType.SPARSE_EMBEDDING); var model = ((List>) getModels.get("models")).get(0); @@ -37,7 +37,7 @@ public void testMockService() throws IOException { public void testMockServiceWithMultipleInputs() throws IOException { String inferenceEntityId = "test-mock-with-multi-inputs"; - putModel(inferenceEntityId, mockServiceModelConfig(), TaskType.SPARSE_EMBEDDING); + putModel(inferenceEntityId, mockSparseServiceModelConfig(), TaskType.SPARSE_EMBEDDING); // The response is randomly generated, the input can be anything var inference = inferOnMockService( @@ -52,7 +52,7 @@ public void testMockServiceWithMultipleInputs() throws IOException { @SuppressWarnings("unchecked") public void testMockService_DoesNotReturnSecretsInGetResponse() throws IOException { String inferenceEntityId = "test-mock"; - var putModel = putModel(inferenceEntityId, mockServiceModelConfig(), TaskType.SPARSE_EMBEDDING); + var putModel = putModel(inferenceEntityId, mockSparseServiceModelConfig(), TaskType.SPARSE_EMBEDDING); var getModels = getModels(inferenceEntityId, TaskType.SPARSE_EMBEDDING); var model = ((List>) getModels.get("models")).get(0); @@ -68,7 +68,7 @@ public void testMockService_DoesNotReturnSecretsInGetResponse() throws IOExcepti @SuppressWarnings("unchecked") public void testMockService_DoesNotReturnHiddenField_InModelResponses() throws IOException { String inferenceEntityId = "test-mock"; - var putModel = putModel(inferenceEntityId, mockServiceModelConfig(), TaskType.SPARSE_EMBEDDING); + var putModel = putModel(inferenceEntityId, mockSparseServiceModelConfig(), TaskType.SPARSE_EMBEDDING); var getModels = getModels(inferenceEntityId, TaskType.SPARSE_EMBEDDING); var model = ((List>) getModels.get("models")).get(0); @@ -87,7 +87,7 @@ public void testMockService_DoesNotReturnHiddenField_InModelResponses() throws I @SuppressWarnings("unchecked") public void testMockService_DoesReturnHiddenField_InModelResponses() throws IOException { String inferenceEntityId = "test-mock"; - var putModel = putModel(inferenceEntityId, mockServiceModelConfig(null, true), TaskType.SPARSE_EMBEDDING); + var putModel = putModel(inferenceEntityId, mockSparseServiceModelConfig(null, true), TaskType.SPARSE_EMBEDDING); var getModels = getModels(inferenceEntityId, TaskType.SPARSE_EMBEDDING); var model = ((List>) getModels.get("models")).get(0); diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/AbstractTestInferenceService.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/AbstractTestInferenceService.java new file mode 100644 index 0000000000000..99dfc9582eb05 --- /dev/null +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/AbstractTestInferenceService.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.xpack.inference.mock; + +import org.elasticsearch.TransportVersion; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.ValidationException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.inference.InferenceService; +import org.elasticsearch.inference.Model; +import org.elasticsearch.inference.ModelConfigurations; +import org.elasticsearch.inference.ModelSecrets; +import org.elasticsearch.inference.SecretSettings; +import org.elasticsearch.inference.ServiceSettings; +import org.elasticsearch.inference.TaskSettings; +import org.elasticsearch.inference.TaskType; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Map; + +public abstract class AbstractTestInferenceService implements InferenceService { + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersion.current(); // fine for these tests but will not work for cluster upgrade tests + } + + @SuppressWarnings("unchecked") + protected static Map getTaskSettingsMap(Map settings) { + Map taskSettingsMap; + // task settings are optional + if (settings.containsKey(ModelConfigurations.TASK_SETTINGS)) { + taskSettingsMap = (Map) settings.remove(ModelConfigurations.TASK_SETTINGS); + } else { + taskSettingsMap = Map.of(); + } + + return taskSettingsMap; + } + + @Override + @SuppressWarnings("unchecked") + public TestServiceModel parsePersistedConfigWithSecrets( + String modelId, + TaskType taskType, + Map config, + Map secrets + ) { + var serviceSettingsMap = (Map) config.remove(ModelConfigurations.SERVICE_SETTINGS); + var secretSettingsMap = (Map) secrets.remove(ModelSecrets.SECRET_SETTINGS); + + var serviceSettings = getServiceSettingsFromMap(serviceSettingsMap); + var secretSettings = TestSecretSettings.fromMap(secretSettingsMap); + + var taskSettingsMap = getTaskSettingsMap(config); + var taskSettings = TestTaskSettings.fromMap(taskSettingsMap); + + return new TestServiceModel(modelId, taskType, name(), serviceSettings, taskSettings, secretSettings); + } + + @Override + @SuppressWarnings("unchecked") + public Model parsePersistedConfig(String modelId, TaskType taskType, Map config) { + var serviceSettingsMap = (Map) config.remove(ModelConfigurations.SERVICE_SETTINGS); + + var serviceSettings = getServiceSettingsFromMap(serviceSettingsMap); + + var taskSettingsMap = getTaskSettingsMap(config); + var taskSettings = TestTaskSettings.fromMap(taskSettingsMap); + + return new TestServiceModel(modelId, taskType, name(), serviceSettings, taskSettings, null); + } + + protected abstract ServiceSettings getServiceSettingsFromMap(Map serviceSettingsMap); + + @Override + public void start(Model model, ActionListener listener) { + listener.onResponse(true); + } + + @Override + public void close() throws IOException {} + + public static class TestServiceModel extends Model { + + public TestServiceModel( + String modelId, + TaskType taskType, + String service, + ServiceSettings serviceSettings, + TestTaskSettings taskSettings, + TestSecretSettings secretSettings + ) { + super(new ModelConfigurations(modelId, taskType, service, serviceSettings, taskSettings), new ModelSecrets(secretSettings)); + } + + @Override + public TestDenseInferenceServiceExtension.TestServiceSettings getServiceSettings() { + return (TestDenseInferenceServiceExtension.TestServiceSettings) super.getServiceSettings(); + } + + @Override + public TestTaskSettings getTaskSettings() { + return (TestTaskSettings) super.getTaskSettings(); + } + + @Override + public TestSecretSettings getSecretSettings() { + return (TestSecretSettings) super.getSecretSettings(); + } + } + + public record TestTaskSettings(Integer temperature) implements TaskSettings { + + static final String NAME = "test_task_settings"; + + public static TestTaskSettings fromMap(Map map) { + Integer temperature = (Integer) map.remove("temperature"); + return new TestTaskSettings(temperature); + } + + public TestTaskSettings(StreamInput in) throws IOException { + this(in.readOptionalVInt()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeOptionalVInt(temperature); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (temperature != null) { + builder.field("temperature", temperature); + } + builder.endObject(); + return builder; + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersion.current(); // fine for these tests but will not work for cluster upgrade tests + } + } + + public record TestSecretSettings(String apiKey) implements SecretSettings { + + static final String NAME = "test_secret_settings"; + + public static TestSecretSettings fromMap(Map map) { + ValidationException validationException = new ValidationException(); + + String apiKey = (String) map.remove("api_key"); + + if (apiKey == null) { + validationException.addValidationError("missing api_key"); + } + + if (validationException.validationErrors().isEmpty() == false) { + throw validationException; + } + + return new TestSecretSettings(apiKey); + } + + public TestSecretSettings(StreamInput in) throws IOException { + this(in.readString()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(apiKey); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("api_key", apiKey); + builder.endObject(); + return builder; + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersion.current(); // fine for these tests but will not work for cluster upgrade tests + } + } +} diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDenseInferenceServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDenseInferenceServiceExtension.java new file mode 100644 index 0000000000000..54fe6e01946b4 --- /dev/null +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestDenseInferenceServiceExtension.java @@ -0,0 +1,224 @@ +/* + * 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.mock; + +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.ValidationException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.inference.ChunkedInferenceServiceResults; +import org.elasticsearch.inference.ChunkingOptions; +import org.elasticsearch.inference.InferenceServiceExtension; +import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; +import org.elasticsearch.inference.Model; +import org.elasticsearch.inference.ModelConfigurations; +import org.elasticsearch.inference.ServiceSettings; +import org.elasticsearch.inference.SimilarityMeasure; +import org.elasticsearch.inference.TaskType; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xpack.core.inference.results.TextEmbeddingResults; +import org.elasticsearch.xpack.core.ml.inference.results.ChunkedTextEmbeddingResults; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class TestDenseInferenceServiceExtension implements InferenceServiceExtension { + @Override + public List getInferenceServiceFactories() { + return List.of(TestInferenceService::new); + } + + public static class TestInferenceService extends AbstractTestInferenceService { + private static final String NAME = "text_embedding_test_service"; + + public TestInferenceService(InferenceServiceFactoryContext context) {} + + @Override + public String name() { + return NAME; + } + + @Override + @SuppressWarnings("unchecked") + public void parseRequestConfig( + String modelId, + TaskType taskType, + Map config, + Set platformArchitectures, + ActionListener parsedModelListener + ) { + var serviceSettingsMap = (Map) config.remove(ModelConfigurations.SERVICE_SETTINGS); + var serviceSettings = TestServiceSettings.fromMap(serviceSettingsMap); + var secretSettings = TestSecretSettings.fromMap(serviceSettingsMap); + + var taskSettingsMap = getTaskSettingsMap(config); + var taskSettings = TestTaskSettings.fromMap(taskSettingsMap); + + parsedModelListener.onResponse(new TestServiceModel(modelId, taskType, name(), serviceSettings, taskSettings, secretSettings)); + } + + @Override + public void infer( + Model model, + List input, + Map taskSettings, + InputType inputType, + ActionListener listener + ) { + switch (model.getConfigurations().getTaskType()) { + case ANY, TEXT_EMBEDDING -> listener.onResponse( + makeResults(input, ((TestServiceModel) model).getServiceSettings().dimensions()) + ); + default -> listener.onFailure( + new ElasticsearchStatusException( + TaskType.unsupportedTaskTypeErrorMsg(model.getConfigurations().getTaskType(), name()), + RestStatus.BAD_REQUEST + ) + ); + } + } + + @Override + public void chunkedInfer( + Model model, + List input, + Map taskSettings, + InputType inputType, + ChunkingOptions chunkingOptions, + ActionListener> listener + ) { + switch (model.getConfigurations().getTaskType()) { + case ANY, TEXT_EMBEDDING -> listener.onResponse( + makeChunkedResults(input, ((TestServiceModel) model).getServiceSettings().dimensions()) + ); + default -> listener.onFailure( + new ElasticsearchStatusException( + TaskType.unsupportedTaskTypeErrorMsg(model.getConfigurations().getTaskType(), name()), + RestStatus.BAD_REQUEST + ) + ); + } + } + + private TextEmbeddingResults makeResults(List input, int dimensions) { + List embeddings = new ArrayList<>(); + for (int i = 0; i < input.size(); i++) { + List values = new ArrayList<>(); + for (int j = 0; j < dimensions; j++) { + values.add((float) j); + } + embeddings.add(new TextEmbeddingResults.Embedding(values)); + } + return new TextEmbeddingResults(embeddings); + } + + private List makeChunkedResults(List input, int dimensions) { + var results = new ArrayList(); + for (int i = 0; i < input.size(); i++) { + double[] values = new double[dimensions]; + for (int j = 0; j < 5; j++) { + values[j] = j; + } + results.add( + new org.elasticsearch.xpack.core.inference.results.ChunkedTextEmbeddingResults( + List.of(new ChunkedTextEmbeddingResults.EmbeddingChunk(input.get(i), values)) + ) + ); + } + return results; + } + + protected ServiceSettings getServiceSettingsFromMap(Map serviceSettingsMap) { + return TestServiceSettings.fromMap(serviceSettingsMap); + } + } + + public record TestServiceSettings(String model, Integer dimensions, SimilarityMeasure similarity) implements ServiceSettings { + + static final String NAME = "test_text_embedding_service_settings"; + + public static TestServiceSettings fromMap(Map map) { + ValidationException validationException = new ValidationException(); + + String model = (String) map.remove("model"); + if (model == null) { + validationException.addValidationError("missing model"); + } + + Integer dimensions = (Integer) map.remove("dimensions"); + if (dimensions == null) { + validationException.addValidationError("missing dimensions"); + } + + SimilarityMeasure similarity = null; + String similarityStr = (String) map.remove("similarity"); + if (similarityStr != null) { + similarity = SimilarityMeasure.valueOf(similarityStr); + } + + return new TestServiceSettings(model, dimensions, similarity); + } + + public TestServiceSettings(StreamInput in) throws IOException { + this(in.readString(), in.readOptionalInt(), in.readOptionalEnum(SimilarityMeasure.class)); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("model", model); + builder.field("dimensions", dimensions); + if (similarity != null) { + builder.field("similarity", similarity); + } + builder.endObject(); + return builder; + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersion.current(); // fine for these tests but will not work for cluster upgrade tests + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(model); + out.writeInt(dimensions); + out.writeOptionalEnum(similarity); + } + + @Override + public ToXContentObject getFilteredXContentObject() { + return (builder, params) -> { + builder.startObject(); + builder.field("model", model); + builder.field("dimensions", dimensions); + if (similarity != null) { + builder.field("similarity", similarity); + } + builder.endObject(); + return builder; + }; + } + + } + +} diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java index 0345d7b6e5926..6460b06f13800 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServicePlugin.java @@ -20,20 +20,25 @@ public class TestInferenceServicePlugin extends Plugin { @Override public List getNamedWriteables() { return List.of( - new NamedWriteableRegistry.Entry( - ServiceSettings.class, - TestInferenceServiceExtension.TestServiceSettings.NAME, - TestInferenceServiceExtension.TestServiceSettings::new - ), new NamedWriteableRegistry.Entry( TaskSettings.class, - TestInferenceServiceExtension.TestTaskSettings.NAME, - TestInferenceServiceExtension.TestTaskSettings::new + AbstractTestInferenceService.TestTaskSettings.NAME, + AbstractTestInferenceService.TestTaskSettings::new ), new NamedWriteableRegistry.Entry( SecretSettings.class, - TestInferenceServiceExtension.TestSecretSettings.NAME, - TestInferenceServiceExtension.TestSecretSettings::new + AbstractTestInferenceService.TestSecretSettings.NAME, + AbstractTestInferenceService.TestSecretSettings::new + ), + new NamedWriteableRegistry.Entry( + ServiceSettings.class, + TestDenseInferenceServiceExtension.TestServiceSettings.NAME, + TestDenseInferenceServiceExtension.TestServiceSettings::new + ), + new NamedWriteableRegistry.Entry( + ServiceSettings.class, + TestSparseInferenceServiceExtension.TestServiceSettings.NAME, + TestSparseInferenceServiceExtension.TestServiceSettings::new ) ); } diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestSparseInferenceServiceExtension.java similarity index 56% rename from x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServiceExtension.java rename to x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestSparseInferenceServiceExtension.java index 215125960c4fc..e5020774a70f3 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestInferenceServiceExtension.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestSparseInferenceServiceExtension.java @@ -15,16 +15,12 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.inference.ChunkedInferenceServiceResults; import org.elasticsearch.inference.ChunkingOptions; -import org.elasticsearch.inference.InferenceService; import org.elasticsearch.inference.InferenceServiceExtension; import org.elasticsearch.inference.InferenceServiceResults; import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelConfigurations; -import org.elasticsearch.inference.ModelSecrets; -import org.elasticsearch.inference.SecretSettings; import org.elasticsearch.inference.ServiceSettings; -import org.elasticsearch.inference.TaskSettings; import org.elasticsearch.inference.TaskType; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xcontent.ToXContentObject; @@ -40,13 +36,13 @@ import java.util.Map; import java.util.Set; -public class TestInferenceServiceExtension implements InferenceServiceExtension { +public class TestSparseInferenceServiceExtension implements InferenceServiceExtension { @Override public List getInferenceServiceFactories() { return List.of(TestInferenceService::new); } - public static class TestInferenceService implements InferenceService { + public static class TestInferenceService extends AbstractTestInferenceService { private static final String NAME = "test_service"; public TestInferenceService(InferenceServiceExtension.InferenceServiceFactoryContext context) {} @@ -56,31 +52,13 @@ public String name() { return NAME; } - @Override - public TransportVersion getMinimalSupportedVersion() { - return TransportVersion.current(); // fine for these tests but will not work for cluster upgrade tests - } - - @SuppressWarnings("unchecked") - private static Map getTaskSettingsMap(Map settings) { - Map taskSettingsMap; - // task settings are optional - if (settings.containsKey(ModelConfigurations.TASK_SETTINGS)) { - taskSettingsMap = (Map) settings.remove(ModelConfigurations.TASK_SETTINGS); - } else { - taskSettingsMap = Map.of(); - } - - return taskSettingsMap; - } - @Override @SuppressWarnings("unchecked") public void parseRequestConfig( String modelId, TaskType taskType, Map config, - Set platfromArchitectures, + Set platformArchitectures, ActionListener parsedModelListener ) { var serviceSettingsMap = (Map) config.remove(ModelConfigurations.SERVICE_SETTINGS); @@ -93,39 +71,6 @@ public void parseRequestConfig( parsedModelListener.onResponse(new TestServiceModel(modelId, taskType, name(), serviceSettings, taskSettings, secretSettings)); } - @Override - @SuppressWarnings("unchecked") - public TestServiceModel parsePersistedConfigWithSecrets( - String modelId, - TaskType taskType, - Map config, - Map secrets - ) { - var serviceSettingsMap = (Map) config.remove(ModelConfigurations.SERVICE_SETTINGS); - var secretSettingsMap = (Map) secrets.remove(ModelSecrets.SECRET_SETTINGS); - - var serviceSettings = TestServiceSettings.fromMap(serviceSettingsMap); - var secretSettings = TestSecretSettings.fromMap(secretSettingsMap); - - var taskSettingsMap = getTaskSettingsMap(config); - var taskSettings = TestTaskSettings.fromMap(taskSettingsMap); - - return new TestServiceModel(modelId, taskType, name(), serviceSettings, taskSettings, secretSettings); - } - - @Override - @SuppressWarnings("unchecked") - public Model parsePersistedConfig(String modelId, TaskType taskType, Map config) { - var serviceSettingsMap = (Map) config.remove(ModelConfigurations.SERVICE_SETTINGS); - - var serviceSettings = TestServiceSettings.fromMap(serviceSettingsMap); - - var taskSettingsMap = getTaskSettingsMap(config); - var taskSettings = TestTaskSettings.fromMap(taskSettingsMap); - - return new TestServiceModel(modelId, taskType, name(), serviceSettings, taskSettings, null); - } - @Override public void infer( Model model, @@ -189,42 +134,10 @@ private List makeChunkedResults(List inp return List.of(new ChunkedSparseEmbeddingResults(chunks)); } - @Override - public void start(Model model, ActionListener listener) { - listener.onResponse(true); - } - - @Override - public void close() throws IOException {} - } - - public static class TestServiceModel extends Model { - - public TestServiceModel( - String modelId, - TaskType taskType, - String service, - TestServiceSettings serviceSettings, - TestTaskSettings taskSettings, - TestSecretSettings secretSettings - ) { - super(new ModelConfigurations(modelId, taskType, service, serviceSettings, taskSettings), new ModelSecrets(secretSettings)); - } - - @Override - public TestServiceSettings getServiceSettings() { - return (TestServiceSettings) super.getServiceSettings(); - } - - @Override - public TestTaskSettings getTaskSettings() { - return (TestTaskSettings) super.getTaskSettings(); + protected ServiceSettings getServiceSettingsFromMap(Map serviceSettingsMap) { + return TestServiceSettings.fromMap(serviceSettingsMap); } - @Override - public TestSecretSettings getSecretSettings() { - return (TestSecretSettings) super.getSecretSettings(); - } } public record TestServiceSettings(String model, String hiddenField, boolean shouldReturnHiddenField) implements ServiceSettings { @@ -300,91 +213,4 @@ public ToXContentObject getFilteredXContentObject() { }; } } - - public record TestTaskSettings(Integer temperature) implements TaskSettings { - - static final String NAME = "test_task_settings"; - - public static TestTaskSettings fromMap(Map map) { - Integer temperature = (Integer) map.remove("temperature"); - return new TestTaskSettings(temperature); - } - - public TestTaskSettings(StreamInput in) throws IOException { - this(in.readOptionalVInt()); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeOptionalVInt(temperature); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - if (temperature != null) { - builder.field("temperature", temperature); - } - builder.endObject(); - return builder; - } - - @Override - public String getWriteableName() { - return NAME; - } - - @Override - public TransportVersion getMinimalSupportedVersion() { - return TransportVersion.current(); // fine for these tests but will not work for cluster upgrade tests - } - } - - public record TestSecretSettings(String apiKey) implements SecretSettings { - - static final String NAME = "test_secret_settings"; - - public static TestSecretSettings fromMap(Map map) { - ValidationException validationException = new ValidationException(); - - String apiKey = (String) map.remove("api_key"); - - if (apiKey == null) { - validationException.addValidationError("missing api_key"); - } - - if (validationException.validationErrors().isEmpty() == false) { - throw validationException; - } - - return new TestSecretSettings(apiKey); - } - - public TestSecretSettings(StreamInput in) throws IOException { - this(in.readString()); - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(apiKey); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("api_key", apiKey); - builder.endObject(); - return builder; - } - - @Override - public String getWriteableName() { - return NAME; - } - - @Override - public TransportVersion getMinimalSupportedVersion() { - return TransportVersion.current(); // fine for these tests but will not work for cluster upgrade tests - } - } } diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension b/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension index 019a6dad7be85..c1908dc788251 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/resources/META-INF/services/org.elasticsearch.inference.InferenceServiceExtension @@ -1 +1,2 @@ -org.elasticsearch.xpack.inference.mock.TestInferenceServiceExtension +org.elasticsearch.xpack.inference.mock.TestSparseInferenceServiceExtension +org.elasticsearch.xpack.inference.mock.TestDenseInferenceServiceExtension From 1f1636e1f7665fcfdf8dca491d9ca828bc659526 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 21 Feb 2024 14:09:54 +0100 Subject: [PATCH 112/250] Fix error 500 on invalid ParentIdQuery (#105693) We need to enforce non-null values here, otherwise we'll error out and return a 500 when a user fails to set either id or type. closes #105366 --- docs/changelog/105693.yaml | 6 ++++++ .../org/elasticsearch/join/query/ParentIdQueryBuilder.java | 4 ++-- .../elasticsearch/join/query/ParentIdQueryBuilderTests.java | 5 +++++ 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/105693.yaml diff --git a/docs/changelog/105693.yaml b/docs/changelog/105693.yaml new file mode 100644 index 0000000000000..8d14d611e19a3 --- /dev/null +++ b/docs/changelog/105693.yaml @@ -0,0 +1,6 @@ +pr: 105693 +summary: Fix error 500 on invalid `ParentIdQuery` +area: Search +type: bug +issues: + - 105366 diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentIdQueryBuilder.java b/modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentIdQueryBuilder.java index 8fb72ddce1935..89850862cd63f 100644 --- a/modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentIdQueryBuilder.java +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentIdQueryBuilder.java @@ -51,8 +51,8 @@ public final class ParentIdQueryBuilder extends AbstractQueryBuilder new ParentIdQueryBuilder(null, randomAlphaOfLength(5))); + expectThrows(IllegalArgumentException.class, () -> new ParentIdQueryBuilder(randomAlphaOfLength(5), null)); + } + public void testDisallowExpensiveQueries() { SearchExecutionContext searchExecutionContext = mock(SearchExecutionContext.class); when(searchExecutionContext.allowExpensiveQueries()).thenReturn(false); From d693fc8b1939dbf94d8ee34c467ca8d3e2a9e4a1 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 21 Feb 2024 14:12:52 +0100 Subject: [PATCH 113/250] Fix search response leaks in async search tests (#105675) Fixing all of these muted test classes, tried my best to keep indention changes to a minimum but it wasn't possible to avoid them in all cases unfortunately. --- .../test/rest/RestActionTestCase.java | 5 +- .../search/AsyncSearchResponseTests.java | 694 ++++++++++-------- .../search/AsyncStatusResponseTests.java | 45 +- .../RestSubmitAsyncSearchActionTests.java | 2 - 4 files changed, 403 insertions(+), 343 deletions(-) diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/RestActionTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/rest/RestActionTestCase.java index 9e638425d5c5c..fad8575ae1d58 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/rest/RestActionTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/rest/RestActionTestCase.java @@ -13,6 +13,7 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.ActionType; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.Releasables; import org.elasticsearch.indices.breaker.NoneCircuitBreakerService; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestRequest; @@ -68,6 +69,8 @@ protected void dispatchRequest(RestRequest request) { ThreadContext threadContext = verifyingClient.threadPool().getThreadContext(); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { controller.dispatchRequest(request, channel, threadContext); + } finally { + Releasables.close(channel.capturedResponse()); } } @@ -154,7 +157,7 @@ public Task exe ) { @SuppressWarnings("unchecked") // Callers are responsible for lining this up Response response = (Response) executeLocallyVerifier.get().apply(action, request); - listener.onResponse(response); + ActionListener.respondAndRelease(listener, response); return request.createTask( taskIdGenerator.incrementAndGet(), "transport", diff --git a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncSearchResponseTests.java b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncSearchResponseTests.java index afabd8c7a7bc3..98513f611a5d8 100644 --- a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncSearchResponseTests.java +++ b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncSearchResponseTests.java @@ -8,7 +8,6 @@ package org.elasticsearch.xpack.search; import org.apache.lucene.index.CorruptIndexException; -import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.TransportVersion; import org.elasticsearch.action.OriginalIndices; import org.elasticsearch.action.search.SearchResponse; @@ -33,6 +32,7 @@ import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.search.action.AsyncSearchResponse; +import org.junit.After; import org.junit.Before; import java.io.IOException; @@ -48,7 +48,6 @@ import static java.util.Collections.emptyList; import static org.elasticsearch.xpack.core.async.GetAsyncResultRequestTests.randomSearchId; -@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/104838") public class AsyncSearchResponseTests extends ESTestCase { private final SearchResponse searchResponse = randomSearchResponse(randomBoolean()); private NamedWriteableRegistry namedWriteableRegistry; @@ -61,6 +60,11 @@ public void registerNamedObjects() { namedWriteableRegistry = new NamedWriteableRegistry(namedWriteables); } + @After + public void releaseResponse() { + searchResponse.decRef(); + } + protected Writeable.Reader instanceReader() { return AsyncSearchResponse::new; } @@ -77,7 +81,11 @@ protected void assertEqualInstances(AsyncSearchResponse expectedInstance, AsyncS public final void testSerialization() throws IOException { for (int runs = 0; runs < 10; runs++) { AsyncSearchResponse testInstance = createTestInstance(); - assertSerialization(testInstance); + try { + assertSerialization(testInstance).decRef(); + } finally { + testInstance.decRef(); + } } } @@ -160,44 +168,47 @@ static void assertEqualResponses(AsyncSearchResponse expected, AsyncSearchRespon public void testToXContentWithoutSearchResponse() throws IOException { Date date = new Date(); AsyncSearchResponse asyncSearchResponse = new AsyncSearchResponse("id", true, true, date.getTime(), date.getTime()); + try { + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + ChunkedToXContent.wrapAsToXContent(asyncSearchResponse).toXContent(builder, ToXContent.EMPTY_PARAMS); + assertEquals(Strings.format(""" + { + "id" : "id", + "is_partial" : true, + "is_running" : true, + "start_time_in_millis" : %s, + "expiration_time_in_millis" : %s + }""", date.getTime(), date.getTime()), Strings.toString(builder)); + } - try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { - builder.prettyPrint(); - ChunkedToXContent.wrapAsToXContent(asyncSearchResponse).toXContent(builder, ToXContent.EMPTY_PARAMS); - assertEquals(Strings.format(""" - { - "id" : "id", - "is_partial" : true, - "is_running" : true, - "start_time_in_millis" : %s, - "expiration_time_in_millis" : %s - }""", date.getTime(), date.getTime()), Strings.toString(builder)); - } - - try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { - builder.prettyPrint(); - builder.humanReadable(true); - ChunkedToXContent.wrapAsToXContent(asyncSearchResponse) - .toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("human", "true"))); - assertEquals( - Strings.format( - """ - { - "id" : "id", - "is_partial" : true, - "is_running" : true, - "start_time" : "%s", - "start_time_in_millis" : %s, - "expiration_time" : "%s", - "expiration_time_in_millis" : %s - }""", - XContentElasticsearchExtension.DEFAULT_FORMATTER.format(date.toInstant()), - date.getTime(), - XContentElasticsearchExtension.DEFAULT_FORMATTER.format(date.toInstant()), - date.getTime() - ), - Strings.toString(builder) - ); + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + builder.humanReadable(true); + ChunkedToXContent.wrapAsToXContent(asyncSearchResponse) + .toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("human", "true"))); + assertEquals( + Strings.format( + """ + { + "id" : "id", + "is_partial" : true, + "is_running" : true, + "start_time" : "%s", + "start_time_in_millis" : %s, + "expiration_time" : "%s", + "expiration_time_in_millis" : %s + }""", + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(date.toInstant()), + date.getTime(), + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(date.toInstant()), + date.getTime() + ), + Strings.toString(builder) + ); + } + } finally { + asyncSearchResponse.decRef(); } } @@ -227,89 +238,98 @@ public void testToXContentWithSearchResponseAfterCompletion() throws IOException SearchResponse.Clusters.EMPTY ); - AsyncSearchResponse asyncSearchResponse = new AsyncSearchResponse( - "id", - searchResponse, - null, - false, - isRunning, - startTimeMillis, - expirationTimeMillis - ); - - try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { - builder.prettyPrint(); - ChunkedToXContent.wrapAsToXContent(asyncSearchResponse).toXContent(builder, ToXContent.EMPTY_PARAMS); - assertEquals(Strings.format(""" - { - "id" : "id", - "is_partial" : false, - "is_running" : false, - "start_time_in_millis" : %s, - "expiration_time_in_millis" : %s, - "completion_time_in_millis" : %s, - "response" : { - "took" : %s, - "timed_out" : false, - "num_reduce_phases" : 2, - "_shards" : { - "total" : 10, - "successful" : 9, - "skipped" : 1, - "failed" : 0 - }, - "hits" : { - "max_score" : 0.0, - "hits" : [ ] - } - } - }""", startTimeMillis, expirationTimeMillis, expectedCompletionTime, took), Strings.toString(builder)); + AsyncSearchResponse asyncSearchResponse; + try { + asyncSearchResponse = new AsyncSearchResponse( + "id", + searchResponse, + null, + false, + isRunning, + startTimeMillis, + expirationTimeMillis + ); + } finally { + searchResponse.decRef(); } - try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { - builder.prettyPrint(); - builder.humanReadable(true); - ChunkedToXContent.wrapAsToXContent(asyncSearchResponse) - .toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("human", "true"))); - assertEquals( - Strings.format( - """ - { - "id" : "id", - "is_partial" : false, - "is_running" : false, - "start_time" : "%s", - "start_time_in_millis" : %s, - "expiration_time" : "%s", - "expiration_time_in_millis" : %s, - "completion_time" : "%s", - "completion_time_in_millis" : %s, - "response" : { - "took" : %s, - "timed_out" : false, - "num_reduce_phases" : 2, - "_shards" : { - "total" : 10, - "successful" : 9, - "skipped" : 1, - "failed" : 0 - }, - "hits" : { - "max_score" : 0.0, - "hits" : [ ] - } - } - }""", - XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(startTimeMillis)), - startTimeMillis, - XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expirationTimeMillis)), - expirationTimeMillis, - XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expectedCompletionTime)), - expectedCompletionTime, - took - ), - Strings.toString(builder) - ); + try { + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + ChunkedToXContent.wrapAsToXContent(asyncSearchResponse).toXContent(builder, ToXContent.EMPTY_PARAMS); + assertEquals(Strings.format(""" + { + "id" : "id", + "is_partial" : false, + "is_running" : false, + "start_time_in_millis" : %s, + "expiration_time_in_millis" : %s, + "completion_time_in_millis" : %s, + "response" : { + "took" : %s, + "timed_out" : false, + "num_reduce_phases" : 2, + "_shards" : { + "total" : 10, + "successful" : 9, + "skipped" : 1, + "failed" : 0 + }, + "hits" : { + "max_score" : 0.0, + "hits" : [ ] + } + } + }""", startTimeMillis, expirationTimeMillis, expectedCompletionTime, took), Strings.toString(builder)); + } + + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + builder.humanReadable(true); + ChunkedToXContent.wrapAsToXContent(asyncSearchResponse) + .toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("human", "true"))); + assertEquals( + Strings.format( + """ + { + "id" : "id", + "is_partial" : false, + "is_running" : false, + "start_time" : "%s", + "start_time_in_millis" : %s, + "expiration_time" : "%s", + "expiration_time_in_millis" : %s, + "completion_time" : "%s", + "completion_time_in_millis" : %s, + "response" : { + "took" : %s, + "timed_out" : false, + "num_reduce_phases" : 2, + "_shards" : { + "total" : 10, + "successful" : 9, + "skipped" : 1, + "failed" : 0 + }, + "hits" : { + "max_score" : 0.0, + "hits" : [ ] + } + } + }""", + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(startTimeMillis)), + startTimeMillis, + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expirationTimeMillis)), + expirationTimeMillis, + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expectedCompletionTime)), + expectedCompletionTime, + took + ), + Strings.toString(builder) + ); + } + } finally { + asyncSearchResponse.decRef(); } } @@ -339,135 +359,143 @@ public void testToXContentWithCCSSearchResponseWhileRunning() throws IOException ShardSearchFailure.EMPTY_ARRAY, clusters ); + AsyncSearchResponse asyncSearchResponse; + try { + asyncSearchResponse = new AsyncSearchResponse( + "id", + searchResponse, + null, + true, + isRunning, + startTimeMillis, + expirationTimeMillis + ); + } finally { + searchResponse.decRef(); + } - AsyncSearchResponse asyncSearchResponse = new AsyncSearchResponse( - "id", - searchResponse, - null, - true, - isRunning, - startTimeMillis, - expirationTimeMillis - ); - - try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { - builder.prettyPrint(); - ChunkedToXContent.wrapAsToXContent(asyncSearchResponse).toXContent(builder, ToXContent.EMPTY_PARAMS); - assertEquals(Strings.format(""" - { - "id" : "id", - "is_partial" : true, - "is_running" : true, - "start_time_in_millis" : %s, - "expiration_time_in_millis" : %s, - "response" : { - "took" : %s, - "timed_out" : false, - "num_reduce_phases" : 2, - "_shards" : { - "total" : 10, - "successful" : 9, - "skipped" : 1, - "failed" : 0 - }, - "_clusters" : { - "total" : 3, - "successful" : 0, - "skipped" : 0, - "running" : 3, - "partial" : 0, - "failed" : 0, - "details" : { - "cluster_1" : { - "status" : "running", - "indices" : "foo,bar*", - "timed_out" : false + try { + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + ChunkedToXContent.wrapAsToXContent(asyncSearchResponse).toXContent(builder, ToXContent.EMPTY_PARAMS); + assertEquals(Strings.format(""" + { + "id" : "id", + "is_partial" : true, + "is_running" : true, + "start_time_in_millis" : %s, + "expiration_time_in_millis" : %s, + "response" : { + "took" : %s, + "timed_out" : false, + "num_reduce_phases" : 2, + "_shards" : { + "total" : 10, + "successful" : 9, + "skipped" : 1, + "failed" : 0 }, - "cluster_2" : { - "status" : "running", - "indices" : "foo,bar*", - "timed_out" : false + "_clusters" : { + "total" : 3, + "successful" : 0, + "skipped" : 0, + "running" : 3, + "partial" : 0, + "failed" : 0, + "details" : { + "cluster_1" : { + "status" : "running", + "indices" : "foo,bar*", + "timed_out" : false + }, + "cluster_2" : { + "status" : "running", + "indices" : "foo,bar*", + "timed_out" : false + }, + "cluster_0" : { + "status" : "running", + "indices" : "foo,bar*", + "timed_out" : false + } + } }, - "cluster_0" : { - "status" : "running", - "indices" : "foo,bar*", - "timed_out" : false + "hits" : { + "max_score" : 0.0, + "hits" : [ ] } } - }, - "hits" : { - "max_score" : 0.0, - "hits" : [ ] - } - } - }""", startTimeMillis, expirationTimeMillis, took), Strings.toString(builder)); - } + }""", startTimeMillis, expirationTimeMillis, took), Strings.toString(builder)); + } - try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { - builder.prettyPrint(); - builder.humanReadable(true); - ChunkedToXContent.wrapAsToXContent(asyncSearchResponse) - .toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("human", "true"))); - assertEquals( - Strings.format( - """ - { - "id" : "id", - "is_partial" : true, - "is_running" : true, - "start_time" : "%s", - "start_time_in_millis" : %s, - "expiration_time" : "%s", - "expiration_time_in_millis" : %s, - "response" : { - "took" : %s, - "timed_out" : false, - "num_reduce_phases" : 2, - "_shards" : { - "total" : 10, - "successful" : 9, - "skipped" : 1, - "failed" : 0 - }, - "_clusters" : { - "total" : 3, - "successful" : 0, - "skipped" : 0, - "running" : 3, - "partial" : 0, - "failed" : 0, - "details" : { - "cluster_1" : { - "status" : "running", - "indices" : "foo,bar*", - "timed_out" : false + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + builder.humanReadable(true); + ChunkedToXContent.wrapAsToXContent(asyncSearchResponse) + .toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("human", "true"))); + assertEquals( + Strings.format( + """ + { + "id" : "id", + "is_partial" : true, + "is_running" : true, + "start_time" : "%s", + "start_time_in_millis" : %s, + "expiration_time" : "%s", + "expiration_time_in_millis" : %s, + "response" : { + "took" : %s, + "timed_out" : false, + "num_reduce_phases" : 2, + "_shards" : { + "total" : 10, + "successful" : 9, + "skipped" : 1, + "failed" : 0 }, - "cluster_2" : { - "status" : "running", - "indices" : "foo,bar*", - "timed_out" : false + "_clusters" : { + "total" : 3, + "successful" : 0, + "skipped" : 0, + "running" : 3, + "partial" : 0, + "failed" : 0, + "details" : { + "cluster_1" : { + "status" : "running", + "indices" : "foo,bar*", + "timed_out" : false + }, + "cluster_2" : { + "status" : "running", + "indices" : "foo,bar*", + "timed_out" : false + }, + "cluster_0" : { + "status" : "running", + "indices" : "foo,bar*", + "timed_out" : false + } + } }, - "cluster_0" : { - "status" : "running", - "indices" : "foo,bar*", - "timed_out" : false + "hits" : { + "max_score" : 0.0, + "hits" : [ ] } } - }, - "hits" : { - "max_score" : 0.0, - "hits" : [ ] - } - } - }""", - XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(startTimeMillis)), - startTimeMillis, - XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expirationTimeMillis)), - expirationTimeMillis, - took - ), - Strings.toString(builder) - ); + }""", + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(startTimeMillis)), + startTimeMillis, + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expirationTimeMillis)), + expirationTimeMillis, + took + ), + Strings.toString(builder) + ); + } + } finally { + asyncSearchResponse.decRef(); } } @@ -566,15 +594,20 @@ public void testToXContentWithCCSSearchResponseAfterCompletion() throws IOExcept clusters ); - AsyncSearchResponse asyncSearchResponse = new AsyncSearchResponse( - "id", - searchResponse, - null, - false, - isRunning, - startTimeMillis, - expirationTimeMillis - ); + AsyncSearchResponse asyncSearchResponse; + try { + asyncSearchResponse = new AsyncSearchResponse( + "id", + searchResponse, + null, + false, + isRunning, + startTimeMillis, + expirationTimeMillis + ); + } finally { + searchResponse.decRef(); + } try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { builder.prettyPrint(); @@ -680,6 +713,8 @@ public void testToXContentWithCCSSearchResponseAfterCompletion() throws IOExcept } } }""", startTimeMillis, expirationTimeMillis, expectedCompletionTime, took), Strings.toString(builder)); + } finally { + asyncSearchResponse.decRef(); } } @@ -707,85 +742,92 @@ public void testToXContentWithSearchResponseWhileRunning() throws IOException { ShardSearchFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); - - AsyncSearchResponse asyncSearchResponse = new AsyncSearchResponse( - "id", - searchResponse, - null, - true, - isRunning, - startTimeMillis, - expirationTimeMillis - ); - - try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { - builder.prettyPrint(); - ChunkedToXContent.wrapAsToXContent(asyncSearchResponse).toXContent(builder, ToXContent.EMPTY_PARAMS); - assertEquals(Strings.format(""" - { - "id" : "id", - "is_partial" : true, - "is_running" : true, - "start_time_in_millis" : %s, - "expiration_time_in_millis" : %s, - "response" : { - "took" : %s, - "timed_out" : false, - "num_reduce_phases" : 2, - "_shards" : { - "total" : 10, - "successful" : 9, - "skipped" : 1, - "failed" : 0 - }, - "hits" : { - "max_score" : 0.0, - "hits" : [ ] - } - } - }""", startTimeMillis, expirationTimeMillis, took), Strings.toString(builder)); + AsyncSearchResponse asyncSearchResponse; + try { + asyncSearchResponse = new AsyncSearchResponse( + "id", + searchResponse, + null, + true, + isRunning, + startTimeMillis, + expirationTimeMillis + ); + } finally { + searchResponse.decRef(); } + try { + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + ChunkedToXContent.wrapAsToXContent(asyncSearchResponse).toXContent(builder, ToXContent.EMPTY_PARAMS); + assertEquals(Strings.format(""" + { + "id" : "id", + "is_partial" : true, + "is_running" : true, + "start_time_in_millis" : %s, + "expiration_time_in_millis" : %s, + "response" : { + "took" : %s, + "timed_out" : false, + "num_reduce_phases" : 2, + "_shards" : { + "total" : 10, + "successful" : 9, + "skipped" : 1, + "failed" : 0 + }, + "hits" : { + "max_score" : 0.0, + "hits" : [ ] + } + } + }""", startTimeMillis, expirationTimeMillis, took), Strings.toString(builder)); + } - try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { - builder.prettyPrint(); - builder.humanReadable(true); - ChunkedToXContent.wrapAsToXContent(asyncSearchResponse) - .toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("human", "true"))); - assertEquals( - Strings.format( - """ - { - "id" : "id", - "is_partial" : true, - "is_running" : true, - "start_time" : "%s", - "start_time_in_millis" : %s, - "expiration_time" : "%s", - "expiration_time_in_millis" : %s, - "response" : { - "took" : %s, - "timed_out" : false, - "num_reduce_phases" : 2, - "_shards" : { - "total" : 10, - "successful" : 9, - "skipped" : 1, - "failed" : 0 - }, - "hits" : { - "max_score" : 0.0, - "hits" : [ ] - } - } - }""", - XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(startTimeMillis)), - startTimeMillis, - XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expirationTimeMillis)), - expirationTimeMillis, - took - ), - Strings.toString(builder) - ); + try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { + builder.prettyPrint(); + builder.humanReadable(true); + ChunkedToXContent.wrapAsToXContent(asyncSearchResponse) + .toXContent(builder, new ToXContent.MapParams(Collections.singletonMap("human", "true"))); + assertEquals( + Strings.format( + """ + { + "id" : "id", + "is_partial" : true, + "is_running" : true, + "start_time" : "%s", + "start_time_in_millis" : %s, + "expiration_time" : "%s", + "expiration_time_in_millis" : %s, + "response" : { + "took" : %s, + "timed_out" : false, + "num_reduce_phases" : 2, + "_shards" : { + "total" : 10, + "successful" : 9, + "skipped" : 1, + "failed" : 0 + }, + "hits" : { + "max_score" : 0.0, + "hits" : [ ] + } + } + }""", + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(startTimeMillis)), + startTimeMillis, + XContentElasticsearchExtension.DEFAULT_FORMATTER.format(Instant.ofEpochMilli(expirationTimeMillis)), + expirationTimeMillis, + took + ), + Strings.toString(builder) + ); + } + } finally { + asyncSearchResponse.decRef(); } } diff --git a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncStatusResponseTests.java b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncStatusResponseTests.java index 2786d9772108a..6be128ac733b4 100644 --- a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncStatusResponseTests.java +++ b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/AsyncStatusResponseTests.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.search; -import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.common.Strings; @@ -27,7 +26,6 @@ import static org.elasticsearch.xpack.core.async.GetAsyncResultRequestTests.randomSearchId; -@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/104838") public class AsyncStatusResponseTests extends AbstractWireSerializingTestCase { @Override @@ -266,10 +264,13 @@ public void testToXContent() throws IOException { public void testGetStatusFromStoredSearchRandomizedInputs() { boolean ccs = randomBoolean(); String searchId = randomSearchId(); - AsyncSearchResponse asyncSearchResponse = AsyncSearchResponseTests.randomAsyncSearchResponse( - searchId, - AsyncSearchResponseTests.randomSearchResponse(ccs) - ); + SearchResponse searchResponse = AsyncSearchResponseTests.randomSearchResponse(ccs); + AsyncSearchResponse asyncSearchResponse; + try { + asyncSearchResponse = AsyncSearchResponseTests.randomAsyncSearchResponse(searchId, searchResponse); + } finally { + searchResponse.decRef(); + } try { if (asyncSearchResponse.getSearchResponse() == null && asyncSearchResponse.getFailure() == null @@ -339,8 +340,12 @@ public void testGetStatusFromStoredSearchFailedShardsScenario() { new ShardSearchFailure[] { new ShardSearchFailure(new RuntimeException("foo")) }, clusters ); - - AsyncSearchResponse asyncSearchResponse = new AsyncSearchResponse(searchId, searchResponse, null, false, false, 100, 200); + AsyncSearchResponse asyncSearchResponse; + try { + asyncSearchResponse = new AsyncSearchResponse(searchId, searchResponse, null, false, false, 100, 200); + } finally { + searchResponse.decRef(); + } try { AsyncStatusResponse statusFromStoredSearch = AsyncStatusResponse.getStatusFromStoredSearch(asyncSearchResponse, 100, searchId); assertNotNull(statusFromStoredSearch); @@ -368,8 +373,12 @@ public void testGetStatusFromStoredSearchWithEmptyClustersSuccessfullyCompleted( ShardSearchFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY ); - - AsyncSearchResponse asyncSearchResponse = new AsyncSearchResponse(searchId, searchResponse, null, false, false, 100, 200); + AsyncSearchResponse asyncSearchResponse; + try { + asyncSearchResponse = new AsyncSearchResponse(searchId, searchResponse, null, false, false, 100, 200); + } finally { + searchResponse.decRef(); + } try { AsyncStatusResponse statusFromStoredSearch = AsyncStatusResponse.getStatusFromStoredSearch(asyncSearchResponse, 100, searchId); assertNotNull(statusFromStoredSearch); @@ -415,8 +424,12 @@ public void testGetStatusFromStoredSearchWithNonEmptyClustersSuccessfullyComplet ShardSearchFailure.EMPTY_ARRAY, clusters ); - - AsyncSearchResponse asyncSearchResponse = new AsyncSearchResponse(searchId, searchResponse, null, false, false, 100, 200); + AsyncSearchResponse asyncSearchResponse; + try { + asyncSearchResponse = new AsyncSearchResponse(searchId, searchResponse, null, false, false, 100, 200); + } finally { + searchResponse.decRef(); + } try { AsyncStatusResponse statusFromStoredSearch = AsyncStatusResponse.getStatusFromStoredSearch(asyncSearchResponse, 100, searchId); assertNotNull(statusFromStoredSearch); @@ -464,9 +477,13 @@ public void testGetStatusFromStoredSearchWithNonEmptyClustersStillRunning() { ShardSearchFailure.EMPTY_ARRAY, clusters ); - boolean isRunning = true; - AsyncSearchResponse asyncSearchResponse = new AsyncSearchResponse(searchId, searchResponse, null, false, isRunning, 100, 200); + AsyncSearchResponse asyncSearchResponse; + try { + asyncSearchResponse = new AsyncSearchResponse(searchId, searchResponse, null, false, isRunning, 100, 200); + } finally { + searchResponse.decRef(); + } try { AsyncStatusResponse statusFromStoredSearch = AsyncStatusResponse.getStatusFromStoredSearch(asyncSearchResponse, 100, searchId); assertNotNull(statusFromStoredSearch); diff --git a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchActionTests.java b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchActionTests.java index 2c9708a930186..fe6ed8b57d1e0 100644 --- a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchActionTests.java +++ b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchActionTests.java @@ -6,7 +6,6 @@ */ package org.elasticsearch.xpack.search; -import org.apache.lucene.tests.util.LuceneTestCase; import org.apache.lucene.util.SetOnce; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; @@ -30,7 +29,6 @@ import static org.hamcrest.Matchers.instanceOf; import static org.mockito.Mockito.mock; -@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/104838") public class RestSubmitAsyncSearchActionTests extends RestActionTestCase { private RestSubmitAsyncSearchAction action; From 2d4a49af539818b7244f140d37745c13e0346f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Wed, 21 Feb 2024 15:06:32 +0100 Subject: [PATCH 114/250] [DOCS] Fixes get settings and update settings security API docs (#105686) * [DOCS] Fixes get settings and update settings security API docs. * [DOCS] Further edits. --- docs/reference/rest-api/security.asciidoc | 2 + .../rest-api/security/get-settings.asciidoc | 9 ++++- .../security/update-settings.asciidoc | 39 ++++++++++++------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/docs/reference/rest-api/security.asciidoc b/docs/reference/rest-api/security.asciidoc index 94b632490ad86..e5c42a93d34b1 100644 --- a/docs/reference/rest-api/security.asciidoc +++ b/docs/reference/rest-api/security.asciidoc @@ -12,6 +12,8 @@ Use the following APIs to perform security activities. * <> * <> * <> +* <> +* <> * <> [discrete] diff --git a/docs/reference/rest-api/security/get-settings.asciidoc b/docs/reference/rest-api/security/get-settings.asciidoc index 5c38b96903cbd..46e4e0cf529bb 100644 --- a/docs/reference/rest-api/security/get-settings.asciidoc +++ b/docs/reference/rest-api/security/get-settings.asciidoc @@ -5,6 +5,8 @@ Get Security settings ++++ +Retrieves settings for the security internal indices. + [[security-api-get-settings-prereqs]] ==== {api-prereq-title} @@ -14,11 +16,16 @@ ==== {api-description-title} This API allows a user to retrieve the user-configurable settings for the Security internal index (`.security` and associated indices). Only a subset of -the index settings — those that are user-configurable—will be shown. This includes: +the index settings — those that are user-configurable—will be shown. This +includes: - `index.auto_expand_replicas` - `index.number_of_replicas` + +[[security-api-get-settings-example]] +==== {api-examples-title} + An example of retrieving the security settings: [source,console] diff --git a/docs/reference/rest-api/security/update-settings.asciidoc b/docs/reference/rest-api/security/update-settings.asciidoc index 0ea41d86e85ed..652b722b0af48 100644 --- a/docs/reference/rest-api/security/update-settings.asciidoc +++ b/docs/reference/rest-api/security/update-settings.asciidoc @@ -5,11 +5,31 @@ Update Security settings ++++ +Updates the settings of the security internal indices. + + [[security-api-update-settings-prereqs]] ==== {api-prereq-title} * To use this API, you must have at least the `manage_security` cluster privilege. + +[[security-api-update-settings-request-body]] +==== {api-request-body-title} + +`security`:: +(Optional, object) Settings to be used for the index used for most security +configuration, including Native realm users and roles configured via the API. + +`security-tokens`:: +(Optional, object) Settings to be used for the index used to store +<>. + +`security`:: +(Optional, object) Settings to be used for the index used to store +<> information. + + [[security-api-update-settings-desc]] ==== {api-description-title} This API allows a user to modify the settings for the Security internal indices @@ -19,6 +39,10 @@ be modified. This includes: - `index.auto_expand_replicas` - `index.number_of_replicas` + +[[security-api-update-settings-example]] +==== {api-examples-title} + An example of modifying the Security settings: [source,console] @@ -43,18 +67,3 @@ The configured settings can be retrieved using the is not in use on the system, but settings are provided for it, the request will be rejected - this API does not yet support configuring the settings for these indices before they are in use. - - -==== {api-request-body-title} - -`security`:: -(Optional, object) Settings to be used for the index used for most security -configuration, including Native realm users and roles configured via the API. - -`security-tokens`:: -(Optional, object) Settings to be used for the index used to store -<>. - -`security`:: -(Optional, object) Settings to be used for the index used to store -<> information. From edf96a5212c07e2af74608b94fcc2a3781fe18a3 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Wed, 21 Feb 2024 09:28:20 -0500 Subject: [PATCH 115/250] Add RCS1.0 security test for the ResolveCluster API (#105524) --- ...teClusterSecurityRCS1ResolveClusterIT.java | 241 ++++++++++++++++++ ...eClusterSecurityRCS2ResolveClusterIT.java} | 19 +- 2 files changed, 246 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1ResolveClusterIT.java rename x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/{RemoteClusterSecurityResolveClusterIT.java => RemoteClusterSecurityRCS2ResolveClusterIT.java} (96%) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1ResolveClusterIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1ResolveClusterIT.java new file mode 100644 index 0000000000000..813739a8e0d06 --- /dev/null +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS1ResolveClusterIT.java @@ -0,0 +1,241 @@ +/* + * 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.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.core.Strings; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.junit.ClassRule; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; + +import java.io.IOException; +import java.util.Map; + +import static org.elasticsearch.action.search.SearchResponse.LOCAL_CLUSTER_NAME_REPRESENTATION; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +/** + * Tests the _resolve/cluster API under RCS1.0 security model + */ +public class RemoteClusterSecurityRCS1ResolveClusterIT extends AbstractRemoteClusterSecurityTestCase { + + static { + fulfillingCluster = ElasticsearchCluster.local().name("fulfilling-cluster").nodes(3).apply(commonClusterConfig).build(); + + queryCluster = ElasticsearchCluster.local().name("query-cluster").apply(commonClusterConfig).build(); + } + + @ClassRule + public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster); + + @SuppressWarnings("unchecked") + public void testResolveClusterUnderRCS1() throws Exception { + // Setup RCS 1.0 (basicSecurity=true) + configureRemoteCluster("my_remote_cluster", fulfillingCluster, true, randomBoolean(), randomBoolean()); + + { + // Query cluster -> add role for test user - do not give any privileges for remote_indices + var putRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); + putRoleRequest.setJsonEntity(""" + { + "indices": [ + { + "names": ["local_index"], + "privileges": ["read"] + } + ] + }"""); + assertOK(adminClient().performRequest(putRoleRequest)); + + // Query cluster -> create user and assign role + var putUserRequest = new Request("PUT", "/_security/user/" + REMOTE_SEARCH_USER); + putUserRequest.setJsonEntity(""" + { + "password": "x-pack-test-password", + "roles" : ["remote_search"] + }"""); + assertOK(adminClient().performRequest(putUserRequest)); + + // Query cluster -> create test index + var indexDocRequest = new Request("POST", "/local_index/_doc?refresh=true"); + indexDocRequest.setJsonEntity("{\"local_foo\": \"local_bar\"}"); + assertOK(client().performRequest(indexDocRequest)); + + // Fulfilling cluster -> create test indices + Request bulkRequest = new Request("POST", "/_bulk?refresh=true"); + bulkRequest.setJsonEntity(Strings.format(""" + { "index": { "_index": "index1" } } + { "foo": "bar" } + { "index": { "_index": "secretindex" } } + { "bar": "foo" } + """)); + assertOK(performRequestAgainstFulfillingCluster(bulkRequest)); + } + { + // TEST CASE 1: Query cluster -> try to resolve local and remote star patterns (no access to remote cluster) + Request starResolveRequest = new Request("GET", "_resolve/cluster/*,my_remote_cluster:*"); + Response response = performRequestWithRemoteSearchUser(starResolveRequest); + assertOK(response); + Map responseMap = responseAsMap(response); + assertLocalMatching(responseMap); + + Map remoteClusterResponse = (Map) responseMap.get("my_remote_cluster"); + assertThat((Boolean) remoteClusterResponse.get("connected"), equalTo(true)); + assertThat((String) remoteClusterResponse.get("error"), containsString("unauthorized for user [remote_search_user]")); + + // TEST CASE 2: Query cluster -> add user role and user on remote cluster and try resolve again + var putRoleOnRemoteClusterRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); + putRoleOnRemoteClusterRequest.setJsonEntity(""" + { + "indices": [ + { + "names": ["index*"], + "privileges": ["read", "read_cross_cluster"] + } + ] + }"""); + assertOK(performRequestAgainstFulfillingCluster(putRoleOnRemoteClusterRequest)); + + var putUserOnRemoteClusterRequest = new Request("PUT", "/_security/user/" + REMOTE_SEARCH_USER); + putUserOnRemoteClusterRequest.setJsonEntity(""" + { + "password": "x-pack-test-password", + "roles" : ["remote_search"] + }"""); + assertOK(performRequestAgainstFulfillingCluster(putUserOnRemoteClusterRequest)); + + // Query cluster -> resolve local and remote with proper access + response = performRequestWithRemoteSearchUser(starResolveRequest); + assertOK(response); + responseMap = responseAsMap(response); + assertLocalMatching(responseMap); + assertRemoteMatching(responseMap); + } + { + // TEST CASE 3: Query cluster -> resolve index1 for local index without any local privilege + Request localOnly1 = new Request("GET", "_resolve/cluster/index1"); + ResponseException exc = expectThrows(ResponseException.class, () -> performRequestWithRemoteSearchUser(localOnly1)); + assertThat(exc.getResponse().getStatusLine().getStatusCode(), is(403)); + assertThat( + exc.getMessage(), + containsString( + "action [indices:admin/resolve/cluster] is unauthorized for user " + + "[remote_search_user] with effective roles [remote_search] on indices [index1]" + ) + ); + } + { + // TEST CASE 4: Query cluster -> resolve local for local index without any local privilege using wildcard + Request localOnlyWildcard1 = new Request("GET", "_resolve/cluster/index1*"); + Response response = performRequestWithRemoteSearchUser(localOnlyWildcard1); + assertOK(response); + Map responseMap = responseAsMap(response); + assertMatching((Map) responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), false); + } + { + // TEST CASE 5: Query cluster -> resolve remote and local without permission where using wildcard 'index1*' + Request localNoPermsRemoteWithPerms = new Request("GET", "_resolve/cluster/index1*,my_remote_cluster:index1"); + Response response = performRequestWithRemoteSearchUser(localNoPermsRemoteWithPerms); + assertOK(response); + Map responseMap = responseAsMap(response); + assertMatching((Map) responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), false); + assertRemoteMatching(responseMap); + } + { + // TEST CASE 6: Query cluster -> resolve remote only for existing and privileged index + Request remoteOnly1 = new Request("GET", "_resolve/cluster/my_remote_cluster:index1"); + Response response = performRequestWithRemoteSearchUser(remoteOnly1); + assertOK(response); + Map responseMap = responseAsMap(response); + assertThat(responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), nullValue()); + assertRemoteMatching(responseMap); + } + { + // TEST CASE 7: Query cluster -> resolve remote only for existing but non-privileged index + Request remoteOnly2 = new Request("GET", "_resolve/cluster/my_remote_cluster:secretindex"); + Response response = performRequestWithRemoteSearchUser(remoteOnly2); + assertOK(response); + Map responseMap = responseAsMap(response); + assertThat(responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), nullValue()); + Map remoteClusterResponse = (Map) responseMap.get("my_remote_cluster"); + assertThat((Boolean) remoteClusterResponse.get("connected"), equalTo(true)); + assertThat((String) remoteClusterResponse.get("error"), containsString("unauthorized for user [remote_search_user]")); + assertThat((String) remoteClusterResponse.get("error"), containsString("on indices [secretindex]")); + } + { + // TEST CASE 8: Query cluster -> resolve remote only for non-existing and non-privileged index + Request remoteOnly3 = new Request("GET", "_resolve/cluster/my_remote_cluster:doesnotexist"); + Response response = performRequestWithRemoteSearchUser(remoteOnly3); + assertOK(response); + Map responseMap = responseAsMap(response); + assertThat(responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), nullValue()); + Map remoteClusterResponse = (Map) responseMap.get("my_remote_cluster"); + assertThat((Boolean) remoteClusterResponse.get("connected"), equalTo(true)); + assertThat((String) remoteClusterResponse.get("error"), containsString("unauthorized for user [remote_search_user]")); + assertThat((String) remoteClusterResponse.get("error"), containsString("on indices [doesnotexist]")); + } + { + // TEST CASE 9: Query cluster -> resolve remote only for non-existing but privileged (by index pattern) index + Request remoteOnly4 = new Request("GET", "_resolve/cluster/my_remote_cluster:index99"); + Response response = performRequestWithRemoteSearchUser(remoteOnly4); + assertOK(response); + Map responseMap = responseAsMap(response); + assertThat(responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), nullValue()); + Map remoteClusterResponse = (Map) responseMap.get("my_remote_cluster"); + assertThat((Boolean) remoteClusterResponse.get("connected"), equalTo(true)); + assertThat((String) remoteClusterResponse.get("error"), containsString("no such index [index99]")); + } + { + // TEST CASE 10: Query cluster -> resolve remote only for some existing/privileged, + // non-existing/privileged, existing/non-privileged + Request remoteOnly5 = new Request( + "GET", + "_resolve/cluster/my_remote_cluster:index1,my_remote_cluster:secretindex,my_remote_cluster:index99" + ); + Response response = performRequestWithRemoteSearchUser(remoteOnly5); + assertOK(response); + Map responseMap = responseAsMap(response); + assertThat(responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), nullValue()); + Map remoteClusterResponse = (Map) responseMap.get("my_remote_cluster"); + assertThat((Boolean) remoteClusterResponse.get("connected"), equalTo(true)); + assertThat((String) remoteClusterResponse.get("error"), containsString("unauthorized for user [remote_search_user]")); + assertThat((String) remoteClusterResponse.get("error"), containsString("on indices [secretindex]")); + } + } + + private Response performRequestWithRemoteSearchUser(final Request request) throws IOException { + request.setOptions( + RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", headerFromRandomAuthMethod(REMOTE_SEARCH_USER, PASS)) + ); + return client().performRequest(request); + } + + @SuppressWarnings("unchecked") + private void assertLocalMatching(Map responseMap) { + assertMatching((Map) responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), true); + } + + @SuppressWarnings("unchecked") + private void assertRemoteMatching(Map responseMap) { + assertMatching((Map) responseMap.get("my_remote_cluster"), true); + } + + private void assertMatching(Map perClusterResponse, boolean matching) { + assertThat((Boolean) perClusterResponse.get("connected"), equalTo(true)); + assertThat((Boolean) perClusterResponse.get("matching_indices"), equalTo(matching)); + assertThat(perClusterResponse.get("version"), notNullValue()); + } +} diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityResolveClusterIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2ResolveClusterIT.java similarity index 96% rename from x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityResolveClusterIT.java rename to x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2ResolveClusterIT.java index da6d930371bc9..a3bc56dafce98 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityResolveClusterIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2ResolveClusterIT.java @@ -36,7 +36,10 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; -public class RemoteClusterSecurityResolveClusterIT extends AbstractRemoteClusterSecurityTestCase { +/** + * Tests the _resolve/cluster API under RCS2.0 security model + */ +public class RemoteClusterSecurityRCS2ResolveClusterIT extends AbstractRemoteClusterSecurityTestCase { private static final AtomicReference> API_KEY_MAP_REF = new AtomicReference<>(); private static final AtomicReference> REST_API_KEY_MAP_REF = new AtomicReference<>(); @@ -168,7 +171,6 @@ public void testResolveCluster() throws Exception { """)); assertOK(performRequestAgainstFulfillingCluster(bulkRequest)); } - { // TEST CASE 1: Query cluster -> try to resolve local and remote star patterns (no access to remote cluster) final Request starResolveRequest = new Request("GET", "_resolve/cluster/*,my_remote_cluster:*"); @@ -212,9 +214,8 @@ public void testResolveCluster() throws Exception { assertLocalMatching(responseMap); assertRemoteMatching(responseMap); } - - // TEST CASE 3: Query cluster -> resolve index1 for local index without any local privilege { + // TEST CASE 3: Query cluster -> resolve index1 for local index without any local privilege final Request localOnly1 = new Request("GET", "_resolve/cluster/index1"); ResponseException exc = expectThrows(ResponseException.class, () -> performRequestWithRemoteSearchUser(localOnly1)); assertThat(exc.getResponse().getStatusLine().getStatusCode(), is(403)); @@ -226,7 +227,6 @@ public void testResolveCluster() throws Exception { ) ); } - { // TEST CASE 4: Query cluster -> resolve local for local index without any local privilege using wildcard final Request localOnlyWildcard1 = new Request("GET", "_resolve/cluster/index1*"); @@ -235,7 +235,6 @@ public void testResolveCluster() throws Exception { Map responseMap = responseAsMap(response); assertMatching((Map) responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), false); } - { // TEST CASE 5: Query cluster -> resolve remote and local without permission where using wildcard 'index1*' final Request localNoPermsRemoteWithPerms = new Request("GET", "_resolve/cluster/index1*,my_remote_cluster:index1"); @@ -245,7 +244,6 @@ public void testResolveCluster() throws Exception { assertMatching((Map) responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), false); assertRemoteMatching(responseMap); } - { // TEST CASE 6: Query cluster -> resolve remote only for existing and privileged index final Request remoteOnly1 = new Request("GET", "_resolve/cluster/my_remote_cluster:index1"); @@ -255,7 +253,6 @@ public void testResolveCluster() throws Exception { assertThat(responseMap.get(LOCAL_CLUSTER_NAME_REPRESENTATION), nullValue()); assertRemoteMatching(responseMap); } - { // TEST CASE 7: Query cluster -> resolve remote only for existing but non-privileged index final Request remoteOnly2 = new Request("GET", "_resolve/cluster/my_remote_cluster:secretindex"); @@ -266,10 +263,8 @@ public void testResolveCluster() throws Exception { Map remoteClusterResponse = (Map) responseMap.get("my_remote_cluster"); assertThat((Boolean) remoteClusterResponse.get("connected"), equalTo(true)); assertThat((String) remoteClusterResponse.get("error"), containsString("is unauthorized for user")); - assertThat((String) remoteClusterResponse.get("error"), containsString("with assigned roles [remote_search]")); assertThat((String) remoteClusterResponse.get("error"), containsString("on indices [secretindex]")); } - { // TEST CASE 8: Query cluster -> resolve remote only for non-existing and non-privileged index final Request remoteOnly3 = new Request("GET", "_resolve/cluster/my_remote_cluster:doesnotexist"); @@ -280,10 +275,8 @@ public void testResolveCluster() throws Exception { Map remoteClusterResponse = (Map) responseMap.get("my_remote_cluster"); assertThat((Boolean) remoteClusterResponse.get("connected"), equalTo(true)); assertThat((String) remoteClusterResponse.get("error"), containsString("is unauthorized for user")); - assertThat((String) remoteClusterResponse.get("error"), containsString("with assigned roles [remote_search]")); assertThat((String) remoteClusterResponse.get("error"), containsString("on indices [doesnotexist]")); } - { // TEST CASE 9: Query cluster -> resolve remote only for non-existing but privileged (by index pattern) index final Request remoteOnly4 = new Request("GET", "_resolve/cluster/my_remote_cluster:index99"); @@ -296,7 +289,6 @@ public void testResolveCluster() throws Exception { assertThat((Boolean) remoteClusterResponse.get("skip_unavailable"), equalTo(false)); assertThat((String) remoteClusterResponse.get("error"), containsString("no such index [index99]")); } - { // TEST CASE 10: Query cluster -> resolve remote only for some existing/privileged, // non-existing/privileged, existing/non-privileged @@ -311,7 +303,6 @@ public void testResolveCluster() throws Exception { Map remoteClusterResponse = (Map) responseMap.get("my_remote_cluster"); assertThat((Boolean) remoteClusterResponse.get("connected"), equalTo(true)); assertThat((String) remoteClusterResponse.get("error"), containsString("is unauthorized for user")); - assertThat((String) remoteClusterResponse.get("error"), containsString("with assigned roles [remote_search]")); assertThat((String) remoteClusterResponse.get("error"), containsString("on indices [secretindex]")); } } From 5f508a1d16ddd1d9c3a80bd120873c8e0d704ab3 Mon Sep 17 00:00:00 2001 From: Kathleen DeRusso Date: Wed, 21 Feb 2024 10:02:56 -0500 Subject: [PATCH 116/250] Fix transport bug handling errors in rule query (#105667) --- .../test/entsearch/260_rule_query_search.yml | 38 +++++++++++++++ .../application/rules/RuleQueryBuilder.java | 47 ++++++------------- 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/260_rule_query_search.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/260_rule_query_search.yml index f67c955126235..edd9d7c2e140d 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/260_rule_query_search.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/260_rule_query_search.yml @@ -69,6 +69,44 @@ setup: - 'doc3' +--- +"Perform a rule query specifying a ruleset that does not exist": + - skip: + version: " - 8.12.99" + reason: Bugfix that was broken in previous versions + + - do: + catch: /resource_not_found_exception/ + search: + body: + query: + rule_query: + organic: + query_string: + default_field: text + query: search + match_criteria: + foo: bar + ruleset_id: nonexistent-ruleset + +--- +"Perform a rule query with malformed rule": + - skip: + version: " - 8.12.99" + reason: Bugfix that was broken in previous versions + + - do: + catch: bad_request + search: + body: + query: + rule_query: + organic: + query_string: + default_field: text + query: search + ruleset_id: test-ruleset + --- "Perform a rule query with an ID match": diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/RuleQueryBuilder.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/RuleQueryBuilder.java index 11d2945a97354..bc45b24027e0e 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/RuleQueryBuilder.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/rules/RuleQueryBuilder.java @@ -8,21 +8,17 @@ import org.apache.lucene.search.Query; import org.apache.lucene.util.SetOnce; -import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.get.GetRequest; -import org.elasticsearch.action.get.GetResponse; -import org.elasticsearch.client.internal.Client; -import org.elasticsearch.client.internal.OriginSettingClient; +import org.elasticsearch.action.get.TransportGetAction; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.logging.HeaderWarning; -import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.query.AbstractQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; @@ -45,6 +41,7 @@ import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; import static org.elasticsearch.xpack.core.ClientHelper.ENT_SEARCH_ORIGIN; +import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; import static org.elasticsearch.xpack.searchbusinessrules.PinnedQueryBuilder.MAX_NUM_PINNED_HITS; /** @@ -207,36 +204,22 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) { AppliedQueryRules appliedRules = new AppliedQueryRules(); queryRewriteContext.registerAsyncAction((client, listener) -> { - Client clientWithOrigin = new OriginSettingClient(client, ENT_SEARCH_ORIGIN); - clientWithOrigin.get(getRequest, new ActionListener<>() { - @Override - public void onResponse(GetResponse getResponse) { - if (getResponse.isExists() == false) { - throw new ResourceNotFoundException("query ruleset " + rulesetId + " not found"); - } - QueryRuleset queryRuleset = QueryRuleset.fromXContentBytes( - rulesetId, - getResponse.getSourceAsBytesRef(), - XContentType.JSON - ); - for (QueryRule rule : queryRuleset.rules()) { - rule.applyRule(appliedRules, matchCriteria); - } - pinnedIdsSetOnce.set(appliedRules.pinnedIds().stream().distinct().toList()); - pinnedDocsSetOnce.set(appliedRules.pinnedDocs().stream().distinct().toList()); - listener.onResponse(null); + executeAsyncWithOrigin(client, ENT_SEARCH_ORIGIN, TransportGetAction.TYPE, getRequest, ActionListener.wrap(getResponse -> { + + if (getResponse.isExists() == false) { + listener.onFailure(new ResourceNotFoundException("query ruleset " + rulesetId + " not found")); + return; } - @Override - public void onFailure(Exception e) { - Throwable cause = ExceptionsHelper.unwrapCause(e); - if (cause instanceof IndexNotFoundException) { - listener.onFailure(new ResourceNotFoundException("query ruleset " + rulesetId + " not found")); - } else { - listener.onFailure(e); - } + QueryRuleset queryRuleset = QueryRuleset.fromXContentBytes(rulesetId, getResponse.getSourceAsBytesRef(), XContentType.JSON); + for (QueryRule rule : queryRuleset.rules()) { + rule.applyRule(appliedRules, matchCriteria); } - }); + pinnedIdsSetOnce.set(appliedRules.pinnedIds().stream().distinct().toList()); + pinnedDocsSetOnce.set(appliedRules.pinnedDocs().stream().distinct().toList()); + listener.onResponse(null); + + }, listener::onFailure)); }); return new RuleQueryBuilder(organicQuery, matchCriteria, this.rulesetId, null, null, pinnedIdsSetOnce::get, pinnedDocsSetOnce::get) From 86f4b1819412d6862c1aa5b874707cc90c8460b5 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Wed, 21 Feb 2024 16:07:05 +0100 Subject: [PATCH 117/250] Reduce InternalGeoGrid in a streaming fashion (#105651) --- .../bucket/geogrid/InternalGeoGrid.java | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoGrid.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoGrid.java index c040e50da1aa6..bbf92cbf679d0 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoGrid.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/geogrid/InternalGeoGrid.java @@ -17,11 +17,11 @@ import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; +import org.elasticsearch.search.aggregations.bucket.MultiBucketAggregatorsReducer; import org.elasticsearch.search.aggregations.support.SamplingContext; import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -81,34 +81,35 @@ public List getBuckets() { protected AggregatorReducer getLeaderReducer(AggregationReduceContext reduceContext, int size) { return new AggregatorReducer() { - LongObjectPagedHashMap> buckets = null; + final LongObjectPagedHashMap bucketsReducer = new LongObjectPagedHashMap<>( + size, + reduceContext.bigArrays() + ); @Override public void accept(InternalAggregation aggregation) { @SuppressWarnings("unchecked") - InternalGeoGrid grid = (InternalGeoGrid) aggregation; - if (buckets == null) { - buckets = new LongObjectPagedHashMap<>(grid.buckets.size(), reduceContext.bigArrays()); - } - for (InternalGeoGridBucket bucket : grid.buckets) { - List existingBuckets = buckets.get(bucket.hashAsLong()); - if (existingBuckets == null) { - existingBuckets = new ArrayList<>(size); - buckets.put(bucket.hashAsLong(), existingBuckets); + final InternalGeoGrid grid = (InternalGeoGrid) aggregation; + for (InternalGeoGridBucket bucket : grid.getBuckets()) { + MultiBucketAggregatorsReducer reducer = bucketsReducer.get(bucket.hashAsLong()); + if (reducer == null) { + reducer = new MultiBucketAggregatorsReducer(reduceContext, size); + bucketsReducer.put(bucket.hashAsLong(), reducer); } - existingBuckets.add(bucket); + reducer.accept(bucket); } } @Override public InternalAggregation get() { final int size = Math.toIntExact( - reduceContext.isFinalReduce() == false ? buckets.size() : Math.min(requiredSize, buckets.size()) + reduceContext.isFinalReduce() == false ? bucketsReducer.size() : Math.min(requiredSize, bucketsReducer.size()) ); final BucketPriorityQueue ordered = new BucketPriorityQueue<>(size); - for (LongObjectPagedHashMap.Cursor> cursor : buckets) { - ordered.insertWithOverflow(reduceBucket(cursor.value, reduceContext)); - } + bucketsReducer.iterator().forEachRemaining(entry -> { + InternalGeoGridBucket bucket = createBucket(entry.key, entry.value.getDocCount(), entry.value.get()); + ordered.insertWithOverflow(bucket); + }); final InternalGeoGridBucket[] list = new InternalGeoGridBucket[ordered.size()]; for (int i = ordered.size() - 1; i >= 0; i--) { list[i] = ordered.pop(); @@ -119,7 +120,8 @@ public InternalAggregation get() { @Override public void close() { - Releasables.close(buckets); + bucketsReducer.iterator().forEachRemaining(r -> Releasables.close(r.value)); + Releasables.close(bucketsReducer); } }; } @@ -142,17 +144,6 @@ public InternalAggregation finalizeSampling(SamplingContext samplingContext) { ); } - private InternalGeoGridBucket reduceBucket(List buckets, AggregationReduceContext context) { - assert buckets.isEmpty() == false; - long docCount = 0; - for (InternalGeoGridBucket bucket : buckets) { - docCount += bucket.docCount; - } - final List aggregations = new BucketAggregationList<>(buckets); - final InternalAggregations aggs = InternalAggregations.reduce(aggregations, context); - return createBucket(buckets.get(0).hashAsLong, docCount, aggs); - } - protected abstract B createBucket(long hashAsLong, long docCount, InternalAggregations aggregations); @Override From 0da52203723cb75755320acd83fc0aeee8e3834d Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Wed, 21 Feb 2024 16:17:34 +0100 Subject: [PATCH 118/250] Call real memory circuit breaker in BucketsAggregator#collectBucket instead of BucketsAggregator#collectExistingBucket (#105668) --- .../aggregations/bucket/BucketsAggregator.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java index fc2e7f04f2c59..b55d98685ab54 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/BucketsAggregator.java @@ -78,13 +78,6 @@ public final void grow(long maxBucketOrd) { */ public final void collectBucket(LeafBucketCollector subCollector, int doc, long bucketOrd) throws IOException { grow(bucketOrd + 1); - collectExistingBucket(subCollector, doc, bucketOrd); - } - - /** - * Same as {@link #collectBucket(LeafBucketCollector, int, long)}, but doesn't check if the docCounts needs to be re-sized. - */ - public final void collectExistingBucket(LeafBucketCollector subCollector, int doc, long bucketOrd) throws IOException { int docCount = docCountProvider.getDocCount(doc); if (docCounts.increment(bucketOrd, docCount) == docCount) { // We call the circuit breaker the time to time in order to give it a chance to check available @@ -97,6 +90,14 @@ public final void collectExistingBucket(LeafBucketCollector subCollector, int do subCollector.collect(doc, bucketOrd); } + /** + * Same as {@link #collectBucket(LeafBucketCollector, int, long)}, but doesn't check if the docCounts needs to be re-sized. + */ + public final void collectExistingBucket(LeafBucketCollector subCollector, int doc, long bucketOrd) throws IOException { + docCounts.increment(bucketOrd, docCountProvider.getDocCount(doc)); + subCollector.collect(doc, bucketOrd); + } + /** * Merge doc counts. If the {@linkplain Aggregator} is delayed then you must also call * {@link BestBucketsDeferringCollector#rewriteBuckets(LongUnaryOperator)} to merge the delayed buckets. From 280fa408bedb2243670669e1d8e999a44e9dfa51 Mon Sep 17 00:00:00 2001 From: Przemyslaw Gomulka Date: Wed, 21 Feb 2024 17:23:10 +0100 Subject: [PATCH 119/250] Improve EarlyDeprecationindexingIT test reliability (#105696) this test intends to test the bulkProcessor2's request consumer (see DeprecationIndexingComponent#getBulkProcessor) scheduling requests before the startup is completed (flush is enabled). To verify this behaviour the flush has to happen before the templates are loaded. To test this reliably the flush interval in the test should be as small as possible (not hardcoded 5s as of now) This commit introduces a setting (not meant to be exposed/documented) to allow for the flush interval to be configured. It also adds additional trace logging to help with troubleshooting. relates #104716 --- .../qa/early-deprecation-rest/build.gradle | 5 ++++- .../xpack/deprecation/Deprecation.java | 8 +++++++- .../logging/DeprecationIndexingAppender.java | 19 ++++++++++++++++--- .../logging/DeprecationIndexingComponent.java | 14 +++++++++++++- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/deprecation/qa/early-deprecation-rest/build.gradle b/x-pack/plugin/deprecation/qa/early-deprecation-rest/build.gradle index ab09c31d6f80c..2d8859bdcea3d 100644 --- a/x-pack/plugin/deprecation/qa/early-deprecation-rest/build.gradle +++ b/x-pack/plugin/deprecation/qa/early-deprecation-rest/build.gradle @@ -27,9 +27,12 @@ restResources { testClusters.configureEach { testDistribution = 'DEFAULT' - setting 'cluster.deprecation_indexing.enabled', 'true' setting 'xpack.security.enabled', 'false' setting 'xpack.license.self_generated.type', 'trial' + setting 'cluster.deprecation_indexing.enabled', 'true' + setting 'cluster.deprecation_indexing.flush_interval', '1ms' + setting 'logger.org.elasticsearch.xpack.deprecation','TRACE' + setting 'logger.org.elasticsearch.xpack.deprecation.logging','TRACE' } // Test clusters run with security disabled diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/Deprecation.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/Deprecation.java index 4e2c9da25e78b..85b7c89e7cb85 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/Deprecation.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/Deprecation.java @@ -34,6 +34,7 @@ import java.util.function.Supplier; import static org.elasticsearch.xpack.deprecation.DeprecationChecks.SKIP_DEPRECATIONS_SETTING; +import static org.elasticsearch.xpack.deprecation.logging.DeprecationIndexingComponent.DEPRECATION_INDEXING_FLUSH_INTERVAL; /** * The plugin class for the Deprecation API @@ -110,6 +111,11 @@ public Collection createComponents(PluginServices services) { @Override public List> getSettings() { - return List.of(USE_X_OPAQUE_ID_IN_FILTERING, WRITE_DEPRECATION_LOGS_TO_INDEX, SKIP_DEPRECATIONS_SETTING); + return List.of( + USE_X_OPAQUE_ID_IN_FILTERING, + WRITE_DEPRECATION_LOGS_TO_INDEX, + SKIP_DEPRECATIONS_SETTING, + DEPRECATION_INDEXING_FLUSH_INTERVAL + ); } } diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingAppender.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingAppender.java index edd9a85862b01..22637b1640b51 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingAppender.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingAppender.java @@ -7,6 +7,8 @@ package org.elasticsearch.xpack.deprecation.logging; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.Core; import org.apache.logging.log4j.core.Filter; @@ -16,6 +18,7 @@ import org.apache.logging.log4j.core.config.plugins.Plugin; import org.elasticsearch.action.DocWriteRequest; import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.core.Strings; import org.elasticsearch.xcontent.XContentType; import java.util.Objects; @@ -28,6 +31,7 @@ */ @Plugin(name = "DeprecationIndexingAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) public class DeprecationIndexingAppender extends AbstractAppender { + private static final Logger logger = LogManager.getLogger(DeprecationIndexingAppender.class); public static final String DEPRECATION_MESSAGES_DATA_STREAM = ".logs-deprecation.elasticsearch-default"; private final Consumer requestConsumer; @@ -40,9 +44,10 @@ public class DeprecationIndexingAppender extends AbstractAppender { /** * Creates a new appender. - * @param name the appender's name - * @param filter a filter to apply directly on the appender - * @param layout the layout to use for formatting message. It must return a JSON string. + * + * @param name the appender's name + * @param filter a filter to apply directly on the appender + * @param layout the layout to use for formatting message. It must return a JSON string. * @param requestConsumer a callback to handle the actual indexing of the log message. */ public DeprecationIndexingAppender(String name, Filter filter, Layout layout, Consumer requestConsumer) { @@ -56,6 +61,13 @@ public DeprecationIndexingAppender(String name, Filter filter, Layout la */ @Override public void append(LogEvent event) { + logger.trace( + () -> Strings.format( + "Received deprecation log event. Appender is %s. message = %s", + isEnabled ? "enabled" : "disabled", + event.getMessage().getFormattedMessage() + ) + ); if (this.isEnabled == false) { return; } @@ -71,6 +83,7 @@ public void append(LogEvent event) { /** * Sets whether this appender is enabled or disabled. When disabled, the appender will * not perform indexing operations. + * * @param enabled the enabled status of the appender. */ public void setEnabled(boolean enabled) { diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingComponent.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingComponent.java index 6a59a6832c91f..29041b0c58434 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingComponent.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingComponent.java @@ -27,6 +27,7 @@ import org.elasticsearch.common.logging.ECSJsonLayout; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.logging.RateLimitingFilter; +import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.TimeValue; import org.elasticsearch.xpack.core.ClientHelper; @@ -46,6 +47,13 @@ * It also starts and stops the appender */ public class DeprecationIndexingComponent extends AbstractLifecycleComponent implements ClusterStateListener { + + public static final Setting DEPRECATION_INDEXING_FLUSH_INTERVAL = Setting.timeSetting( + "cluster.deprecation_indexing.flush_interval", + TimeValue.timeValueSeconds(5), + Setting.Property.NodeScope + ); + private static final Logger logger = LogManager.getLogger(DeprecationIndexingComponent.class); private final DeprecationIndexingAppender appender; @@ -190,6 +198,7 @@ public void enableDeprecationLogIndexing(boolean newEnabled) { * @return an initialised bulk processor */ private BulkProcessor2 getBulkProcessor(Client client, Settings settings) { + TimeValue flushInterval = DEPRECATION_INDEXING_FLUSH_INTERVAL.get(settings); BulkProcessor2.Listener listener = new DeprecationBulkListener(); return BulkProcessor2.builder((bulkRequest, actionListener) -> { /* @@ -198,13 +207,16 @@ private BulkProcessor2 getBulkProcessor(Client client, Settings settings) { * in-flight-bytes limit has been exceeded. This means that we don't have to worry about bounding pendingRequestsBuffer. */ if (flushEnabled.get()) { + logger.trace("Flush is enabled, sending a bulk request"); client.bulk(bulkRequest, actionListener); flushBuffer(); // just in case something was missed after the first flush } else { + logger.trace("Flush is disabled, scheduling a bulk request"); + // this is an unbounded queue, so the entry will always be accepted pendingRequestsBuffer.offer(() -> client.bulk(bulkRequest, actionListener)); } - }, listener, client.threadPool()).setMaxNumberOfRetries(3).setFlushInterval(TimeValue.timeValueSeconds(5)).build(); + }, listener, client.threadPool()).setMaxNumberOfRetries(3).setFlushInterval(flushInterval).build(); } private static class DeprecationBulkListener implements BulkProcessor2.Listener { From 6b50b6ddf9c0f0401dc97781eaf5e167e2e0f543 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Wed, 21 Feb 2024 17:45:51 +0100 Subject: [PATCH 120/250] Block updates to log level for restricted loggers if less specific than INFO (#105020) To prevent leaking sensitive information such as credentials and keys in logs, this commit prevents configuring some restricted loggers (currently `org.apache.http` and `com.amazonaws.request`) at high verbosity unless the NetworkTraceFlag (`es.insecure_network_trace_enabled`) is enabled. --- docs/reference/setup/logging-config.asciidoc | 6 +- .../snapshot-restore/repository-s3.asciidoc | 15 ++- .../logging/EvilLoggerConfigurationTests.java | 9 +- qa/restricted-loggers/build.gradle | 18 +++ .../common/logging/LoggersTests.java | 110 +++++++++++++++++ server/build.gradle | 2 +- .../ClusterUpdateSettingsRequest.java | 8 ++ .../elasticsearch/common/logging/Loggers.java | 112 ++++++++++++++++-- .../common/logging/LoggersTests.java | 74 ++++++++++++ x-pack/plugin/security/build.gradle | 4 + .../oidc/OpenIdConnectAuthenticatorTests.java | 1 + .../testkit/S3SnapshotRepoTestKitIT.java | 2 + 12 files changed, 343 insertions(+), 18 deletions(-) create mode 100644 qa/restricted-loggers/build.gradle create mode 100644 qa/restricted-loggers/src/test/java/org/elasticsearch/common/logging/LoggersTests.java diff --git a/docs/reference/setup/logging-config.asciidoc b/docs/reference/setup/logging-config.asciidoc index 0ce2b8f1bfb59..69fa086d67673 100644 --- a/docs/reference/setup/logging-config.asciidoc +++ b/docs/reference/setup/logging-config.asciidoc @@ -150,7 +150,9 @@ update settings API>> to change the related logger's log level. Each logger accepts Log4j 2's built-in log levels, from least to most verbose: `OFF`, `FATAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`, and `TRACE`. The default log level is `INFO`. Messages logged at higher verbosity levels (`DEBUG` and `TRACE`) are -only intended for expert use. +only intended for expert use. To prevent leaking sensitive information in logs, +{es} will reject setting certain loggers to higher verbosity levels unless +<> is enabled. [source,console] ---- @@ -227,7 +229,7 @@ to `OFF` in `log4j2.properties` : ---- logger.deprecation.level = OFF ---- -Alternatively, you can change the logging level dynamically: +Alternatively, you can change the logging level dynamically: [source,console] ---- diff --git a/docs/reference/snapshot-restore/repository-s3.asciidoc b/docs/reference/snapshot-restore/repository-s3.asciidoc index b01f7322f9834..0c79793ee6c5a 100644 --- a/docs/reference/snapshot-restore/repository-s3.asciidoc +++ b/docs/reference/snapshot-restore/repository-s3.asciidoc @@ -564,7 +564,15 @@ is usually simplest to collect these logs and provide them to the supplier of your storage system for further analysis. If the incompatibility is not clear from the logs emitted by the storage system, configure {es} to log every request it makes to the S3 API by <> of the `com.amazonaws.request` logger to `DEBUG`: +logging level>> of the `com.amazonaws.request` logger to `DEBUG`. + +To prevent leaking sensitive information such as credentials and keys in logs, +{es} rejects configuring this logger at high verbosity unless +<> is enabled. +To do so, you must explicitly enable it on each node by setting the system +property `es.insecure_network_trace_enabled` to `true`. + +Once enabled, you can configure the `com.amazonaws.request` logger: [source,console] ---- @@ -585,8 +593,9 @@ https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/java-dg-logging.html documentation for further information, including details about other loggers that can be used to obtain even more verbose logs. When you have finished collecting the logs needed by your supplier, set the logger settings back to -`null` to return to the default logging configuration. See <> -and <> for more information. +`null` to return to the default logging configuration and disable insecure network +trace logging again. See <> and <> for +more information. [[repository-s3-linearizable-registers]] ==== Linearizable register implementation diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerConfigurationTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerConfigurationTests.java index c0b52c80d89a9..db531026dbad5 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerConfigurationTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerConfigurationTests.java @@ -26,6 +26,7 @@ import java.util.Map; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.notNullValue; @@ -144,7 +145,13 @@ public void testLoggingLevelsFromSettings() throws IOException, UserException { final LoggerContext ctx = (LoggerContext) LogManager.getContext(false); final Configuration config = ctx.getConfiguration(); final Map loggerConfigs = config.getLoggers(); - assertThat(loggerConfigs.size(), equalTo(5)); + + if (rootLevel.isMoreSpecificThan(Level.INFO)) { + assertThat(loggerConfigs.size(), equalTo(5)); + } else { + // below INFO restricted loggers will be set in addition + assertThat(loggerConfigs.size(), greaterThan(5)); + } assertThat(loggerConfigs, hasKey("")); assertThat(loggerConfigs.get("").getLevel(), equalTo(rootLevel)); assertThat(loggerConfigs, hasKey("foo")); diff --git a/qa/restricted-loggers/build.gradle b/qa/restricted-loggers/build.gradle new file mode 100644 index 0000000000000..f08f886d52888 --- /dev/null +++ b/qa/restricted-loggers/build.gradle @@ -0,0 +1,18 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +apply plugin: 'elasticsearch.standalone-test' + +dependencies { + testImplementation project(":test:framework") +} + +tasks.named("test").configure { + // do not enable TRACE_ENABLED + systemProperties.remove('es.insecure_network_trace_enabled') +} diff --git a/qa/restricted-loggers/src/test/java/org/elasticsearch/common/logging/LoggersTests.java b/qa/restricted-loggers/src/test/java/org/elasticsearch/common/logging/LoggersTests.java new file mode 100644 index 0000000000000..d8229b2401290 --- /dev/null +++ b/qa/restricted-loggers/src/test/java/org/elasticsearch/common/logging/LoggersTests.java @@ -0,0 +1,110 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.common.logging; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; + +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.common.logging.Loggers.checkRestrictedLoggers; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +public class LoggersTests extends ESTestCase { + + public void testClusterUpdateSettingsRequestValidationForLoggers() { + assertThat(Loggers.RESTRICTED_LOGGERS, hasSize(greaterThan(0))); + + ClusterUpdateSettingsRequest request = new ClusterUpdateSettingsRequest(); + for (String logger : Loggers.RESTRICTED_LOGGERS) { + var validation = request.persistentSettings(Map.of("logger." + logger, org.elasticsearch.logging.Level.DEBUG)).validate(); + assertNotNull(validation); + assertThat(validation.validationErrors(), contains("Level [DEBUG] is not permitted for logger [" + logger + "]")); + // INFO is permitted + assertNull(request.persistentSettings(Map.of("logger." + logger, org.elasticsearch.logging.Level.INFO)).validate()); + } + } + + public void testCheckRestrictedLoggers() { + assertThat(Loggers.RESTRICTED_LOGGERS, hasSize(greaterThan(0))); + + Settings settings; + for (String restricted : Loggers.RESTRICTED_LOGGERS) { + for (String suffix : List.of("", ".xyz")) { + String logger = restricted + suffix; + for (Level level : List.of(Level.ALL, Level.TRACE, Level.DEBUG)) { + settings = Settings.builder().put("logger." + logger, level).build(); + List errors = checkRestrictedLoggers(settings); + assertThat(errors, contains("Level [" + level + "] is not permitted for logger [" + logger + "]")); + } + for (Level level : List.of(Level.ERROR, Level.WARN, Level.INFO)) { + settings = Settings.builder().put("logger." + logger, level).build(); + assertThat(checkRestrictedLoggers(settings), hasSize(0)); + } + + settings = Settings.builder().put("logger." + logger, "INVALID").build(); + assertThat(checkRestrictedLoggers(settings), hasSize(0)); + + settings = Settings.builder().put("logger." + logger, (String) null).build(); + assertThat(checkRestrictedLoggers(settings), hasSize(0)); + } + } + } + + public void testSetLevelWithRestrictions() { + assertThat(Loggers.RESTRICTED_LOGGERS, hasSize(greaterThan(0))); + + for (String restricted : Loggers.RESTRICTED_LOGGERS) { + + // 'org.apache.http' is an example of a restricted logger, + // a restricted component logger would be `org.apache.http.client.HttpClient` for instance, + // and the parent logger is `org.apache`. + Logger restrictedLogger = LogManager.getLogger(restricted); + Logger restrictedComponent = LogManager.getLogger(restricted + ".component"); + Logger parentLogger = LogManager.getLogger(restricted.substring(0, restricted.lastIndexOf('.'))); + + Loggers.setLevel(restrictedLogger, Level.INFO); + assertHasINFO(restrictedLogger, restrictedComponent); + + for (Logger log : List.of(restrictedComponent, restrictedLogger)) { + // DEBUG is rejected due to restriction + Loggers.setLevel(log, Level.DEBUG); + assertHasINFO(restrictedComponent, restrictedLogger); + } + + // OK for parent `org.apache`, but restriction is enforced for restricted descendants + Loggers.setLevel(parentLogger, Level.DEBUG); + assertEquals(Level.DEBUG, parentLogger.getLevel()); + assertHasINFO(restrictedComponent, restrictedLogger); + + // Inheriting DEBUG of parent `org.apache` is rejected + Loggers.setLevel(restrictedLogger, (Level) null); + assertHasINFO(restrictedComponent, restrictedLogger); + + // DEBUG of root logger isn't propagated to restricted loggers + Loggers.setLevel(LogManager.getRootLogger(), Level.DEBUG); + assertEquals(Level.DEBUG, LogManager.getRootLogger().getLevel()); + assertHasINFO(restrictedComponent, restrictedLogger); + } + } + + private static void assertHasINFO(Logger... loggers) { + for (Logger log : loggers) { + assertThat("Unexpected log level for [" + log.getName() + "]", log.getLevel(), is(Level.INFO)); + } + } +} diff --git a/server/build.gradle b/server/build.gradle index 9ae223de1748e..8c5c1735c13c8 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -152,7 +152,7 @@ if (BuildParams.isSnapshotBuild() == false) { } tasks.named("test").configure { - systemProperty 'es.insecure_network_trace_enabled', 'true' + systemProperty 'es.insecure_network_trace_enabled', 'true' } tasks.named("thirdPartyAudit").configure { diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/ClusterUpdateSettingsRequest.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/ClusterUpdateSettingsRequest.java index 23348716ffcca..5b49a41ed9476 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/ClusterUpdateSettingsRequest.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/ClusterUpdateSettingsRequest.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.support.master.AcknowledgedRequest; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; @@ -62,6 +63,13 @@ public ActionRequestValidationException validate() { if (transientSettings.isEmpty() && persistentSettings.isEmpty()) { validationException = addValidationError("no settings to update", validationException); } + // for bwc we have to reject logger settings on the REST level instead of using a validator + for (String error : Loggers.checkRestrictedLoggers(transientSettings)) { + validationException = addValidationError(error, validationException); + } + for (String error : Loggers.checkRestrictedLoggers(persistentSettings)) { + validationException = addValidationError(error, validationException); + } return validationException; } diff --git a/server/src/main/java/org/elasticsearch/common/logging/Loggers.java b/server/src/main/java/org/elasticsearch/common/logging/Loggers.java index bf0f7c49c80fb..23511fcbe0f72 100644 --- a/server/src/main/java/org/elasticsearch/common/logging/Loggers.java +++ b/server/src/main/java/org/elasticsearch/common/logging/Loggers.java @@ -16,11 +16,17 @@ import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.Configurator; import org.apache.logging.log4j.core.config.LoggerConfig; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.transport.NetworkTraceFlag; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -29,8 +35,18 @@ */ public class Loggers { + private Loggers() {}; + public static final String SPACE = " "; + /** + * Restricted loggers can't be set to a level less specific than INFO. + * For some loggers this might be permitted if {@link NetworkTraceFlag#TRACE_ENABLED} is enabled. + */ + static final List RESTRICTED_LOGGERS = NetworkTraceFlag.TRACE_ENABLED + ? Collections.emptyList() + : List.of("org.apache.http", "com.amazonaws.request"); + public static final Setting LOG_DEFAULT_LEVEL_SETTING = new Setting<>( "logger.level", Level.INFO.name(), @@ -42,6 +58,30 @@ public class Loggers { (key) -> new Setting<>(key, Level.INFO.name(), Level::valueOf, Setting.Property.Dynamic, Setting.Property.NodeScope) ); + public static List checkRestrictedLoggers(Settings settings) { + return checkRestrictedLoggers(settings, RESTRICTED_LOGGERS); + } + + // visible for testing only + static List checkRestrictedLoggers(Settings settings, List restrictions) { + List errors = null; + for (String key : settings.keySet()) { + if (LOG_LEVEL_SETTING.match(key)) { + Level level = Level.toLevel(settings.get(key), null); + if (level != null) { + String logger = key.substring("logger.".length()); + if (level.intLevel() > Level.INFO.intLevel() && restrictions.stream().anyMatch(r -> isSameOrDescendantOf(logger, r))) { + if (errors == null) { + errors = new ArrayList<>(2); + } + errors.add(Strings.format("Level [%s] is not permitted for logger [%s]", level, logger)); + } + } + } + } + return errors == null ? Collections.emptyList() : errors; + } + public static Logger getLogger(Class clazz, ShardId shardId, String... prefixes) { return getLogger( clazz, @@ -100,33 +140,83 @@ private static String formatPrefix(String... prefixes) { * level. */ public static void setLevel(Logger logger, String level) { - final Level l; - if (level == null) { - l = null; - } else { - l = Level.valueOf(level); - } - setLevel(logger, l); + setLevel(logger, level == null ? null : Level.valueOf(level), RESTRICTED_LOGGERS); } + /** + * Set the level of the logger. If the new level is null, the logger will inherit it's level from its nearest ancestor with a non-null + * level. + */ public static void setLevel(Logger logger, Level level) { - if (LogManager.ROOT_LOGGER_NAME.equals(logger.getName()) == false) { - Configurator.setLevel(logger.getName(), level); - } else { + setLevel(logger, level, RESTRICTED_LOGGERS); + } + + // visible for testing only + static void setLevel(Logger logger, Level level, List restrictions) { + // If configuring an ancestor / root, the restriction has to be explicitly set afterward. + boolean setRestriction = false; + + if (isRootLogger(logger.getName())) { + assert level != null : "Log level is required when configuring the root logger"; final LoggerContext ctx = LoggerContext.getContext(false); final Configuration config = ctx.getConfiguration(); final LoggerConfig loggerConfig = config.getLoggerConfig(logger.getName()); loggerConfig.setLevel(level); ctx.updateLoggers(); + setRestriction = level.intLevel() > Level.INFO.intLevel(); + } else { + Level actual = level != null ? level : parentLoggerLevel(logger); + if (actual.intLevel() > Level.INFO.intLevel()) { + for (String restricted : restrictions) { + if (isSameOrDescendantOf(logger.getName(), restricted)) { + LogManager.getLogger(Loggers.class) + .warn("Level [{}/{}] not permitted for logger [{}], skipping.", level, actual, logger.getName()); + return; + } + if (isDescendantOf(restricted, logger.getName())) { + setRestriction = true; + } + } + } + Configurator.setLevel(logger.getName(), level); } // we have to descend the hierarchy final LoggerContext ctx = LoggerContext.getContext(false); for (final LoggerConfig loggerConfig : ctx.getConfiguration().getLoggers().values()) { - if (LogManager.ROOT_LOGGER_NAME.equals(logger.getName()) || loggerConfig.getName().startsWith(logger.getName() + ".")) { + if (isDescendantOf(loggerConfig.getName(), logger.getName())) { Configurator.setLevel(loggerConfig.getName(), level); } } + + if (setRestriction) { + // if necessary, after setting the level of an ancestor, enforce restriction again + for (String restricted : restrictions) { + if (isDescendantOf(restricted, logger.getName())) { + setLevel(LogManager.getLogger(restricted), Level.INFO, Collections.emptyList()); + } + } + } + } + + private static Level parentLoggerLevel(Logger logger) { + int idx = logger.getName().lastIndexOf('.'); + if (idx != -1) { + return LogManager.getLogger(logger.getName().substring(0, idx)).getLevel(); + } + return LogManager.getRootLogger().getLevel(); + } + + private static boolean isRootLogger(String name) { + return LogManager.ROOT_LOGGER_NAME.equals(name); + } + + private static boolean isDescendantOf(String candidate, String ancestor) { + return isRootLogger(ancestor) || candidate.startsWith(ancestor + "."); + } + + private static boolean isSameOrDescendantOf(String candidate, String ancestor) { + return candidate.equals(ancestor) || isDescendantOf(candidate, ancestor); } public static void addAppender(final Logger logger, final Appender appender) { diff --git a/server/src/test/java/org/elasticsearch/common/logging/LoggersTests.java b/server/src/test/java/org/elasticsearch/common/logging/LoggersTests.java index 8e8f0c75fa945..77603aaae068d 100644 --- a/server/src/test/java/org/elasticsearch/common/logging/LoggersTests.java +++ b/server/src/test/java/org/elasticsearch/common/logging/LoggersTests.java @@ -11,18 +11,86 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; import java.io.IOException; import java.net.UnknownHostException; +import java.util.List; import static java.util.Arrays.asList; +import static org.elasticsearch.common.logging.Loggers.checkRestrictedLoggers; import static org.elasticsearch.core.Strings.format; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; public class LoggersTests extends ESTestCase { + // Loggers.RESTRICTED_LOGGERS may be disabled by NetworkTraceFlag.TRACE_ENABLED, use internal API for testing + private List restrictedLoggers = List.of("org.apache.http", "com.amazonaws.request"); + + public void testCheckRestrictedLoggers() { + Settings settings; + for (String restricted : restrictedLoggers) { + for (String suffix : List.of("", ".xyz")) { + String logger = restricted + suffix; + for (Level level : List.of(Level.ALL, Level.TRACE, Level.DEBUG)) { + settings = Settings.builder().put("logger." + logger, level).build(); + List errors = checkRestrictedLoggers(settings, restrictedLoggers); + assertThat(errors, contains("Level [" + level + "] is not permitted for logger [" + logger + "]")); + } + for (Level level : List.of(Level.ERROR, Level.WARN, Level.INFO)) { + settings = Settings.builder().put("logger." + logger, level).build(); + assertThat(checkRestrictedLoggers(settings, restrictedLoggers), hasSize(0)); + } + + settings = Settings.builder().put("logger." + logger, "INVALID").build(); + assertThat(checkRestrictedLoggers(settings, restrictedLoggers), hasSize(0)); + + settings = Settings.builder().put("logger." + logger, (String) null).build(); + assertThat(checkRestrictedLoggers(settings, restrictedLoggers), hasSize(0)); + } + } + } + + public void testSetLevelWithRestrictions() { + for (String restricted : restrictedLoggers) { + + // 'org.apache.http' is an example of a restricted logger, + // a restricted component logger would be `org.apache.http.client.HttpClient` for instance, + // and the parent logger is `org.apache`. + Logger restrictedLogger = LogManager.getLogger(restricted); + Logger restrictedComponent = LogManager.getLogger(restricted + ".component"); + Logger parentLogger = LogManager.getLogger(restricted.substring(0, restricted.lastIndexOf('.'))); + + Loggers.setLevel(restrictedLogger, Level.INFO, restrictedLoggers); + assertHasINFO(restrictedLogger, restrictedComponent); + + for (Logger log : List.of(restrictedComponent, restrictedLogger)) { + // DEBUG is rejected due to restriction + Loggers.setLevel(log, Level.DEBUG, restrictedLoggers); + assertHasINFO(restrictedComponent, restrictedLogger); + } + + // OK for parent `org.apache`, but restriction is enforced for restricted descendants + Loggers.setLevel(parentLogger, Level.DEBUG, restrictedLoggers); + assertEquals(Level.DEBUG, parentLogger.getLevel()); + assertHasINFO(restrictedComponent, restrictedLogger); + + // Inheriting DEBUG of parent `org.apache` is rejected + Loggers.setLevel(restrictedLogger, null, restrictedLoggers); + assertHasINFO(restrictedComponent, restrictedLogger); + + // DEBUG of root logger isn't propagated to restricted loggers + Loggers.setLevel(LogManager.getRootLogger(), Level.DEBUG, restrictedLoggers); + assertEquals(Level.DEBUG, LogManager.getRootLogger().getLevel()); + assertHasINFO(restrictedComponent, restrictedLogger); + } + } + public void testStringSupplierAndFormatting() throws Exception { // adding a random id to allow test to run multiple times. See AbstractConfiguration#addAppender final MockAppender appender = new MockAppender("trace_appender" + randomInt()); @@ -69,4 +137,10 @@ private Throwable randomException() { new IllegalArgumentException("index must be between 10 and 100") ); } + + private static void assertHasINFO(Logger... loggers) { + for (Logger log : loggers) { + assertThat("Unexpected log level for [" + log.getName() + "]", log.getLevel(), is(Level.INFO)); + } + } } diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index 08496060f431b..07308d5d29a9a 100644 --- a/x-pack/plugin/security/build.gradle +++ b/x-pack/plugin/security/build.gradle @@ -153,6 +153,10 @@ dependencies { testImplementation('org.apache.directory.mavibot:mavibot:1.0.0-M8') } +tasks.named("test").configure { + systemProperty 'es.insecure_network_trace_enabled', 'true' +} + tasks.named("processInternalClusterTestResources").configure { from(project(xpackModule('core')).file('src/main/config')) from(project(xpackModule('core')).file('src/test/resources')) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java index 057a55ea4708d..c4e4d58d27178 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java @@ -1065,6 +1065,7 @@ public void testHttpClientConnectionTtlBehaviour() throws URISyntaxException, Il final MockLogAppender appender = new MockLogAppender(); appender.start(); Loggers.addAppender(logger, appender); + // Note: Setting an org.apache.http logger to DEBUG requires es.insecure_network_trace_enabled=true Loggers.setLevel(logger, Level.DEBUG); try { appender.addExpectation( diff --git a/x-pack/plugin/snapshot-repo-test-kit/qa/s3/src/javaRestTest/java/org/elasticsearch/repositories/blobstore/testkit/S3SnapshotRepoTestKitIT.java b/x-pack/plugin/snapshot-repo-test-kit/qa/s3/src/javaRestTest/java/org/elasticsearch/repositories/blobstore/testkit/S3SnapshotRepoTestKitIT.java index dcdfc24406a2b..9a57b07d74a79 100644 --- a/x-pack/plugin/snapshot-repo-test-kit/qa/s3/src/javaRestTest/java/org/elasticsearch/repositories/blobstore/testkit/S3SnapshotRepoTestKitIT.java +++ b/x-pack/plugin/snapshot-repo-test-kit/qa/s3/src/javaRestTest/java/org/elasticsearch/repositories/blobstore/testkit/S3SnapshotRepoTestKitIT.java @@ -36,6 +36,8 @@ public class S3SnapshotRepoTestKitIT extends AbstractSnapshotRepoTestKitRestTest .setting("logger.org.elasticsearch.repositories.blobstore.testkit", "TRACE") .setting("logger.com.amazonaws.request", "DEBUG") .setting("logger.org.apache.http.wire", "DEBUG") + // Necessary to permit setting the above two restricted loggers to DEBUG + .jvmArg("-Des.insecure_network_trace_enabled=true") .build(); @ClassRule From 80dae50b7fc9ed372d2d844bda849e16ee7803cd Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Wed, 21 Feb 2024 18:16:07 +0100 Subject: [PATCH 121/250] Optimized readString for StreamInputs operating on arrays to minimize copying (#104692) This commit implements a readString operating directly on the byte array for BytesReferenceStreamInput, ByteArrayStreamInput and ByteBufferStreamInput to avoid unnecessary allocations. Unfortunately writeString ignores surrogate pairs and encodes these naively character by character, which isn't compatible with the UTF-8 encoding of surrogate pairs (a 4-byte sequence). In case such a surrogate pair is detected or if the byte array doesn't contain enough bytes to read the entire string, this will fall back to the default implementation of readString. --- .../bytes/BytesReferenceStreamInput.java | 18 +++ .../io/stream/ByteArrayStreamInput.java | 24 ++++ .../io/stream/ByteBufferStreamInput.java | 18 +++ .../common/io/stream/StreamInput.java | 60 +++++++++ .../common/io/stream/StreamInputTests.java | 125 ++++++++++++++++++ 5 files changed, 245 insertions(+) create mode 100644 server/src/test/java/org/elasticsearch/common/io/stream/StreamInputTests.java diff --git a/server/src/main/java/org/elasticsearch/common/bytes/BytesReferenceStreamInput.java b/server/src/main/java/org/elasticsearch/common/bytes/BytesReferenceStreamInput.java index d42e1874b2d58..22bed3ea0b1e9 100644 --- a/server/src/main/java/org/elasticsearch/common/bytes/BytesReferenceStreamInput.java +++ b/server/src/main/java/org/elasticsearch/common/bytes/BytesReferenceStreamInput.java @@ -84,6 +84,24 @@ public long readLong() throws IOException { } } + @Override + public String readString() throws IOException { + final int chars = readArraySize(); + if (slice.hasArray()) { + // attempt reading bytes directly into a string to minimize copying + final String string = tryReadStringFromBytes( + slice.array(), + slice.position() + slice.arrayOffset(), + slice.limit() + slice.arrayOffset(), + chars + ); + if (string != null) { + return string; + } + } + return doReadString(chars); + } + @Override public int readVInt() throws IOException { if (slice.remaining() >= 5) { diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/ByteArrayStreamInput.java b/server/src/main/java/org/elasticsearch/common/io/stream/ByteArrayStreamInput.java index 478ae231e16ff..52eee5af3f6f5 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/ByteArrayStreamInput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/ByteArrayStreamInput.java @@ -31,6 +31,16 @@ public ByteArrayStreamInput(byte[] bytes) { reset(bytes); } + @Override + public String readString() throws IOException { + final int chars = readArraySize(); + String string = tryReadStringFromBytes(bytes, pos, limit, chars); + if (string != null) { + return string; + } + return doReadString(chars); + } + @Override public int read() throws IOException { if (limit - pos <= 0) { @@ -65,6 +75,20 @@ public void skipBytes(long count) { pos += (int) count; } + @Override + public long skip(long n) throws IOException { + if (n <= 0L) { + return 0L; + } + int available = available(); + if (n < available) { + pos += (int) n; + return n; + } + pos = limit; + return available; + } + @Override public void close() { // No-op diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/ByteBufferStreamInput.java b/server/src/main/java/org/elasticsearch/common/io/stream/ByteBufferStreamInput.java index f4ae17175fa2d..41d129406551f 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/ByteBufferStreamInput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/ByteBufferStreamInput.java @@ -120,6 +120,24 @@ public static long readVLong(ByteBuffer buffer) throws IOException { return i; } + @Override + public String readString() throws IOException { + final int chars = readArraySize(); + if (buffer.hasArray()) { + // attempt reading bytes directly into a string to minimize copying + final String string = tryReadStringFromBytes( + buffer.array(), + buffer.position() + buffer.arrayOffset(), + buffer.limit() + buffer.arrayOffset(), + chars + ); + if (string != null) { + return string; + } + } + return doReadString(chars); + } + @Override public int read() throws IOException { if (buffer.hasRemaining() == false) { diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java b/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java index 83aa7fb096693..7281616a8d25f 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java @@ -58,6 +58,9 @@ import java.util.function.Function; import java.util.function.IntFunction; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.UTF_8; + /** * A stream from this node to another node. Technically, it can also be streamed to a byte array but that is mostly for testing. * @@ -445,7 +448,10 @@ private char[] ensureLargeSpare(int charCount) { public String readString() throws IOException { final int charCount = readArraySize(); + return doReadString(charCount); + } + protected String doReadString(final int charCount) throws IOException { final char[] charBuffer = charCount > SMALL_STRING_LIMIT ? ensureLargeSpare(charCount) : smallSpare.get(); int charsOffset = 0; @@ -531,6 +537,60 @@ public String readString() throws IOException { return new String(charBuffer, 0, charCount); } + protected String tryReadStringFromBytes(final byte[] bytes, final int start, final int limit, final int chars) throws IOException { + final int end = start + chars; + if (limit < end) { + return null; // not enough bytes to read chars + } + for (int pos = start; pos < end; pos++) { + if ((bytes[pos] & 0x80) != 0) { + // not an ASCII char, fall back to reading a UTF-8 string + return tryReadUtf8StringFromBytes(bytes, start, limit, pos, end - pos); + } + } + skip(chars); // skip the number of chars (equals bytes) on the stream input + // We already validated the top bit is never set (so there's no negatives). + // Using ISO_8859_1 over US_ASCII safes another scan to check just that and is equivalent otherwise. + return new String(bytes, start, chars, ISO_8859_1); + } + + private String tryReadUtf8StringFromBytes(final byte[] bytes, final int start, final int limit, int pos, int chars) throws IOException { + while (pos < limit && chars-- > 0) { + int c = bytes[pos] & 0xff; + switch (c >> 4) { + case 0, 1, 2, 3, 4, 5, 6, 7 -> pos++; + case 12, 13 -> pos += 2; + case 14 -> { + // surrogate pairs are incorrectly encoded, these can't be directly read from bytes + if (maybeHighSurrogate(bytes, pos, limit)) return null; + pos += 3; + } + default -> throwOnBrokenChar(c); + } + } + + if (chars == 0 && pos <= limit) { + pos = pos - start; + skip(pos); // skip the number of bytes relative to start on the stream input + return new String(bytes, start, pos, UTF_8); + } + + // not enough bytes to read all chars from array + return null; + } + + private static boolean maybeHighSurrogate(final byte[] bytes, final int pos, final int limit) { + if (pos + 2 >= limit) { + return true; // beyond limit, we can't tell + } + int c1 = bytes[pos] & 0xff; + int c2 = bytes[pos + 1] & 0xff; + int c3 = bytes[pos + 2] & 0xff; + int surrogateCandidate = ((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F); + // check if in the high surrogate range + return surrogateCandidate >= 0xD800 && surrogateCandidate <= 0xDBFF; + } + private static void throwOnBrokenChar(int c) throws IOException { throw new IOException("Invalid string; unexpected character: " + c + " hex: " + Integer.toHexString(c)); } diff --git a/server/src/test/java/org/elasticsearch/common/io/stream/StreamInputTests.java b/server/src/test/java/org/elasticsearch/common/io/stream/StreamInputTests.java new file mode 100644 index 0000000000000..645461778f637 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/io/stream/StreamInputTests.java @@ -0,0 +1,125 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.common.io.stream; + +import org.elasticsearch.test.ESTestCase; +import org.mockito.Mockito; + +import java.io.IOException; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +// Note: read* methods are tested for concrete implementations, this just covers helpers to read strings +public class StreamInputTests extends ESTestCase { + + private StreamInput in = Mockito.spy(StreamInput.class); + byte[] bytes = "0123456789".getBytes(UTF_8); + + public void testCalculateByteLengthOfAscii() throws IOException { + // not enough bytes to read all chars + assertNull(in.tryReadStringFromBytes(bytes, 1, 10, 10)); + assertNull(in.tryReadStringFromBytes(bytes, 0, 9, 10)); + verify(in, never()).skip(anyLong()); + + assertThat(in.tryReadStringFromBytes(bytes, 9, 10, 1), is("9")); + verify(in).skip(1); + clearInvocations(in); + + assertThat(in.tryReadStringFromBytes(bytes, 0, 10, 10), is("0123456789")); + verify(in).skip(10); + } + + public void testCalculateByteLengthOfNonAscii() throws IOException { + // copy a two bytes char into bytes + System.arraycopy("©".getBytes(UTF_8), 0, bytes, 0, 2); + + assertNull(in.tryReadStringFromBytes(bytes, 0, 1, 1)); + verify(in, never()).skip(anyLong()); + + assertThat(in.tryReadStringFromBytes(bytes, 0, 2, 1), is("©")); + verify(in).skip(2); + clearInvocations(in); + + assertThat(in.tryReadStringFromBytes(bytes, 0, 10, 9), is("©23456789")); + verify(in).skip(10); + clearInvocations(in); + + // copy a three bytes char into bytes + System.arraycopy("€".getBytes(UTF_8), 0, bytes, 0, 3); + + assertNull(in.tryReadStringFromBytes(bytes, 0, 2, 1)); + verify(in, never()).skip(anyLong()); + + assertThat(in.tryReadStringFromBytes(bytes, 0, 3, 1), is("€")); + verify(in).skip(3); + clearInvocations(in); + + assertThat(in.tryReadStringFromBytes(bytes, 0, 10, 8), is("€3456789")); + verify(in).skip(10); + clearInvocations(in); + + // not enough bytes to read all chars + assertNull(in.tryReadStringFromBytes(bytes, 0, 10, 9)); + verify(in, never()).skip(anyLong()); + } + + public void testCalculateByteLengthOfIncompleteNonAscii() throws IOException { + // copy first byte to the end of bytes, this way the string can't ever be read completely + System.arraycopy("©".getBytes(UTF_8), 0, bytes, 9, 1); + + assertThat(in.tryReadStringFromBytes(bytes, 8, 10, 1), is("8")); + verify(in).skip(1); + clearInvocations(in); + + assertNull(in.tryReadStringFromBytes(bytes, 9, 10, 1)); + verify(in, never()).skip(anyLong()); + + // copy first two bytes of a three bytes char into bytes (similar to above) + System.arraycopy("€".getBytes(UTF_8), 0, bytes, 8, 2); + + assertThat(in.tryReadStringFromBytes(bytes, 7, 10, 1), is("7")); + verify(in).skip(1); + clearInvocations(in); + + assertNull(in.tryReadStringFromBytes(bytes, 8, 10, 1)); + verify(in, never()).skip(anyLong()); + } + + public void testCalculateByteLengthOfSurrogate() throws IOException { + BytesStreamOutput bytesOut = new BytesStreamOutput(); + bytesOut.writeString("ab💩"); + bytes = bytesOut.bytes.array(); + + assertThat(bytes[0], is((byte) 4)); // 2+2 characters + assertThat(in.tryReadStringFromBytes(bytes, 1, bytes.length, 2), is("ab")); + verify(in).skip(2); + clearInvocations(in); + + // surrogates use a special encoding, their byte length differs to what new String expects + assertNull(in.tryReadStringFromBytes(bytes, 1, bytes.length, 4)); + assertNull(in.tryReadStringFromBytes(bytes, 3, bytes.length, 2)); + assertNull(in.tryReadStringFromBytes(bytes, 3, bytes.length, 1)); + verify(in, never()).skip(anyLong()); + + // set limit so tight that we cannot read the first 3 byte char + assertNull(in.tryReadStringFromBytes(bytes, 3, 5, 1)); + verify(in, never()).skip(anyLong()); + + // if using the UTF-8 encoding, the surrogate pair is encoded as 4 bytes (rather than 2x 3 bytes) + // this form of encoding isn't supported + System.arraycopy("💩".getBytes(UTF_8), 0, bytes, 0, 4); + assertThrows(IOException.class, () -> in.tryReadStringFromBytes(bytes, 0, bytes.length, 2)); + verify(in, never()).skip(anyLong()); + } +} From ce8402ff555cb159c8242df5bf17f6fdff1e288d Mon Sep 17 00:00:00 2001 From: David Roberts Date: Wed, 21 Feb 2024 17:27:29 +0000 Subject: [PATCH 122/250] [ML] Make regex more efficient (#105705) The regex that was used to detect document IDs left over from version 5.4 had a leading (.*) which can be very inefficient. It's not hard to refactor the test for version 5.4 doc IDs to use a more deterministic regex plus a simple scan for a single character. --- .../ml/job/process/autodetect/state/ModelState.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/state/ModelState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/state/ModelState.java index de2f6d1fe7849..3c352b4b7dec7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/state/ModelState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/job/process/autodetect/state/ModelState.java @@ -21,7 +21,7 @@ public final class ModelState { */ public static final String TYPE = "model_state"; - private static final Pattern V_5_4_DOC_ID_REGEX = Pattern.compile("(.*)-\\d{10}#\\d+"); + private static final Pattern V_5_4_DOC_ID_SUFFIX_REGEX = Pattern.compile("^\\d{10}#\\d+$"); public static String documentId(String jobId, String snapshotId, int docNum) { return jobId + "_" + TYPE + "_" + snapshotId + "#" + docNum; @@ -43,9 +43,13 @@ public static String extractJobId(String docId) { * and ended with hash and an integer. */ private static String v54ExtractJobId(String docId) { - Matcher matcher = V_5_4_DOC_ID_REGEX.matcher(docId); + int potentialSuffixIndex = docId.lastIndexOf('-'); + if (potentialSuffixIndex <= 0 || potentialSuffixIndex >= docId.length() - 1) { + return null; + } + Matcher matcher = V_5_4_DOC_ID_SUFFIX_REGEX.matcher(docId.subSequence(potentialSuffixIndex + 1, docId.length())); if (matcher.matches()) { - return matcher.group(1); + return docId.substring(0, potentialSuffixIndex); } return null; } From 31f9b71890f4fbe52e8754ccdebb4bf81738bcb3 Mon Sep 17 00:00:00 2001 From: Mike Pellegrini Date: Wed, 21 Feb 2024 13:40:24 -0500 Subject: [PATCH 123/250] Updated SemanticQueryBuilder to implement doRewrite --- .../queries/SemanticQueryBuilder.java | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) 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 c44a7125a6fd7..6819b3f097720 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 @@ -8,17 +8,30 @@ package org.elasticsearch.xpack.inference.queries; import org.apache.lucene.search.Query; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.index.query.AbstractQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.inference.InferenceServiceResults; +import org.elasticsearch.inference.InputType; +import org.elasticsearch.inference.TaskType; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xpack.core.inference.action.InferenceAction; import java.io.IOException; +import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; + +import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; +import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; public class SemanticQueryBuilder extends AbstractQueryBuilder { public static final String NAME = "semantic_query"; @@ -28,6 +41,8 @@ public class SemanticQueryBuilder extends AbstractQueryBuilder inferenceResultsSupplier; + public SemanticQueryBuilder(String fieldName, String query) { if (fieldName == null) { throw new IllegalArgumentException("[" + NAME + "] requires a fieldName"); @@ -45,6 +60,12 @@ public SemanticQueryBuilder(StreamInput in) throws IOException { this.query = in.readString(); } + private SemanticQueryBuilder(SemanticQueryBuilder other, SetOnce inferenceResultsSupplier) { + this.fieldName = other.fieldName; + this.query = other.query; + this.inferenceResultsSupplier = inferenceResultsSupplier; + } + @Override public String getWriteableName() { return NAME; @@ -71,6 +92,46 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep builder.endObject(); } + @Override + protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) { + if (inferenceResultsSupplier != null) { + return this; + } + + Set modelsForField = queryRewriteContext.getModelsForField(fieldName); + if (modelsForField.isEmpty()) { + throw new IllegalArgumentException("field [" + fieldName + "] is not a semantic_text field type"); + } + + if (modelsForField.size() > 1) { + // TODO: Handle multi-index semantic queries + throw new IllegalArgumentException("field [" + fieldName + "] has multiple models associated with it"); + } + + // TODO: How to determine task type? + InferenceAction.Request inferenceRequest = new InferenceAction.Request( + TaskType.SPARSE_EMBEDDING, + modelsForField.iterator().next(), + List.of(query), + Map.of(), + InputType.SEARCH + ); + + SetOnce inferenceResultsSupplier = new SetOnce<>(); + queryRewriteContext.registerAsyncAction((client, listener) -> executeAsyncWithOrigin( + client, + ML_ORIGIN, + InferenceAction.INSTANCE, + inferenceRequest, + listener.delegateFailureAndWrap((l, inferenceResponse) -> { + inferenceResultsSupplier.set(inferenceResponse.getResults()); + l.onResponse(null); + }) + )); + + return new SemanticQueryBuilder(this, inferenceResultsSupplier); + } + @Override protected Query doToQuery(SearchExecutionContext context) throws IOException { // TODO: Implement From a9f4b649ce348d1c1813301091df73c228f0b85b Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Wed, 21 Feb 2024 20:05:18 +0100 Subject: [PATCH 124/250] Delegate readString in FilterStreamInput to possibly use an optimized readString of the delegate. (#105712) This is necessary so that the optimized `readString` added in https://github.com/elastic/elasticsearch/pull/104692 is actually used when wrapped in a `FilterStreamInput`. --- .../elasticsearch/common/io/stream/FilterStreamInput.java | 5 +++++ .../index/translog/BufferedChecksumStreamInput.java | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/FilterStreamInput.java b/server/src/main/java/org/elasticsearch/common/io/stream/FilterStreamInput.java index 0e817e16c0b76..c0ef0e0abf39b 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/FilterStreamInput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/FilterStreamInput.java @@ -26,6 +26,11 @@ protected FilterStreamInput(StreamInput delegate) { this.delegate = delegate; } + @Override + public String readString() throws IOException { + return delegate.readString(); + } + @Override public byte readByte() throws IOException { return delegate.readByte(); diff --git a/server/src/main/java/org/elasticsearch/index/translog/BufferedChecksumStreamInput.java b/server/src/main/java/org/elasticsearch/index/translog/BufferedChecksumStreamInput.java index 6ff91a688c97c..6d1456040c8fa 100644 --- a/server/src/main/java/org/elasticsearch/index/translog/BufferedChecksumStreamInput.java +++ b/server/src/main/java/org/elasticsearch/index/translog/BufferedChecksumStreamInput.java @@ -48,6 +48,11 @@ public long getChecksum() { return this.digest.getValue(); } + @Override + public String readString() throws IOException { + return doReadString(readArraySize()); // always use the unoptimized slow path + } + @Override public byte readByte() throws IOException { final byte b = delegate.readByte(); From 40af2756f921afa902994c6dfcd56948c66bcfb2 Mon Sep 17 00:00:00 2001 From: Benjamin Trent <4357155+benwtrent@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:35:33 -0500 Subject: [PATCH 125/250] updating release notes --- docs/changelog/105578.yaml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/changelog/105578.yaml b/docs/changelog/105578.yaml index cbc58e9636a4d..5b7ebd250be0e 100644 --- a/docs/changelog/105578.yaml +++ b/docs/changelog/105578.yaml @@ -1,5 +1,13 @@ pr: 105578 summary: Upgrade to Lucene 9.10.0 area: Search -type: enhancement -issues: [] +type: feature +issues: + - 104556 +highlight: + title: "New Lucene 9.10 release" + body: |- + - https://github.com/apache/lucene/pull/13090: Prevent humongous allocations in ScalarQuantizer when building quantiles. + - https://github.com/apache/lucene/pull/12962: Speedup concurrent multi-segment HNWS graph search + - https://github.com/apache/lucene/pull/13033: PointRangeQuery now exits earlier on segments whose values don't intersect with the query range. When a PointRangeQuery is a required clause of a boolean query, this helps save work on other required clauses of the same boolean query. + - https://github.com/apache/lucene/pull/13026: Propagate minimum competitive score in ReqOptSumScorer. From 011693a515d7b42390cadfe843e56de63adb5f4a Mon Sep 17 00:00:00 2001 From: Mike Pellegrini Date: Wed, 21 Feb 2024 15:23:53 -0500 Subject: [PATCH 126/250] Added semanticQuery to SemanticTextFieldMapper --- .../inference/src/main/java/module-info.java | 1 + .../mapper/SemanticTextFieldMapper.java | 32 +++++++++++++++++++ .../queries/SemanticQueryBuilder.java | 3 ++ 3 files changed, 36 insertions(+) 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 ddd56c758d67c..09a0adb384c2d 100644 --- a/x-pack/plugin/inference/src/main/java/module-info.java +++ b/x-pack/plugin/inference/src/main/java/module-info.java @@ -18,6 +18,7 @@ requires org.apache.httpcomponents.httpcore.nio; requires org.apache.lucene.core; requires org.elasticsearch.logging; + requires org.apache.lucene.join; 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/mapper/SemanticTextFieldMapper.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java index 027b85a9a9f45..69dad8e46a113 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java @@ -8,7 +8,10 @@ package org.elasticsearch.xpack.inference.mapper; import org.apache.lucene.search.Query; +import org.apache.lucene.search.join.BitSetProducer; +import org.apache.lucene.search.join.ScoreMode; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.index.fielddata.FieldDataContext; import org.elasticsearch.index.fielddata.IndexFieldData; import org.elasticsearch.index.mapper.DocumentParserContext; @@ -20,11 +23,18 @@ import org.elasticsearch.index.mapper.SourceValueFetcher; import org.elasticsearch.index.mapper.TextSearchInfo; import org.elasticsearch.index.mapper.ValueFetcher; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.index.search.ESToParentBlockJoinQuery; +import org.elasticsearch.inference.InferenceResults; +import org.elasticsearch.xpack.core.ml.inference.results.TextExpansionResults; import java.io.IOException; import java.util.Map; +import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.INFERENCE_CHUNKS_RESULTS; + /** * A {@link FieldMapper} for semantic text fields. These fields have a model id reference, that is used for performing inference * at ingestion and query time. @@ -126,5 +136,27 @@ public ValueFetcher valueFetcher(SearchExecutionContext context, String format) public IndexFieldData.Builder fielddataBuilder(FieldDataContext fieldDataContext) { throw new IllegalArgumentException("[semantic_text] fields do not support sorting, scripting or aggregating"); } + + public Query semanticQuery( + InferenceResults inferenceResults, + SearchExecutionContext context, + float boost, + String queryName + ) throws IOException { + String fieldName = name() + "." + INFERENCE_CHUNKS_RESULTS; + BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery().minimumShouldMatch(1).boost(boost).queryName(queryName); + + // TODO: Support dense vectors + if (inferenceResults instanceof TextExpansionResults textExpansionResults) { + for (TextExpansionResults.WeightedToken weightedToken : textExpansionResults.getWeightedTokens()) { + queryBuilder.should(QueryBuilders.termQuery(fieldName, weightedToken.token()).boost(weightedToken.weight())); + } + } else { + throw new IllegalArgumentException("Unsupported inference results type [" + inferenceResults.getWriteableName() + "]"); + } + + BitSetProducer parentFilter = context.bitsetFilter(Queries.newNonNestedFilter(context.indexVersionCreated())); + return new ESToParentBlockJoinQuery(queryBuilder.toQuery(context), parentFilter, ScoreMode.Total, name()); + } } } 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 6819b3f097720..3c7ae64ce9fdf 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 @@ -63,6 +63,8 @@ public SemanticQueryBuilder(StreamInput in) throws IOException { private SemanticQueryBuilder(SemanticQueryBuilder other, SetOnce inferenceResultsSupplier) { this.fieldName = other.fieldName; this.query = other.query; + this.boost = other.boost; + this.queryName = other.queryName; this.inferenceResultsSupplier = inferenceResultsSupplier; } @@ -135,6 +137,7 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) { @Override protected Query doToQuery(SearchExecutionContext context) throws IOException { // TODO: Implement + // TODO: Pass boost to generated query return null; } From c53a8e127b511cc016dbce694a44bdcccad099a6 Mon Sep 17 00:00:00 2001 From: Mike Pellegrini Date: Wed, 21 Feb 2024 15:51:46 -0500 Subject: [PATCH 127/250] Updated SemanticQueryBuilder to implement doToQuery --- .../queries/SemanticQueryBuilder.java | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) 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 3c7ae64ce9fdf..b69861943117b 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 @@ -13,16 +13,19 @@ import org.elasticsearch.TransportVersions; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.query.AbstractQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.inference.InferenceResults; import org.elasticsearch.inference.InferenceServiceResults; import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.TaskType; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xpack.core.inference.action.InferenceAction; +import org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper; import java.io.IOException; import java.util.List; @@ -102,12 +105,12 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) { Set modelsForField = queryRewriteContext.getModelsForField(fieldName); if (modelsForField.isEmpty()) { - throw new IllegalArgumentException("field [" + fieldName + "] is not a semantic_text field type"); + throw new IllegalArgumentException("Field [" + fieldName + "] is not a semantic_text field type"); } if (modelsForField.size() > 1) { // TODO: Handle multi-index semantic queries - throw new IllegalArgumentException("field [" + fieldName + "] has multiple models associated with it"); + throw new IllegalArgumentException("Field [" + fieldName + "] has multiple models associated with it"); } // TODO: How to determine task type? @@ -136,18 +139,40 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) { @Override protected Query doToQuery(SearchExecutionContext context) throws IOException { - // TODO: Implement - // TODO: Pass boost to generated query - return null; + InferenceServiceResults inferenceServiceResults = inferenceResultsSupplier.get(); + if (inferenceServiceResults == null) { + throw new IllegalArgumentException("Inference results supplier for field [" + fieldName + "] is empty"); + } + + List inferenceResultsList = inferenceServiceResults.transformToCoordinationFormat(); + if (inferenceResultsList.isEmpty()) { + throw new IllegalArgumentException("No inference results retrieved for field [" + fieldName + "]"); + } else if (inferenceResultsList.size() > 1) { + // TODO: How to handle multiple inference results? + throw new IllegalArgumentException(inferenceResultsList.size() + " inference results retrieved for field [" + fieldName + "]"); + } + + InferenceResults inferenceResults = inferenceResultsList.get(0); + MappedFieldType fieldType = context.getFieldType(fieldName); + if (fieldType instanceof SemanticTextFieldMapper.SemanticTextFieldType == false) { + // TODO: Better exception type to throw here? + throw new IllegalArgumentException( + "Field [" + fieldName + "] is not registered as a " + SemanticTextFieldMapper.CONTENT_TYPE + " field type" + ); + } + + return ((SemanticTextFieldMapper.SemanticTextFieldType) fieldType).semanticQuery(inferenceResults, context, boost, queryName); } @Override protected boolean doEquals(SemanticQueryBuilder other) { - return Objects.equals(fieldName, other.fieldName) && Objects.equals(query, other.query); + return Objects.equals(fieldName, other.fieldName) + && Objects.equals(query, other.query) + && Objects.equals(inferenceResultsSupplier, other.inferenceResultsSupplier); } @Override protected int doHashCode() { - return Objects.hash(fieldName, query); + return Objects.hash(fieldName, query, inferenceResultsSupplier); } } From 4da7a12e71602dca84d9ca7148f4612e0877993e Mon Sep 17 00:00:00 2001 From: Mike Pellegrini Date: Wed, 21 Feb 2024 16:16:36 -0500 Subject: [PATCH 128/250] Updated SemanticQueryBuilder to add fromXContent --- .../queries/SemanticQueryBuilder.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) 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 b69861943117b..96c274fef526c 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 @@ -11,6 +11,7 @@ import org.apache.lucene.util.SetOnce; import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; +import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.index.mapper.MappedFieldType; @@ -24,6 +25,7 @@ import org.elasticsearch.inference.TaskType; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.inference.action.InferenceAction; import org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper; @@ -175,4 +177,56 @@ protected boolean doEquals(SemanticQueryBuilder other) { protected int doHashCode() { return Objects.hash(fieldName, query, inferenceResultsSupplier); } + + public static SemanticQueryBuilder fromXContent(XContentParser parser) throws IOException { + String fieldName = null; + String query = null; + float boost = AbstractQueryBuilder.DEFAULT_BOOST; + String queryName = null; + + String currentFieldName = null; + for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + throwParsingExceptionOnMultipleFields(NAME, parser.getTokenLocation(), fieldName, currentFieldName); + fieldName = currentFieldName; + for (token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if (QUERY_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + query = parser.text(); + } else if (AbstractQueryBuilder.BOOST_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + boost = parser.floatValue(); + } else if (AbstractQueryBuilder.NAME_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + queryName = parser.text(); + } else { + throw new ParsingException( + parser.getTokenLocation(), + "[" + NAME + "] query does not support [" + currentFieldName + "]" + ); + } + } else { + throw new ParsingException( + parser.getTokenLocation(), + "[" + NAME + "] unknown token [" + token + "] after [" + currentFieldName + "]" + ); + } + } + } + } + + if (fieldName == null) { + throw new ParsingException(parser.getTokenLocation(), "[" + NAME + "] no field name specified"); + } + if (query == null) { + throw new ParsingException(parser.getTokenLocation(), "[" + NAME + "] no query specified"); + } + + SemanticQueryBuilder queryBuilder = new SemanticQueryBuilder(fieldName, query); + queryBuilder.queryName(queryName); + queryBuilder.boost(boost); + return queryBuilder; + } } From 3a1d7e3f67f336c1afc7b83e4ddcf696f5364d2d Mon Sep 17 00:00:00 2001 From: Mike Pellegrini Date: Wed, 21 Feb 2024 16:25:30 -0500 Subject: [PATCH 129/250] Added SemanticQueryBuilder to inference plugin --- .../xpack/inference/InferencePlugin.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 2b4f80f897ea7..e1436271ced54 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 @@ -23,6 +23,7 @@ import org.elasticsearch.features.NodeFeature; import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.index.mapper.MetadataFieldMapper; +import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.indices.SystemIndexDescriptor; import org.elasticsearch.inference.InferenceServiceExtension; import org.elasticsearch.inference.InferenceServiceRegistry; @@ -33,6 +34,7 @@ import org.elasticsearch.plugins.InferenceRegistryPlugin; import org.elasticsearch.plugins.MapperPlugin; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.SearchPlugin; import org.elasticsearch.plugins.SystemIndexPlugin; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; @@ -58,6 +60,7 @@ import org.elasticsearch.xpack.inference.logging.ThrottlerManager; import org.elasticsearch.xpack.inference.mapper.SemanticTextFieldMapper; import org.elasticsearch.xpack.inference.mapper.SemanticTextInferenceResultFieldMapper; +import org.elasticsearch.xpack.inference.queries.SemanticQueryBuilder; import org.elasticsearch.xpack.inference.registry.ModelRegistryImpl; import org.elasticsearch.xpack.inference.rest.RestDeleteInferenceModelAction; import org.elasticsearch.xpack.inference.rest.RestGetInferenceModelAction; @@ -86,7 +89,8 @@ public class InferencePlugin extends Plugin ExtensiblePlugin, SystemIndexPlugin, InferenceRegistryPlugin, - MapperPlugin { + MapperPlugin, + SearchPlugin { /** * When this setting is true the verification check that @@ -300,4 +304,13 @@ public Map getMappers() { public Map getMetadataMappers() { return Map.of(SemanticTextInferenceResultFieldMapper.NAME, SemanticTextInferenceResultFieldMapper.PARSER); } + + @Override + public List> getQueries() { + return List.of(new QuerySpec( + SemanticQueryBuilder.NAME, + SemanticQueryBuilder::new, + SemanticQueryBuilder::fromXContent + )); + } } From be55e4b2f359e560aa531222b3079a1e767dbafb Mon Sep 17 00:00:00 2001 From: Mike Pellegrini Date: Wed, 21 Feb 2024 17:47:37 -0500 Subject: [PATCH 130/250] Use Lucene queries to build semantic query --- .../mapper/SemanticTextFieldMapper.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java index 69dad8e46a113..6d991db7f22ff 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextFieldMapper.java @@ -7,7 +7,12 @@ package org.elasticsearch.xpack.inference.mapper; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.BoostQuery; import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.join.BitSetProducer; import org.apache.lucene.search.join.ScoreMode; import org.elasticsearch.common.Strings; @@ -23,8 +28,6 @@ import org.elasticsearch.index.mapper.SourceValueFetcher; import org.elasticsearch.index.mapper.TextSearchInfo; import org.elasticsearch.index.mapper.ValueFetcher; -import org.elasticsearch.index.query.BoolQueryBuilder; -import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.search.ESToParentBlockJoinQuery; import org.elasticsearch.inference.InferenceResults; @@ -142,21 +145,32 @@ public Query semanticQuery( SearchExecutionContext context, float boost, String queryName - ) throws IOException { + ) { + // Cant use QueryBuilders.boolQuery() because a mapper is not registered for .inference, causing + // TermQueryBuilder#doToQuery to fail (at TermQueryBuilder:202) + // TODO: Handle boost and queryName String fieldName = name() + "." + INFERENCE_CHUNKS_RESULTS; - BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery().minimumShouldMatch(1).boost(boost).queryName(queryName); + BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder().setMinimumNumberShouldMatch(1); // TODO: Support dense vectors if (inferenceResults instanceof TextExpansionResults textExpansionResults) { for (TextExpansionResults.WeightedToken weightedToken : textExpansionResults.getWeightedTokens()) { - queryBuilder.should(QueryBuilders.termQuery(fieldName, weightedToken.token()).boost(weightedToken.weight())); + queryBuilder.add( + new BoostQuery( + new TermQuery( + new Term(fieldName, weightedToken.token()) + ), + weightedToken.weight() + ), + BooleanClause.Occur.SHOULD + ); } } else { throw new IllegalArgumentException("Unsupported inference results type [" + inferenceResults.getWriteableName() + "]"); } BitSetProducer parentFilter = context.bitsetFilter(Queries.newNonNestedFilter(context.indexVersionCreated())); - return new ESToParentBlockJoinQuery(queryBuilder.toQuery(context), parentFilter, ScoreMode.Total, name()); + return new ESToParentBlockJoinQuery(queryBuilder.build(), parentFilter, ScoreMode.Total, name()); } } } From b1fcedd7ae30ff232f419ccee234208eab1456cd Mon Sep 17 00:00:00 2001 From: Niels Bauman <33722607+nielsbauman@users.noreply.github.com> Date: Thu, 22 Feb 2024 09:41:11 +0100 Subject: [PATCH 131/250] Fix `uri_parts` processor behaviour for missing extensions (#105689) The `uri_parts` processor was behaving incorrectly for URI's that included a dot in the path but did not have an extension. Also includes YAML REST tests for the same. --- docs/changelog/105689.yaml | 6 +++ .../ingest/common/UriPartsProcessor.java | 13 +++-- .../ingest/common/UriPartsProcessorTests.java | 25 ++++++++++ .../test/ingest/320_uri_parts_processor.yml | 49 +++++++++++++++++++ 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 docs/changelog/105689.yaml create mode 100644 modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/320_uri_parts_processor.yml diff --git a/docs/changelog/105689.yaml b/docs/changelog/105689.yaml new file mode 100644 index 0000000000000..e76281f1b2fc7 --- /dev/null +++ b/docs/changelog/105689.yaml @@ -0,0 +1,6 @@ +pr: 105689 +summary: Fix `uri_parts` processor behaviour for missing extensions +area: Ingest Node +type: bug +issues: + - 105612 diff --git a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/UriPartsProcessor.java b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/UriPartsProcessor.java index 66e6df5fde58d..c476c6a9d3b9d 100644 --- a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/UriPartsProcessor.java +++ b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/UriPartsProcessor.java @@ -140,9 +140,16 @@ private static Map getUriParts(URI uri, URL fallbackUrl) { } if (path != null) { uriParts.put("path", path); - if (path.contains(".")) { - int periodIndex = path.lastIndexOf('.'); - uriParts.put("extension", periodIndex < path.length() ? path.substring(periodIndex + 1) : ""); + // To avoid any issues with extracting the extension from a path that contains a dot, we explicitly extract the extension + // from the last segment in the path. + var lastSegmentIndex = path.lastIndexOf('/'); + if (lastSegmentIndex >= 0) { + var lastSegment = path.substring(lastSegmentIndex); + int periodIndex = lastSegment.lastIndexOf('.'); + if (periodIndex >= 0) { + // Don't include the dot in the extension field. + uriParts.put("extension", lastSegment.substring(periodIndex + 1)); + } } } if (port != -1) { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/UriPartsProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/UriPartsProcessorTests.java index c7d3052eaa9f3..e7552d23d659a 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/UriPartsProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/UriPartsProcessorTests.java @@ -181,6 +181,31 @@ public void testUrlWithCharactersNotToleratedByUri() throws Exception { ); } + public void testDotPathWithoutExtension() throws Exception { + testUriParsing( + "https://www.google.com/path.withdot/filenamewithoutextension", + Map.of("scheme", "https", "domain", "www.google.com", "path", "/path.withdot/filenamewithoutextension") + ); + } + + public void testDotPathWithExtension() throws Exception { + testUriParsing( + "https://www.google.com/path.withdot/filenamewithextension.txt", + Map.of("scheme", "https", "domain", "www.google.com", "path", "/path.withdot/filenamewithextension.txt", "extension", "txt") + ); + } + + /** + * This test verifies that we return an empty extension instead of null if the URI ends with a period. This is probably + * not behaviour we necessarily want to keep forever, but this test ensures that we're conscious about changing that behaviour. + */ + public void testEmptyExtension() throws Exception { + testUriParsing( + "https://www.google.com/foo/bar.", + Map.of("scheme", "https", "domain", "www.google.com", "path", "/foo/bar.", "extension", "") + ); + } + public void testRemoveIfSuccessfulDoesNotRemoveTargetField() throws Exception { String field = "field"; UriPartsProcessor processor = new UriPartsProcessor(null, null, field, field, true, false, false); diff --git a/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/320_uri_parts_processor.yml b/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/320_uri_parts_processor.yml new file mode 100644 index 0000000000000..53512a4a505f2 --- /dev/null +++ b/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/320_uri_parts_processor.yml @@ -0,0 +1,49 @@ +--- +teardown: + - do: + ingest.delete_pipeline: + id: "uri-parts-pipeline" + ignore: 404 + +--- +"Test URI parts Processor": + - do: + ingest.put_pipeline: + id: "uri-parts-pipeline" + body: > + { + "processors": [ + { + "uri_parts" : { + "field" : "my_uri" + } + } + ] + } + - match: { acknowledged: true } + + - do: + index: + index: test + id: "1" + pipeline: "uri-parts-pipeline" + body: { + my_uri: "https://user:pw@testing.google.com:8080/foo/bar.txt?foo1=bar1&foo2=bar2#anchorVal" + } + + - do: + get: + index: test + id: "1" + - match: { _source.my_uri: "https://user:pw@testing.google.com:8080/foo/bar.txt?foo1=bar1&foo2=bar2#anchorVal" } + - match: { _source.url.original: "https://user:pw@testing.google.com:8080/foo/bar.txt?foo1=bar1&foo2=bar2#anchorVal" } + - match: { _source.url.scheme: "https" } + - match: { _source.url.domain: "testing.google.com" } + - match: { _source.url.fragment: "anchorVal" } + - match: { _source.url.path: "/foo/bar.txt" } + - match: { _source.url.port: 8080 } + - match: { _source.url.username: "user" } + - match: { _source.url.password: "pw" } + - match: { _source.url.user_info: "user:pw" } + - match: { _source.url.query: "foo1=bar1&foo2=bar2" } + - match: { _source.url.extension: "txt" } From 50c1dcb8c62d56077b612293e29ab80adb178e50 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 22 Feb 2024 09:22:29 +0000 Subject: [PATCH 132/250] Small improvements to test-only `Netty4HttpClient` (#105694) - Today we flush every pipelined request, but really we should also be checking that we do the right thing if multiple requests all arrive in a single `read()` call. This commit randomly skips some of the intervening flushes to improve coverage in this area. - Today we call `shutdownGracefully` with the default timeout, which adds a couple of seconds to every single test even though there won't be any more tasks to execute. This commit specifies a zero timeout which saves a bunch of time in these tests. --- .../http/netty4/Netty4HttpClient.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java index 56ba3ae1958f7..7ce962ff56b67 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpClient.java @@ -36,6 +36,7 @@ import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.core.Tuple; import org.elasticsearch.tasks.Task; +import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.netty4.NettyAllocator; import java.io.Closeable; @@ -139,9 +140,20 @@ private synchronized List sendRequests(final SocketAddress rem channelFuture = clientBootstrap.connect(remoteAddress); channelFuture.sync(); + boolean needsFinalFlush = false; for (HttpRequest request : requests) { - channelFuture.channel().writeAndFlush(request); + if (ESTestCase.randomBoolean()) { + channelFuture.channel().writeAndFlush(request); + needsFinalFlush = false; + } else { + channelFuture.channel().write(request); + needsFinalFlush = true; + } + } + if (needsFinalFlush) { + channelFuture.channel().flush(); } + if (latch.await(30L, TimeUnit.SECONDS) == false) { fail("Failed to get all expected responses."); } @@ -157,7 +169,7 @@ private synchronized List sendRequests(final SocketAddress rem @Override public void close() { - clientBootstrap.config().group().shutdownGracefully().awaitUninterruptibly(); + clientBootstrap.config().group().shutdownGracefully(0L, 0L, TimeUnit.SECONDS).awaitUninterruptibly(); } /** From 09bdb16aa3b903a78655d141cf0540a0339165ea Mon Sep 17 00:00:00 2001 From: Craig Taverner Date: Thu, 22 Feb 2024 10:50:37 +0100 Subject: [PATCH 133/250] Support chunked bulk loading of larger data files in CSV tests (#105701) --- .../xpack/esql/CsvTestsDataLoader.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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 302fda9b331e3..9763c362c9b4b 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 @@ -48,6 +48,7 @@ import static org.elasticsearch.xpack.esql.CsvTestUtils.multiValuesAwareCsvToStringArray; public class CsvTestsDataLoader { + private static final int BULK_DATA_SIZE = 100_000; private static final TestsDataset EMPLOYEES = new TestsDataset("employees", "mapping-default.json", "employees.csv"); private static final TestsDataset HOSTS = new TestsDataset("hosts", "mapping-hosts.json", "hosts.csv"); private static final TestsDataset APPS = new TestsDataset("apps", "mapping-apps.json", "apps.csv"); @@ -243,8 +244,6 @@ private static void loadCsvData( CheckedBiFunction p, Logger logger ) throws IOException { - // The indexName is optional for a bulk request, but we use it for routing in MultiClusterSpecIT. - Request request = new Request("POST", "/" + indexName + "/_bulk"); StringBuilder builder = new StringBuilder(); try (BufferedReader reader = org.elasticsearch.xpack.ql.TestUtils.reader(resource)) { String line; @@ -359,10 +358,22 @@ private static void loadCsvData( } } lineNumber++; + if (builder.length() > BULK_DATA_SIZE) { + sendBulkRequest(indexName, builder, client, logger); + builder.setLength(0); + } } - builder.append("\n"); } + if (builder.length() > 0) { + sendBulkRequest(indexName, builder, client, logger); + } + } + private static void sendBulkRequest(String indexName, StringBuilder builder, RestClient client, Logger logger) throws IOException { + // The indexName is optional for a bulk request, but we use it for routing in MultiClusterSpecIT. + builder.append("\n"); + logger.debug("Sending bulk request of [{}] bytes for [{}]", builder.length(), indexName); + Request request = new Request("POST", "/" + indexName + "/_bulk"); request.setJsonEntity(builder.toString()); request.addParameter("refresh", "false"); // will be _forcemerge'd next Response response = client.performRequest(request); @@ -373,7 +384,7 @@ private static void loadCsvData( Map result = XContentHelper.convertToMap(xContentType.xContent(), content, false); Object errors = result.get("errors"); if (Boolean.FALSE.equals(errors)) { - logger.info("Data loading of [{}] OK", indexName); + logger.info("Data loading of [{}] bytes into [{}] OK", builder.length(), indexName); } else { throw new IOException("Data loading of [" + indexName + "] failed with errors: " + errors); } From d842ce9f826cece783570f0a02189fa992fd83c2 Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 22 Feb 2024 10:13:46 +0000 Subject: [PATCH 134/250] Remove control-flow exception in `Netty4HttpPipeliningHandler#write` (#105679) There's no need for a try/catch block here when the only exception in sight is being thrown from within the same method and immediately swallowed. This commit replaces the logic with equivalent code using regular branches. --- .../netty4/Netty4HttpPipeliningHandler.java | 52 +++++++------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java index 86fa635078d4f..b86e168e2e620 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java @@ -133,46 +133,34 @@ protected void handlePipelinedRequest(ChannelHandlerContext ctx, Netty4HttpReque } @Override - public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) throws IOException { + public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) { assert msg instanceof Netty4HttpResponse : "Invalid message type: " + msg.getClass(); - boolean success = false; - try { - final Netty4HttpResponse restResponse = (Netty4HttpResponse) msg; - if (restResponse.getSequence() != writeSequence) { - assert restResponse.getSequence() > writeSequence - : "response sequence [" + restResponse.getSequence() + "] we below write sequence [" + writeSequence + "]"; - if (outboundHoldingQueue.size() >= maxEventsHeld) { - int eventCount = outboundHoldingQueue.size() + 1; - throw new IllegalStateException( - "Too many pipelined events [" + eventCount + "]. Max events allowed [" + maxEventsHeld + "]." - ); - } - // response is not at the current sequence number so we add it to the outbound queue and return - assert outboundHoldingQueue.stream().noneMatch(t -> t.v1().getSequence() == writeSequence) - : "duplicate outbound entries for seqno " + writeSequence; - outboundHoldingQueue.add(new Tuple<>(restResponse, promise)); - success = true; - return; - } - - // response is at the current sequence number and does not need to wait for any other response to be written so we write - // it out directly + final Netty4HttpResponse restResponse = (Netty4HttpResponse) msg; + if (restResponse.getSequence() != writeSequence) { + // response is not at the current sequence number so we add it to the outbound queue + enqueuePipelinedResponse(ctx, restResponse, promise); + } else { + // response is at the current sequence number and does not need to wait for any other response to be written doWrite(ctx, restResponse, promise); - success = true; // see if we have any queued up responses that became writeable due to the above write doWriteQueued(ctx); - } catch (IllegalStateException e) { + } + } + + private void enqueuePipelinedResponse(ChannelHandlerContext ctx, Netty4HttpResponse restResponse, ChannelPromise promise) { + assert restResponse.getSequence() > writeSequence + : "response sequence [" + restResponse.getSequence() + "] we below write sequence [" + writeSequence + "]"; + if (outboundHoldingQueue.size() >= maxEventsHeld) { ctx.channel().close(); - } finally { - if (success == false && promise.isDone() == false) { - // The preceding failure may already have failed the promise; use tryFailure() to avoid log noise about double-completion, - // but also check isDone() first to avoid even constructing another exception in most cases. - promise.tryFailure(new ClosedChannelException()); - } + promise.tryFailure(new ClosedChannelException()); + } else { + assert outboundHoldingQueue.stream().noneMatch(t -> t.v1().getSequence() == restResponse.getSequence()) + : "duplicate outbound entries for seqno " + restResponse.getSequence(); + outboundHoldingQueue.add(new Tuple<>(restResponse, promise)); } } - private void doWriteQueued(ChannelHandlerContext ctx) throws IOException { + private void doWriteQueued(ChannelHandlerContext ctx) { while (outboundHoldingQueue.isEmpty() == false && outboundHoldingQueue.peek().v1().getSequence() == writeSequence) { final Tuple top = outboundHoldingQueue.poll(); assert top != null : "we know the outbound holding queue to not be empty at this point"; From c8a35d349cd72682c57b42806b62e96de5975f1b Mon Sep 17 00:00:00 2001 From: David Turner Date: Thu, 22 Feb 2024 10:17:39 +0000 Subject: [PATCH 135/250] `IndexShardRoutingTable` should always be nonempty (#105720) There's only one remaining test that creates an empty `IndexShardRoutingTable`. This commit fixes that, and then adds an assertion to enforce that all `IndexShardRoutingTable` instances include at least a primary shard. --- .../elasticsearch/action/get/TransportGetAction.java | 6 +++--- .../replication/TransportReplicationAction.java | 2 +- .../cluster/routing/IndexShardRoutingTable.java | 3 ++- .../org/elasticsearch/snapshots/SnapshotsService.java | 7 +++---- .../cluster/routing/IndexShardRoutingTableTests.java | 11 +++++++---- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/get/TransportGetAction.java b/server/src/main/java/org/elasticsearch/action/get/TransportGetAction.java index d3d19fe1714ba..db26da382d3e1 100644 --- a/server/src/main/java/org/elasticsearch/action/get/TransportGetAction.java +++ b/server/src/main/java/org/elasticsearch/action/get/TransportGetAction.java @@ -296,11 +296,11 @@ private void tryGetFromTranslog(GetRequest request, IndexShard indexShard, Disco } static DiscoveryNode getCurrentNodeOfPrimary(ClusterState clusterState, ShardId shardId) { - var shardRoutingTable = clusterState.routingTable().shardRoutingTable(shardId); - if (shardRoutingTable.primaryShard() == null || shardRoutingTable.primaryShard().active() == false) { + final var primaryShard = clusterState.routingTable().shardRoutingTable(shardId).primaryShard(); + if (primaryShard.active() == false) { throw new NoShardAvailableActionException(shardId, "primary shard is not active"); } - DiscoveryNode node = clusterState.nodes().get(shardRoutingTable.primaryShard().currentNodeId()); + DiscoveryNode node = clusterState.nodes().get(primaryShard.currentNodeId()); assert node != null; return node; } diff --git a/server/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java b/server/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java index a935c0e4e06bb..d7ff0359bfd27 100644 --- a/server/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java +++ b/server/src/main/java/org/elasticsearch/action/support/replication/TransportReplicationAction.java @@ -848,7 +848,7 @@ protected void doRun() { : "request waitForActiveShards must be set in resolveRequest"; final ShardRouting primary = state.getRoutingTable().shardRoutingTable(request.shardId()).primaryShard(); - if (primary == null || primary.active() == false) { + if (primary.active() == false) { logger.trace( "primary shard [{}] is not yet active, scheduling a retry: action [{}], request [{}], " + "cluster state version [{}]", diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/IndexShardRoutingTable.java b/server/src/main/java/org/elasticsearch/cluster/routing/IndexShardRoutingTable.java index 8e257ff2c7a54..1e5aaa46c1157 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/IndexShardRoutingTable.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/IndexShardRoutingTable.java @@ -114,7 +114,8 @@ public class IndexShardRoutingTable { allShardsStarted = false; } } - assert primary != null || shards.isEmpty() : shards; + assert shards.isEmpty() == false : "cannot have an empty shard routing table"; + assert primary != null : shards; this.primary = primary; this.replicas = CollectionUtils.wrapUnmodifiableOrEmptySingleton(replicas); this.activeShards = CollectionUtils.wrapUnmodifiableOrEmptySingleton(activeShards); diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java index 3b2868298cf65..a0782fa8814cd 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java @@ -1189,7 +1189,7 @@ private static ImmutableOpenMap processWaitingShar IndexRoutingTable indexShardRoutingTable = routingTable.index(shardId.getIndex()); if (indexShardRoutingTable != null) { IndexShardRoutingTable shardRouting = indexShardRoutingTable.shard(shardId.id()); - if (shardRouting != null && shardRouting.primaryShard() != null) { + if (shardRouting != null) { final var primaryNodeId = shardRouting.primaryShard().currentNodeId(); if (nodeIdRemovalPredicate.test(primaryNodeId)) { if (shardStatus.state() == ShardState.PAUSED_FOR_NODE_REMOVAL) { @@ -1274,9 +1274,8 @@ private static boolean waitingShardsStartedOrUnassigned(SnapshotsInProgress snap return true; } ShardRouting shardRouting = indexShardRoutingTable.shard(shardId.shardId()).primaryShard(); - if (shardRouting != null - && (shardRouting.started() && snapshotsInProgress.isNodeIdForRemoval(shardRouting.currentNodeId()) == false - || shardRouting.unassigned())) { + if (shardRouting.started() && snapshotsInProgress.isNodeIdForRemoval(shardRouting.currentNodeId()) == false + || shardRouting.unassigned()) { return true; } } diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/IndexShardRoutingTableTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/IndexShardRoutingTableTests.java index 838a4268fa1cf..2ae9414711801 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/IndexShardRoutingTableTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/IndexShardRoutingTableTests.java @@ -12,7 +12,6 @@ import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.test.ESTestCase; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -35,9 +34,13 @@ public void testEquals() { Index index = new Index("a", "b"); ShardId shardId = new ShardId(index, 1); ShardId shardId2 = new ShardId(index, 2); - IndexShardRoutingTable table1 = new IndexShardRoutingTable(shardId, new ArrayList<>()); - IndexShardRoutingTable table2 = new IndexShardRoutingTable(shardId, new ArrayList<>()); - IndexShardRoutingTable table3 = new IndexShardRoutingTable(shardId2, new ArrayList<>()); + ShardRouting shardRouting = TestShardRouting.newShardRouting(shardId, null, true, ShardRoutingState.UNASSIGNED); + IndexShardRoutingTable table1 = new IndexShardRoutingTable(shardId, List.of(shardRouting)); + IndexShardRoutingTable table2 = new IndexShardRoutingTable(shardId, List.of(shardRouting)); + IndexShardRoutingTable table3 = new IndexShardRoutingTable( + shardId2, + List.of(TestShardRouting.newShardRouting(shardId2, null, true, ShardRoutingState.UNASSIGNED)) + ); String s = "Some other random object"; assertEquals(table1, table1); assertEquals(table1, table2); From dee0be589ca16c5764761a7d3709c653295c093c Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Thu, 22 Feb 2024 11:43:12 +0100 Subject: [PATCH 136/250] Flatten object mappings when subobjects is false (#103542) --- docs/changelog/103542.yaml | 7 + .../mapping/params/subobjects.asciidoc | 74 +++- .../test/data_stream/150_tsdb.yml | 28 +- .../percolator/PercolatorFieldMapper.java | 2 +- .../index/mapper/DocumentParserContext.java | 2 +- .../index/mapper/FieldMapper.java | 4 +- .../elasticsearch/index/mapper/Mapper.java | 8 +- .../index/mapper/MapperBuilderContext.java | 55 ++- .../index/mapper/MapperMergeContext.java | 4 +- .../index/mapper/NestedObjectMapper.java | 20 +- .../index/mapper/ObjectMapper.java | 140 ++++++-- .../index/mapper/PassThroughObjectMapper.java | 2 +- .../index/mapper/RootObjectMapper.java | 4 +- .../MetadataIndexTemplateServiceTests.java | 30 +- .../index/mapper/DynamicTemplatesTests.java | 2 +- .../index/mapper/MapperServiceTests.java | 337 +++++++++++++++--- .../index/mapper/ObjectMapperMergeTests.java | 18 + .../index/mapper/ObjectMapperTests.java | 88 ++++- .../mapper/PassThroughObjectMapperTests.java | 15 +- 19 files changed, 701 insertions(+), 139 deletions(-) create mode 100644 docs/changelog/103542.yaml diff --git a/docs/changelog/103542.yaml b/docs/changelog/103542.yaml new file mode 100644 index 0000000000000..74e713eb2f606 --- /dev/null +++ b/docs/changelog/103542.yaml @@ -0,0 +1,7 @@ +pr: 103542 +summary: Flatten object mappings when subobjects is false +area: Mapping +type: feature +issues: + - 99860 + - 103497 diff --git a/docs/reference/mapping/params/subobjects.asciidoc b/docs/reference/mapping/params/subobjects.asciidoc index 8bac7ed8cbb37..b0a5d3817c332 100644 --- a/docs/reference/mapping/params/subobjects.asciidoc +++ b/docs/reference/mapping/params/subobjects.asciidoc @@ -24,7 +24,12 @@ PUT my-index-000001 "properties": { "metrics": { "type": "object", - "subobjects": false <1> + "subobjects": false, <1> + "properties": { + "time": { "type": "long" }, + "time.min": { "type": "long" }, + "time.max": { "type": "long" } + } } } } @@ -105,3 +110,70 @@ PUT my-index-000001/_doc/metric_1 <2> The document does not support objects The `subobjects` setting for existing fields and the top-level mapping definition cannot be updated. + +==== Auto-flattening object mappings + +It is generally recommended to define the properties of an object that is configured with `subobjects: false` with dotted field names +(as shown in the first example). +However, it is also possible to define these properties as sub-objects in the mappings. +In that case, the mapping will be automatically flattened before it is stored. +This makes it easier to re-use existing mappings without having to re-write them. + +Note that auto-flattening will not work when certain <> are set +on object mappings that are defined under an object configured with `subobjects: false`: + +* The <> mapping parameter must not be `false`. +* The <> mapping parameter must not contradict the implicit or explicit value of the parent. For example, when `dynamic` is set to `false` in the root of the mapping, object mappers that set `dynamic` to `true` can't be auto-flattened. +* The <> mapping parameter must not be set to `true` explicitly. + +[source,console] +-------------------------------------------------- +PUT my-index-000002 +{ + "mappings": { + "properties": { + "metrics": { + "subobjects": false, + "properties": { + "time": { + "type": "object", <1> + "properties": { + "min": { "type": "long" }, <2> + "max": { "type": "long" } + } + } + } + } + } + } +} +GET my-index-000002/_mapping +-------------------------------------------------- + +[source,console-result] +-------------------------------------------------- +{ + "my-index-000002" : { + "mappings" : { + "properties" : { + "metrics" : { + "subobjects" : false, + "properties" : { + "time.min" : { <3> + "type" : "long" + }, + "time.max" : { + "type" : "long" + } + } + } + } + } + } +} +-------------------------------------------------- + +<1> The metrics object can contain further object mappings that will be auto-flattened. + Object mappings at this level must not set certain mapping parameters as explained above. +<2> This field will be auto-flattened to `"time.min"` before the mapping is stored. +<3> The auto-flattened `"time.min"` field can be inspected by looking at the index mapping. diff --git a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml index 114d968eb5e6c..278c14c09a31a 100644 --- a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml +++ b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/150_tsdb.yml @@ -556,14 +556,13 @@ dynamic templates with nesting: - match: { aggregations.filterA.tsids.buckets.0.doc_count: 2 } --- -dynamic templates - subobject in passthrough object error: +subobject in passthrough object auto flatten: - skip: version: " - 8.12.99" - reason: "Support for dynamic fields was added in 8.13" + reason: "Support for passthrough fields was added in 8.13" - do: - catch: /Tried to add subobject \[subcategory\] to object \[attributes\] which does not support subobjects/ indices.put_index_template: - name: my-dynamic-template + name: my-passthrough-template body: index_patterns: [k9s*] data_stream: {} @@ -576,13 +575,34 @@ dynamic templates - subobject in passthrough object error: properties: attributes: type: passthrough + time_series_dimension: true properties: subcategory: type: object properties: dim: type: keyword + - do: + indices.create_data_stream: + name: k9s + - is_true: acknowledged + # save the backing index names for later use + - do: + indices.get_data_stream: + name: k9s + - set: { data_streams.0.indices.0.index_name: idx0name } + - do: + indices.get_mapping: + index: $idx0name + expand_wildcards: hidden + - match: { .$idx0name.mappings.properties.attributes.properties.subcategory\.dim.type: 'keyword' } + +--- +enable subobjects in passthrough object: + - skip: + version: " - 8.12.99" + reason: "Support for passthrough fields was added in 8.13" - do: catch: /Mapping definition for \[attributes\] has unsupported parameters:\ \[subobjects \:\ true\]/ indices.put_index_template: diff --git a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java index 7ba83f9ce71b5..b571766e12b8f 100644 --- a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java +++ b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolatorFieldMapper.java @@ -138,7 +138,7 @@ public PercolatorFieldMapper build(MapperBuilderContext context) { PercolatorFieldType fieldType = new PercolatorFieldType(context.buildFullName(name()), meta.getValue()); // TODO should percolator even allow multifields? MultiFields multiFields = multiFieldsBuilder.build(this, context); - context = context.createChildContext(name()); + context = context.createChildContext(name(), null); KeywordFieldMapper extractedTermsField = createExtractQueryFieldBuilder( EXTRACTED_TERMS_FIELD_NAME, context, diff --git a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java index 66c5de61bcd92..01e67377adafd 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/DocumentParserContext.java @@ -613,7 +613,7 @@ public final MapperBuilderContext createDynamicMapperBuilderContext() { if (objectMapper instanceof PassThroughObjectMapper passThroughObjectMapper) { containsDimensions = passThroughObjectMapper.containsDimensions(); } - return new MapperBuilderContext(p, mappingLookup().isSourceSynthetic(), false, containsDimensions); + return new MapperBuilderContext(p, mappingLookup().isSourceSynthetic(), false, containsDimensions, dynamic); } public abstract XContentParser parser(); 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 75d9fed2a4d4b..71fd9edd49903 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/FieldMapper.java @@ -479,7 +479,7 @@ public MultiFields build(Mapper.Builder mainFieldBuilder, MapperBuilderContext c return empty(); } else { FieldMapper[] mappers = new FieldMapper[mapperBuilders.size()]; - context = context.createChildContext(mainFieldBuilder.name()); + context = context.createChildContext(mainFieldBuilder.name(), null); int i = 0; for (Map.Entry> entry : this.mapperBuilders.entrySet()) { mappers[i++] = entry.getValue().apply(context); @@ -1230,7 +1230,7 @@ protected void merge(FieldMapper in, Conflicts conflicts, MapperMergeContext map for (Parameter param : getParameters()) { param.merge(in, conflicts); } - MapperMergeContext childContext = mapperMergeContext.createChildContext(in.simpleName()); + MapperMergeContext childContext = mapperMergeContext.createChildContext(in.simpleName(), null); for (FieldMapper newSubField : in.multiFields.mappers) { multiFieldsBuilder.update(newSubField, childContext); } 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 14a71531c6abb..7c047125a80d3 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/Mapper.java @@ -24,10 +24,10 @@ public abstract class Mapper implements ToXContentFragment, Iterable { public abstract static class Builder { - private final String name; + private String name; protected Builder(String name) { - this.name = internFieldName(name); + setName(name); } // TODO rename this to leafName? @@ -37,6 +37,10 @@ public final String name() { /** Returns a newly built mapper. */ public abstract Mapper build(MapperBuilderContext context); + + void setName(String name) { + this.name = internFieldName(name); + } } public interface TypeParser { diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperBuilderContext.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperBuilderContext.java index 4154c936bab52..bbfb9298c23ca 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperBuilderContext.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperBuilderContext.java @@ -9,6 +9,9 @@ package org.elasticsearch.index.mapper; import org.elasticsearch.common.Strings; +import org.elasticsearch.core.Nullable; + +import java.util.Objects; /** * Holds context for building Mapper objects from their Builders @@ -19,32 +22,69 @@ public class MapperBuilderContext { * The root context, to be used when building a tree of mappers */ public static MapperBuilderContext root(boolean isSourceSynthetic, boolean isDataStream) { - return new MapperBuilderContext(null, isSourceSynthetic, isDataStream, false); + return new MapperBuilderContext(null, isSourceSynthetic, isDataStream, false, ObjectMapper.Defaults.DYNAMIC); } private final String path; private final boolean isSourceSynthetic; private final boolean isDataStream; private final boolean parentObjectContainsDimensions; + private final ObjectMapper.Dynamic dynamic; MapperBuilderContext(String path) { - this(path, false, false, false); + this(path, false, false, false, ObjectMapper.Defaults.DYNAMIC); } - MapperBuilderContext(String path, boolean isSourceSynthetic, boolean isDataStream, boolean parentObjectContainsDimensions) { + MapperBuilderContext( + String path, + boolean isSourceSynthetic, + boolean isDataStream, + boolean parentObjectContainsDimensions, + ObjectMapper.Dynamic dynamic + ) { + Objects.requireNonNull(dynamic, "dynamic must not be null"); this.path = path; this.isSourceSynthetic = isSourceSynthetic; this.isDataStream = isDataStream; this.parentObjectContainsDimensions = parentObjectContainsDimensions; + this.dynamic = dynamic; + } + + /** + * Creates a new MapperBuilderContext that is a child of this context + * + * @param name the name of the child context + * @param dynamic strategy for handling dynamic mappings in this context + * @return a new MapperBuilderContext with this context as its parent + */ + public MapperBuilderContext createChildContext(String name, @Nullable ObjectMapper.Dynamic dynamic) { + return createChildContext(name, this.parentObjectContainsDimensions, dynamic); } /** * Creates a new MapperBuilderContext that is a child of this context - * @param name the name of the child context + * + * @param name the name of the child context + * @param dynamic strategy for handling dynamic mappings in this context + * @param parentObjectContainsDimensions whether the parent object contains dimensions * @return a new MapperBuilderContext with this context as its parent */ - public MapperBuilderContext createChildContext(String name) { - return new MapperBuilderContext(buildFullName(name), isSourceSynthetic, isDataStream, parentObjectContainsDimensions); + public MapperBuilderContext createChildContext( + String name, + boolean parentObjectContainsDimensions, + @Nullable ObjectMapper.Dynamic dynamic + ) { + return new MapperBuilderContext( + buildFullName(name), + this.isSourceSynthetic, + this.isDataStream, + parentObjectContainsDimensions, + getDynamic(dynamic) + ); + } + + protected ObjectMapper.Dynamic getDynamic(@Nullable ObjectMapper.Dynamic dynamic) { + return dynamic == null ? this.dynamic : dynamic; } /** @@ -78,4 +118,7 @@ public boolean parentObjectContainsDimensions() { return parentObjectContainsDimensions; } + public ObjectMapper.Dynamic getDynamic() { + return dynamic; + } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MapperMergeContext.java b/server/src/main/java/org/elasticsearch/index/mapper/MapperMergeContext.java index 0af182f315559..8f8854ad47c7d 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MapperMergeContext.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MapperMergeContext.java @@ -46,8 +46,8 @@ public static MapperMergeContext from(MapperBuilderContext mapperBuilderContext, * @param name the name of the child context * @return a new {@link MapperMergeContext} with this context as its parent */ - MapperMergeContext createChildContext(String name) { - return createChildContext(mapperBuilderContext.createChildContext(name)); + MapperMergeContext createChildContext(String name, ObjectMapper.Dynamic dynamic) { + return createChildContext(mapperBuilderContext.createChildContext(name, dynamic)); } /** 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 1216618b1e986..f07d69d86f36c 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/NestedObjectMapper.java @@ -62,7 +62,11 @@ public NestedObjectMapper build(MapperBuilderContext context) { this.includeInRoot = Explicit.IMPLICIT_FALSE; } } - NestedMapperBuilderContext nestedContext = new NestedMapperBuilderContext(context.buildFullName(name()), parentIncludedInRoot); + NestedMapperBuilderContext nestedContext = new NestedMapperBuilderContext( + context.buildFullName(name()), + parentIncludedInRoot, + context.getDynamic(dynamic) + ); final String fullPath = context.buildFullName(name()); final String nestedTypePath; if (indexCreatedVersion.before(IndexVersions.V_8_0_0)) { @@ -117,14 +121,14 @@ private static class NestedMapperBuilderContext extends MapperBuilderContext { final boolean parentIncludedInRoot; - NestedMapperBuilderContext(String path, boolean parentIncludedInRoot) { - super(path); + NestedMapperBuilderContext(String path, boolean parentIncludedInRoot, Dynamic dynamic) { + super(path, false, false, false, dynamic); this.parentIncludedInRoot = parentIncludedInRoot; } @Override - public MapperBuilderContext createChildContext(String name) { - return new NestedMapperBuilderContext(buildFullName(name), parentIncludedInRoot); + public MapperBuilderContext createChildContext(String name, Dynamic dynamic) { + return new NestedMapperBuilderContext(buildFullName(name), parentIncludedInRoot, getDynamic(dynamic)); } } @@ -280,7 +284,11 @@ protected MapperMergeContext createChildContext(MapperMergeContext mapperMergeCo parentIncludedInRoot |= this.includeInParent.value(); } return mapperMergeContext.createChildContext( - new NestedMapperBuilderContext(mapperBuilderContext.buildFullName(name), parentIncludedInRoot) + new NestedMapperBuilderContext( + mapperBuilderContext.buildFullName(name), + parentIncludedInRoot, + mapperBuilderContext.getDynamic(dynamic) + ) ); } 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 a9de4bdd1467a..33e736ff122a1 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/ObjectMapper.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.logging.DeprecationCategory; import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.core.Nullable; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersions; import org.elasticsearch.index.mapper.MapperService.MergeReason; @@ -40,6 +41,7 @@ public class ObjectMapper extends Mapper { public static class Defaults { public static final boolean ENABLED = true; public static final Explicit SUBOBJECTS = Explicit.IMPLICIT_TRUE; + public static final Dynamic DYNAMIC = Dynamic.TRUE; } public enum Dynamic { @@ -69,7 +71,7 @@ DynamicFieldsBuilder getDynamicFieldsBuilder() { */ static Dynamic getRootDynamic(MappingLookup mappingLookup) { ObjectMapper.Dynamic rootDynamic = mappingLookup.getMapping().getRoot().dynamic; - return rootDynamic == null ? ObjectMapper.Dynamic.TRUE : rootDynamic; + return rootDynamic == null ? Defaults.DYNAMIC : rootDynamic; } } @@ -154,7 +156,6 @@ protected final Map buildMappers(MapperBuilderContext mapperBuil Map mappers = new HashMap<>(); for (Mapper.Builder builder : mappersBuilders) { Mapper mapper = builder.build(mapperBuilderContext); - assert mapper instanceof ObjectMapper == false || subobjects.value() : "unexpected object while subobjects are disabled"; Mapper existing = mappers.get(mapper.simpleName()); if (existing != null) { // The same mappings or document may hold the same field twice, either because duplicated JSON keys are allowed or @@ -164,7 +165,12 @@ protected final Map buildMappers(MapperBuilderContext mapperBuil // mix of object notation and dot notation. mapper = existing.merge(mapper, MapperMergeContext.from(mapperBuilderContext, Long.MAX_VALUE)); } - mappers.put(mapper.simpleName(), mapper); + if (subobjects.value() == false && mapper instanceof ObjectMapper objectMapper) { + // We're parsing a mapping that has set `subobjects: false` but has defined sub-objects + objectMapper.asFlattenedFieldMappers(mapperBuilderContext).forEach(m -> mappers.put(m.simpleName(), m)); + } else { + mappers.put(mapper.simpleName(), mapper); + } } return mappers; } @@ -177,7 +183,7 @@ public ObjectMapper build(MapperBuilderContext context) { enabled, subobjects, dynamic, - buildMappers(context.createChildContext(name())) + buildMappers(context.createChildContext(name(), dynamic)) ); } } @@ -300,12 +306,9 @@ protected static void parseProperties( } } - if (objBuilder.subobjects.value() == false - && (type.equals(ObjectMapper.CONTENT_TYPE) - || type.equals(NestedObjectMapper.CONTENT_TYPE) - || type.equals(PassThroughObjectMapper.CONTENT_TYPE))) { + if (objBuilder.subobjects.value() == false && type.equals(NestedObjectMapper.CONTENT_TYPE)) { throw new MapperParsingException( - "Tried to add subobject [" + "Tried to add nested object [" + fieldName + "] to object [" + objBuilder.name() @@ -390,6 +393,8 @@ private static void validateFieldName(String fieldName, IndexVersion indexCreate } else { this.mappers = Map.copyOf(mappers); } + assert subobjects.value() || this.mappers.values().stream().noneMatch(m -> m instanceof ObjectMapper) + : "When subobjects is false, mappers must not contain an ObjectMapper"; } /** @@ -462,7 +467,7 @@ public void validate(MappingLookup mappers) { } protected MapperMergeContext createChildContext(MapperMergeContext mapperMergeContext, String name) { - return mapperMergeContext.createChildContext(name); + return mapperMergeContext.createChildContext(name, dynamic); } public ObjectMapper merge(Mapper mergeWith, MergeReason reason, MapperMergeContext parentMergeContext) { @@ -527,7 +532,13 @@ static MergeResult build( subObjects = existing.subobjects; } MapperMergeContext objectMergeContext = existing.createChildContext(parentMergeContext, existing.simpleName()); - Map mergedMappers = buildMergedMappers(existing, mergeWithObject, reason, objectMergeContext); + Map mergedMappers = buildMergedMappers( + existing, + mergeWithObject, + reason, + objectMergeContext, + subObjects.value() + ); return new MergeResult( enabled, subObjects, @@ -540,25 +551,36 @@ private static Map buildMergedMappers( ObjectMapper existing, ObjectMapper mergeWithObject, MergeReason reason, - MapperMergeContext objectMergeContext + MapperMergeContext objectMergeContext, + boolean subobjects ) { - Iterator iterator = mergeWithObject.iterator(); - if (iterator.hasNext() == false) { - return Map.copyOf(existing.mappers); + Map mergedMappers = new HashMap<>(); + for (Mapper childOfExistingMapper : existing.mappers.values()) { + if (subobjects == false && childOfExistingMapper instanceof ObjectMapper objectMapper) { + // An existing mapping with sub-objects is merged with a mapping that has set `subobjects: false` + objectMapper.asFlattenedFieldMappers(objectMergeContext.getMapperBuilderContext()) + .forEach(m -> mergedMappers.put(m.simpleName(), m)); + } else { + putMergedMapper(mergedMappers, childOfExistingMapper); + } } - Map mergedMappers = new HashMap<>(existing.mappers); - while (iterator.hasNext()) { - Mapper mergeWithMapper = iterator.next(); + for (Mapper mergeWithMapper : mergeWithObject) { Mapper mergeIntoMapper = mergedMappers.get(mergeWithMapper.simpleName()); - Mapper merged = null; if (mergeIntoMapper == null) { - if (objectMergeContext.decrementFieldBudgetIfPossible(mergeWithMapper.getTotalFieldsCount())) { - merged = mergeWithMapper; + if (subobjects == false && mergeWithMapper instanceof ObjectMapper objectMapper) { + // An existing mapping that has set `subobjects: false` is merged with a mapping with sub-objects + objectMapper.asFlattenedFieldMappers(objectMergeContext.getMapperBuilderContext()) + .stream() + .filter(m -> objectMergeContext.decrementFieldBudgetIfPossible(m.getTotalFieldsCount())) + .forEach(m -> putMergedMapper(mergedMappers, m)); + } else if (objectMergeContext.decrementFieldBudgetIfPossible(mergeWithMapper.getTotalFieldsCount())) { + putMergedMapper(mergedMappers, mergeWithMapper); } else if (mergeWithMapper instanceof ObjectMapper om) { - merged = truncateObjectMapper(reason, objectMergeContext, om); + putMergedMapper(mergedMappers, truncateObjectMapper(reason, objectMergeContext, om)); } } else if (mergeIntoMapper instanceof ObjectMapper objectMapper) { - merged = objectMapper.merge(mergeWithMapper, reason, objectMergeContext); + assert subobjects : "existing object mappers are supposed to be flattened if subobjects is false"; + putMergedMapper(mergedMappers, objectMapper.merge(mergeWithMapper, reason, objectMergeContext)); } else { assert mergeIntoMapper instanceof FieldMapper || mergeIntoMapper instanceof FieldAliasMapper; if (mergeWithMapper instanceof NestedObjectMapper) { @@ -570,18 +592,21 @@ private static Map buildMergedMappers( // If we're merging template mappings when creating an index, then a field definition always // replaces an existing one. if (reason == MergeReason.INDEX_TEMPLATE) { - merged = mergeWithMapper; + putMergedMapper(mergedMappers, mergeWithMapper); } else { - merged = mergeIntoMapper.merge(mergeWithMapper, objectMergeContext); + putMergedMapper(mergedMappers, mergeIntoMapper.merge(mergeWithMapper, objectMergeContext)); } } - if (merged != null) { - mergedMappers.put(merged.simpleName(), merged); - } } return Map.copyOf(mergedMappers); } + private static void putMergedMapper(Map mergedMappers, @Nullable Mapper merged) { + if (merged != null) { + mergedMappers.put(merged.simpleName(), merged); + } + } + private static ObjectMapper truncateObjectMapper(MergeReason reason, MapperMergeContext context, ObjectMapper objectMapper) { // there's not enough capacity for the whole object mapper, // so we're just trying to add the shallow object, without it's sub-fields @@ -594,6 +619,65 @@ private static ObjectMapper truncateObjectMapper(MergeReason reason, MapperMerge } } + /** + * Returns all FieldMappers this ObjectMapper or its children hold. + * The name of the FieldMappers will be updated to reflect the hierarchy. + * + * @throws IllegalArgumentException if the mapper cannot be flattened + */ + List asFlattenedFieldMappers(MapperBuilderContext context) { + List flattenedMappers = new ArrayList<>(); + ContentPath path = new ContentPath(); + asFlattenedFieldMappers(context, flattenedMappers, path); + return flattenedMappers; + } + + private void asFlattenedFieldMappers(MapperBuilderContext context, List flattenedMappers, ContentPath path) { + ensureFlattenable(context, path); + path.add(simpleName()); + for (Mapper mapper : mappers.values()) { + if (mapper instanceof FieldMapper fieldMapper) { + FieldMapper.Builder fieldBuilder = fieldMapper.getMergeBuilder(); + fieldBuilder.setName(path.pathAsText(mapper.simpleName())); + flattenedMappers.add(fieldBuilder.build(context)); + } else if (mapper instanceof ObjectMapper objectMapper) { + objectMapper.asFlattenedFieldMappers(context, flattenedMappers, path); + } + } + path.remove(); + } + + private void ensureFlattenable(MapperBuilderContext context, ContentPath path) { + if (dynamic != null && context.getDynamic() != dynamic) { + throwAutoFlatteningException( + path, + "the value of [dynamic] (" + + dynamic + + ") is not compatible with the value from its parent context (" + + context.getDynamic() + + ")" + ); + } + if (isEnabled() == false) { + throwAutoFlatteningException(path, "the value of [enabled] is [false]"); + } + if (subobjects.explicit() && subobjects()) { + throwAutoFlatteningException(path, "the value of [subobjects] is [true]"); + } + } + + private void throwAutoFlatteningException(ContentPath path, String reason) { + throw new IllegalArgumentException( + "Object mapper [" + + path.pathAsText(simpleName()) + + "] was found in a context where subobjects is set to false. " + + "Auto-flattening [" + + path.pathAsText(simpleName()) + + "] failed because " + + reason + ); + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { toXContent(builder, params, null); diff --git a/server/src/main/java/org/elasticsearch/index/mapper/PassThroughObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/PassThroughObjectMapper.java index 4ce7f51ed7386..05ae7e59f69c3 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/PassThroughObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/PassThroughObjectMapper.java @@ -60,7 +60,7 @@ public PassThroughObjectMapper build(MapperBuilderContext context) { context.buildFullName(name()), enabled, dynamic, - buildMappers(context.createChildContext(name())), + buildMappers(context.createChildContext(name(), timeSeriesDimensionSubFields.value(), dynamic)), timeSeriesDimensionSubFields ); } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java index a730d8c2da89e..82ff9ef818579 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/RootObjectMapper.java @@ -111,7 +111,7 @@ public RootObjectMapper.Builder addRuntimeFields(Map runti @Override public RootObjectMapper build(MapperBuilderContext context) { - Map mappers = buildMappers(context); + Map mappers = buildMappers(context.createChildContext(null, dynamic)); mappers.putAll(getAliasMappers(mappers, context)); return new RootObjectMapper( name(), @@ -294,7 +294,7 @@ RuntimeField getRuntimeField(String name) { @Override protected MapperMergeContext createChildContext(MapperMergeContext mapperMergeContext, String name) { assert Objects.equals(mapperMergeContext.getMapperBuilderContext().buildFullName("foo"), "foo"); - return mapperMergeContext; + return mapperMergeContext.createChildContext(null, dynamic); } @Override diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java index 14cb19ba89810..84b6feb1dbffa 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateServiceTests.java @@ -1778,7 +1778,7 @@ public void testIndexTemplateFailsToOverrideComponentTemplateMappingField() thro "properties": { "field2": { "type": "object", - "subobjects": false, + "subobjects": false, "properties": { "foo": { "type": "integer" @@ -1803,12 +1803,12 @@ public void testIndexTemplateFailsToOverrideComponentTemplateMappingField() thro { "properties": { "field2": { - "type": "object", - "properties": { - "bar": { - "type": "object" - } - } + "type": "object", + "properties": { + "bar": { + "type": "nested" + } + } } } }"""), null)) @@ -1834,7 +1834,7 @@ public void testIndexTemplateFailsToOverrideComponentTemplateMappingField() thro assertNotNull(e.getCause().getCause()); assertThat( e.getCause().getCause().getMessage(), - containsString("Tried to add subobject [bar] to object [field2] which does not support subobjects") + containsString("Tried to add nested object [bar] to object [field2] which does not support subobjects") ); } @@ -1920,12 +1920,12 @@ public void testUpdateComponentTemplateFailsIfResolvedIndexTemplatesWouldBeInval { "properties": { "field2": { - "type": "object", - "properties": { - "bar": { - "type": "object" - } - } + "type": "object", + "properties": { + "bar": { + "type": "nested" + } + } } } } @@ -1951,7 +1951,7 @@ public void testUpdateComponentTemplateFailsIfResolvedIndexTemplatesWouldBeInval assertNotNull(e.getCause().getCause().getCause()); assertThat( e.getCause().getCause().getCause().getMessage(), - containsString("Tried to add subobject [bar] to object [field2] which does not support subobjects") + containsString("Tried to add nested object [bar] to object [field2] which does not support subobjects") ); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java b/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java index 38960597647e9..0f285992b749a 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/DynamicTemplatesTests.java @@ -1430,7 +1430,7 @@ public void testSubobjectsFalseWithInnerNestedFromDynamicTemplate() { ); assertThat(exception.getRootCause(), instanceOf(MapperParsingException.class)); assertEquals( - "Tried to add subobject [time] to object [__dynamic__test] which does not support subobjects", + "Tried to add nested object [time] to object [__dynamic__test] which does not support subobjects", exception.getRootCause().getMessage() ); } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java b/server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java index 68e7bd6f24664..7f762bbfa7234 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java @@ -567,10 +567,7 @@ public void testMergeMultipleRoots() throws IOException { } }"""); - final MapperService mapperService = createMapperService(mapping(b -> {})); - mapperService.merge("_doc", List.of(mapping1, mapping2), MergeReason.INDEX_TEMPLATE); - - assertEquals(""" + assertMergeEquals(List.of(mapping1, mapping2), """ { "_doc" : { "_meta" : { @@ -587,7 +584,7 @@ public void testMergeMultipleRoots() throws IOException { } } } - }""", Strings.toString(mapperService.documentMapper().mapping(), true, true)); + }"""); } public void testMergeMultipleRootsWithRootType() throws IOException { @@ -641,10 +638,7 @@ public void testMergeMultipleRootsWithoutRootType() throws IOException { } }"""); - final MapperService mapperService = createMapperService(mapping(b -> {})); - mapperService.merge("_doc", List.of(mapping1, mapping2), MergeReason.INDEX_TEMPLATE); - - assertEquals(""" + assertMergeEquals(List.of(mapping1, mapping2), """ { "_doc" : { "_meta" : { @@ -656,7 +650,7 @@ public void testMergeMultipleRootsWithoutRootType() throws IOException { } } } - }""", Strings.toString(mapperService.documentMapper().mapping(), true, true)); + }"""); } public void testValidMappingSubstitution() throws IOException { @@ -680,10 +674,7 @@ public void testValidMappingSubstitution() throws IOException { } }"""); - final MapperService mapperService = createMapperService(mapping(b -> {})); - mapperService.merge("_doc", List.of(mapping1, mapping2), MergeReason.INDEX_TEMPLATE); - - assertEquals(""" + assertMergeEquals(List.of(mapping1, mapping2), """ { "_doc" : { "properties" : { @@ -693,7 +684,7 @@ public void testValidMappingSubstitution() throws IOException { } } } - }""", Strings.toString(mapperService.documentMapper().mapping(), true, true)); + }"""); } public void testValidMappingSubtreeSubstitution() throws IOException { @@ -770,10 +761,7 @@ public void testSameTypeMerge() throws IOException { } }"""); - final MapperService mapperService = createMapperService(mapping(b -> {})); - mapperService.merge("_doc", List.of(mapping1, mapping2), MergeReason.INDEX_TEMPLATE); - - assertEquals(""" + assertMergeEquals(List.of(mapping1, mapping2), """ { "_doc" : { "properties" : { @@ -788,7 +776,7 @@ public void testSameTypeMerge() throws IOException { } } } - }""", Strings.toString(mapperService.documentMapper().mapping(), true, true)); + }"""); } public void testObjectAndNestedTypeSubstitution() throws IOException { @@ -874,10 +862,7 @@ public void testNestedContradictingProperties() throws IOException { } }"""); - final MapperService mapperService = createMapperService(mapping(b -> {})); - mapperService.merge("_doc", List.of(mapping1, mapping2), MergeReason.INDEX_TEMPLATE); - - assertEquals(""" + assertMergeEquals(List.of(mapping1, mapping2), """ { "_doc" : { "properties" : { @@ -895,7 +880,7 @@ public void testNestedContradictingProperties() throws IOException { } } } - }""", Strings.toString(mapperService.documentMapper().mapping(), true, true)); + }"""); } public void testImplicitObjectHierarchy() throws IOException { @@ -912,10 +897,7 @@ public void testImplicitObjectHierarchy() throws IOException { } }"""); - final MapperService mapperService = createMapperService(mapping(b -> {})); - DocumentMapper bulkMerge = mapperService.merge("_doc", List.of(mapping1), MergeReason.INDEX_TEMPLATE); - - assertEquals(""" + assertMergeEquals(List.of(mapping1), """ { "_doc" : { "properties" : { @@ -932,10 +914,7 @@ public void testImplicitObjectHierarchy() throws IOException { } } } - }""", Strings.toString(mapperService.documentMapper().mapping(), true, true)); - - DocumentMapper sequentialMerge = mapperService.merge("_doc", mapping1, MergeReason.INDEX_TEMPLATE); - assertEquals(bulkMerge.mappingSource(), sequentialMerge.mappingSource()); + }"""); } public void testSubobjectsMerge() throws IOException { @@ -965,7 +944,7 @@ public void testSubobjectsMerge() throws IOException { final MapperService mapperService = createMapperService(mapping(b -> {})); mapperService.merge("_doc", List.of(mapping1, mapping2), MergeReason.INDEX_TEMPLATE); - assertEquals(""" + assertMergeEquals(List.of(mapping1, mapping2), """ { "_doc" : { "properties" : { @@ -979,7 +958,7 @@ public void testSubobjectsMerge() throws IOException { } } } - }""", Strings.toString(mapperService.documentMapper().mapping(), true, true)); + }"""); } public void testContradictingSubobjects() throws IOException { @@ -1039,7 +1018,7 @@ public void testContradictingSubobjects() throws IOException { mapperService = createMapperService(mapping(b -> {})); mapperService.merge("_doc", List.of(mapping2, mapping1), MergeReason.INDEX_TEMPLATE); - assertEquals(""" + assertMergeEquals(List.of(mapping2, mapping1), """ { "_doc" : { "properties" : { @@ -1053,7 +1032,7 @@ public void testContradictingSubobjects() throws IOException { } } } - }""", Strings.toString(mapperService.documentMapper().mapping(), true, true)); + }"""); } public void testSubobjectsImplicitObjectsMerge() throws IOException { @@ -1076,12 +1055,21 @@ public void testSubobjectsImplicitObjectsMerge() throws IOException { } }"""); - final MapperService mapperService = createMapperService(mapping(b -> {})); - MapperParsingException e = expectThrows( - MapperParsingException.class, - () -> mapperService.merge("_doc", List.of(mapping1, mapping2), MergeReason.INDEX_TEMPLATE) - ); - assertThat(e.getMessage(), containsString("Tried to add subobject [child] to object [parent] which does not support subobjects")); + assertMergeEquals(List.of(mapping1, mapping2), """ + { + "_doc" : { + "properties" : { + "parent" : { + "subobjects" : false, + "properties" : { + "child.grandchild" : { + "type" : "keyword" + } + } + } + } + } + }"""); } public void testMultipleTypeMerges() throws IOException { @@ -1467,4 +1455,267 @@ public void testMergeUntilLimitCapacityOnlyForParent() throws IOException { assertNull(mapper.mappers().getMapper("parent.child")); } + public void testAutoFlattenObjectsSubobjectsTopLevelMerge() throws IOException { + CompressedXContent mapping1 = new CompressedXContent(""" + { + "subobjects": false + }"""); + + CompressedXContent mapping2 = new CompressedXContent(""" + { + "properties": { + "parent": { + "properties": { + "child": { + "dynamic": true, + "properties": { + "grandchild": { + "type": "keyword" + } + } + } + } + } + } + }"""); + + assertMergeEquals(List.of(mapping1, mapping2), """ + { + "_doc" : { + "subobjects" : false, + "properties" : { + "parent.child.grandchild" : { + "type" : "keyword" + } + } + } + }"""); + } + + public void testAutoFlattenObjectsSubobjectsMerge() throws IOException { + CompressedXContent mapping1 = new CompressedXContent(""" + { + "properties" : { + "parent" : { + "properties" : { + "child" : { + "type": "object" + } + } + } + } + }"""); + + CompressedXContent mapping2 = new CompressedXContent(""" + { + "properties" : { + "parent" : { + "subobjects" : false, + "properties" : { + "child" : { + "properties" : { + "grandchild" : { + "type" : "keyword" + } + } + } + } + } + } + }"""); + + assertMergeEquals(List.of(mapping1, mapping2), """ + { + "_doc" : { + "properties" : { + "parent" : { + "subobjects" : false, + "properties" : { + "child.grandchild" : { + "type" : "keyword" + } + } + } + } + } + }"""); + + assertMergeEquals(List.of(mapping2, mapping1), """ + { + "_doc" : { + "properties" : { + "parent" : { + "subobjects" : false, + "properties" : { + "child.grandchild" : { + "type" : "keyword" + } + } + } + } + } + }"""); + } + + public void testAutoFlattenObjectsSubobjectsMergeConflictingMappingParameter() throws IOException { + CompressedXContent mapping1 = new CompressedXContent(""" + { + "subobjects": false + }"""); + + CompressedXContent mapping2 = new CompressedXContent(""" + { + "properties": { + "parent": { + "dynamic": "false", + "properties": { + "child": { + "properties": { + "grandchild": { + "type": "keyword" + } + } + } + } + } + } + }"""); + + final MapperService mapperService = createMapperService(mapping(b -> {})); + MapperParsingException e = expectThrows( + MapperParsingException.class, + () -> mapperService.merge("_doc", List.of(mapping1, mapping2), MergeReason.INDEX_TEMPLATE) + ); + assertThat( + e.getMessage(), + containsString( + "Failed to parse mapping: Object mapper [parent] was found in a context where subobjects is set to false. " + + "Auto-flattening [parent] failed because the value of [dynamic] (FALSE) is not compatible " + + "with the value from its parent context (TRUE)" + ) + ); + } + + public void testAutoFlattenObjectsSubobjectsMergeConflictingMappingParameterRoot() throws IOException { + CompressedXContent mapping1 = new CompressedXContent(""" + { + "subobjects": false, + "dynamic": false + }"""); + + CompressedXContent mapping2 = new CompressedXContent(""" + { + "subobjects": false, + "properties": { + "parent": { + "dynamic": "true", + "properties": { + "child": { + "properties": { + "grandchild": { + "type": "keyword" + } + } + } + } + } + } + }"""); + + final MapperService mapperService = createMapperService(mapping(b -> {})); + MapperParsingException e = expectThrows( + MapperParsingException.class, + () -> mapperService.merge("_doc", List.of(mapping1, mapping2), MergeReason.INDEX_TEMPLATE) + ); + assertThat( + e.getMessage(), + containsString( + "Failed to parse mapping: Object mapper [parent] was found in a context where subobjects is set to false. " + + "Auto-flattening [parent] failed because the value of [dynamic] (TRUE) is not compatible " + + "with the value from its parent context (FALSE)" + ) + ); + } + + public void testAutoFlattenObjectsSubobjectsMergeNonConflictingMappingParameter() throws IOException { + CompressedXContent mapping = new CompressedXContent(""" + { + "dynamic": false, + "properties": { + "parent": { + "dynamic": true, + "enabled": false, + "subobjects": false, + "properties": { + "child": { + "properties": { + "grandchild": { + "type": "keyword" + } + } + } + } + } + } + }"""); + + assertMergeEquals(List.of(mapping), """ + { + "_doc" : { + "dynamic" : "false", + "properties" : { + "parent" : { + "dynamic" : "true", + "enabled" : false, + "subobjects" : false, + "properties" : { + "child.grandchild" : { + "type" : "keyword" + } + } + } + } + } + }"""); + } + + public void testExpandDottedNotationToObjectMappers() throws IOException { + CompressedXContent mapping1 = new CompressedXContent(""" + { + "properties": { + "parent.child": { + "type": "keyword" + } + } + }"""); + + CompressedXContent mapping2 = new CompressedXContent("{}"); + + assertMergeEquals(List.of(mapping1, mapping2), """ + { + "_doc" : { + "properties" : { + "parent" : { + "properties" : { + "child" : { + "type" : "keyword" + } + } + } + } + } + }"""); + } + + private void assertMergeEquals(List mappingSources, String expected) throws IOException { + final MapperService mapperServiceBulk = createMapperService(mapping(b -> {})); + // simulates multiple component templates being merged in a composable index template + mapperServiceBulk.merge("_doc", mappingSources, MergeReason.INDEX_TEMPLATE); + assertEquals(expected, Strings.toString(mapperServiceBulk.documentMapper().mapping(), true, true)); + + MapperService mapperServiceSequential = createMapperService(mapping(b -> {})); + // simulates a series of mapping updates + mappingSources.forEach(m -> mapperServiceSequential.merge("_doc", m, MergeReason.INDEX_TEMPLATE)); + assertEquals(expected, Strings.toString(mapperServiceSequential.documentMapper().mapping(), true, true)); + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java index 005b14886d059..e024f2fa7b1ea 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperMergeTests.java @@ -311,6 +311,24 @@ public void testMergeWithLimitRuntimeField() { assertEquals(4, mergedAdd1.getTotalFieldsCount()); } + public void testMergeSubobjectsFalseWithObject() { + RootObjectMapper mergeInto = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add( + new ObjectMapper.Builder("parent", Explicit.IMPLICIT_FALSE) + ).build(MapperBuilderContext.root(false, false)); + RootObjectMapper mergeWith = new RootObjectMapper.Builder("_doc", Explicit.IMPLICIT_TRUE).add( + new ObjectMapper.Builder("parent", Explicit.IMPLICIT_TRUE).add( + new ObjectMapper.Builder("child", Explicit.IMPLICIT_TRUE).add( + new KeywordFieldMapper.Builder("grandchild", IndexVersion.current()) + ) + ) + ).build(MapperBuilderContext.root(false, false)); + + ObjectMapper merged = mergeInto.merge(mergeWith, MapperMergeContext.root(false, false, Long.MAX_VALUE)); + ObjectMapper parentMapper = (ObjectMapper) merged.getMapper("parent"); + assertNotNull(parentMapper); + assertNotNull(parentMapper.getMapper("child.grandchild")); + } + private static RootObjectMapper createRootSubobjectFalseLeafWithDots() { FieldMapper.Builder fieldBuilder = new KeywordFieldMapper.Builder("host.name", IndexVersion.current()); FieldMapper fieldMapper = fieldBuilder.build(MapperBuilderContext.root(false, false)); 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 29e5f8540734b..6472f09ce1be7 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/ObjectMapperTests.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.util.List; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; @@ -362,8 +363,8 @@ public void testSubobjectsFalse() throws Exception { assertNotNull(mapperService.fieldType("metrics.service.time.max")); } - public void testSubobjectsFalseWithInnerObject() { - MapperParsingException exception = expectThrows(MapperParsingException.class, () -> createMapperService(mapping(b -> { + public void testSubobjectsFalseWithInnerObject() throws IOException { + MapperService mapperService = createMapperService(mapping(b -> { b.startObject("metrics.service"); { b.field("subobjects", false); @@ -384,11 +385,9 @@ public void testSubobjectsFalseWithInnerObject() { b.endObject(); } b.endObject(); - }))); - assertEquals( - "Failed to parse mapping: Tried to add subobject [time] to object [service] which does not support subobjects", - exception.getMessage() - ); + })); + assertNull(mapperService.fieldType("metrics.service.time")); + assertNotNull(mapperService.fieldType("metrics.service.time.max")); } public void testSubobjectsFalseWithInnerNested() { @@ -407,7 +406,7 @@ public void testSubobjectsFalseWithInnerNested() { b.endObject(); }))); assertEquals( - "Failed to parse mapping: Tried to add subobject [time] to object [service] which does not support subobjects", + "Failed to parse mapping: Tried to add nested object [time] to object [service] which does not support subobjects", exception.getMessage() ); } @@ -430,8 +429,8 @@ public void testExplicitDefaultSubobjects() throws Exception { assertEquals("{\"_doc\":{\"subobjects\":true}}", Strings.toString(mapperService.mappingLookup().getMapping())); } - public void testSubobjectsFalseRootWithInnerObject() { - MapperParsingException exception = expectThrows(MapperParsingException.class, () -> createMapperService(mappingNoSubobjects(b -> { + public void testSubobjectsFalseRootWithInnerObject() throws IOException { + MapperService mapperService = createMapperService(mappingNoSubobjects(b -> { b.startObject("metrics.service.time"); { b.startObject("properties"); @@ -443,11 +442,9 @@ public void testSubobjectsFalseRootWithInnerObject() { b.endObject(); } b.endObject(); - }))); - assertEquals( - "Failed to parse mapping: Tried to add subobject [metrics.service.time] to object [_doc] which does not support subobjects", - exception.getMessage() - ); + })); + assertNull(mapperService.fieldType("metrics.service.time")); + assertNotNull(mapperService.fieldType("metrics.service.time.max")); } public void testSubobjectsFalseRootWithInnerNested() { @@ -457,7 +454,7 @@ public void testSubobjectsFalseRootWithInnerNested() { b.endObject(); }))); assertEquals( - "Failed to parse mapping: Tried to add subobject [metrics.service] to object [_doc] which does not support subobjects", + "Failed to parse mapping: Tried to add nested object [metrics.service] to object [_doc] which does not support subobjects", exception.getMessage() ); } @@ -575,4 +572,63 @@ private ObjectMapper createObjectMapperWithAllParametersSet(CheckedConsumer fields = objectMapper.asFlattenedFieldMappers(rootContext).stream().map(FieldMapper::name).toList(); + assertThat(fields, containsInAnyOrder("parent.keyword1", "parent.child.keyword2")); + } + + public void testFlattenDynamicIncompatible() { + MapperBuilderContext rootContext = MapperBuilderContext.root(false, false); + ObjectMapper objectMapper = new ObjectMapper.Builder("parent", Explicit.IMPLICIT_TRUE).add( + new ObjectMapper.Builder("child", Explicit.IMPLICIT_TRUE).dynamic(Dynamic.FALSE) + ).build(rootContext); + + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> objectMapper.asFlattenedFieldMappers(rootContext) + ); + assertEquals( + "Object mapper [parent.child] was found in a context where subobjects is set to false. " + + "Auto-flattening [parent.child] failed because the value of [dynamic] (FALSE) is not compatible with " + + "the value from its parent context (TRUE)", + exception.getMessage() + ); + } + + public void testFlattenEnabledFalse() { + MapperBuilderContext rootContext = MapperBuilderContext.root(false, false); + ObjectMapper objectMapper = new ObjectMapper.Builder("parent", Explicit.IMPLICIT_TRUE).enabled(false).build(rootContext); + + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> objectMapper.asFlattenedFieldMappers(rootContext) + ); + assertEquals( + "Object mapper [parent] was found in a context where subobjects is set to false. " + + "Auto-flattening [parent] failed because the value of [enabled] is [false]", + exception.getMessage() + ); + } + + public void testFlattenExplicitSubobjectsTrue() { + MapperBuilderContext rootContext = MapperBuilderContext.root(false, false); + ObjectMapper objectMapper = new ObjectMapper.Builder("parent", Explicit.EXPLICIT_TRUE).build(rootContext); + + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> objectMapper.asFlattenedFieldMappers(rootContext) + ); + assertEquals( + "Object mapper [parent] was found in a context where subobjects is set to false. " + + "Auto-flattening [parent] failed because the value of [subobjects] is [true]", + exception.getMessage() + ); + } } diff --git a/server/src/test/java/org/elasticsearch/index/mapper/PassThroughObjectMapperTests.java b/server/src/test/java/org/elasticsearch/index/mapper/PassThroughObjectMapperTests.java index 40994e2835e2b..b49ed2cf99df6 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/PassThroughObjectMapperTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/PassThroughObjectMapperTests.java @@ -90,9 +90,9 @@ public void testSubobjectsThrows() throws IOException { ); } - public void testAddSubobjectThrows() throws IOException { - MapperException exception = expectThrows(MapperException.class, () -> createMapperService(mapping(b -> { - b.startObject("labels").field("type", "passthrough"); + public void testAddSubobjectAutoFlatten() throws IOException { + MapperService mapperService = createMapperService(mapping(b -> { + b.startObject("labels").field("type", "passthrough").field("time_series_dimension", "true"); { b.startObject("properties"); { @@ -107,12 +107,11 @@ public void testAddSubobjectThrows() throws IOException { b.endObject(); } b.endObject(); - }))); + })); - assertEquals( - "Failed to parse mapping: Tried to add subobject [subobj] to object [labels] which does not support subobjects", - exception.getMessage() - ); + var dim = mapperService.mappingLookup().getMapper("labels.subobj.dim"); + assertThat(dim, instanceOf(KeywordFieldMapper.class)); + assertTrue(((KeywordFieldMapper) dim).fieldType().isDimension()); } public void testWithoutMappers() throws IOException { From d26214f4f377d035c7d99bb74e98e0c86d70d725 Mon Sep 17 00:00:00 2001 From: Tim Grein Date: Thu, 22 Feb 2024 12:14:04 +0100 Subject: [PATCH 137/250] [Connectors API] Extend tests for update connector filtering (#105697) --- .../332_connector_update_filtering.yml | 178 +++++++++++++++++- .../connector/ConnectorFilteringTests.java | 126 ++++++++++++- 2 files changed, 302 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/332_connector_update_filtering.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/332_connector_update_filtering.yml index a693ba5431d4b..abb43806ec793 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/332_connector_update_filtering.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/332_connector_update_filtering.yml @@ -13,7 +13,7 @@ setup: is_native: false service_type: super-connector --- -"Update Connector Filtering": +"Update Connector Filtering with advanced snippet value array": - do: connector.update_filtering: connector_id: test-connector @@ -107,6 +107,7 @@ setup: - match: { filtering.0.domain: DEFAULT } - match: { filtering.0.active.advanced_snippet.created_at: "2023-05-25T12:30:00.000Z" } + - match: { filtering.0.active.advanced_snippet.value.0.tables.0.: "some_table" } - match: { filtering.0.active.rules.0.id: "RULE-ACTIVE-0" } - match: { filtering.0.draft.rules.0.id: "RULE-DRAFT-0" } @@ -115,6 +116,181 @@ setup: - match: { filtering.1.active.rules.0.id: "RULE-ACTIVE-1" } - match: { filtering.1.draft.rules.0.id: "RULE-DRAFT-1" } +--- +"Update Connector Filtering with advanced snippet value object": + - do: + connector.update_filtering: + connector_id: test-connector + body: + filtering: + - active: + advanced_snippet: + created_at: "2023-05-25T12:30:00.000Z" + updated_at: "2023-05-25T12:30:00.000Z" + value: + some_filtering_key: "some_filtering_value" + rules: + - created_at: "2023-05-25T12:30:00.000Z" + field: _ + id: RULE-ACTIVE-0 + order: 0 + policy: include + rule: regex + updated_at: "2023-05-25T12:30:00.000Z" + value: ".*" + validation: + errors: [] + state: valid + domain: DEFAULT + draft: + advanced_snippet: + created_at: "2023-05-25T12:30:00.000Z" + updated_at: "2023-05-25T12:30:00.000Z" + value: {} + rules: + - created_at: "2023-05-25T12:30:00.000Z" + field: _ + id: RULE-DRAFT-0 + order: 0 + policy: include + rule: regex + updated_at: "2023-05-25T12:30:00.000Z" + value: ".*" + validation: + errors: [] + state: valid + - active: + advanced_snippet: + created_at: "2021-05-25T12:30:00.000Z" + updated_at: "2021-05-25T12:30:00.000Z" + value: {} + rules: + - created_at: "2021-05-25T12:30:00.000Z" + field: _ + id: RULE-ACTIVE-1 + order: 0 + policy: include + rule: regex + updated_at: "2021-05-25T12:30:00.000Z" + value: ".*" + validation: + errors: [] + state: valid + domain: TEST + draft: + advanced_snippet: + created_at: "2021-05-25T12:30:00.000Z" + updated_at: "2021-05-25T12:30:00.000Z" + value: {} + rules: + - created_at: "2021-05-25T12:30:00.000Z" + field: _ + id: RULE-DRAFT-1 + order: 0 + policy: exclude + rule: regex + updated_at: "2021-05-25T12:30:00.000Z" + value: ".*" + validation: + errors: [] + state: valid + + - match: { result: updated } + + - do: + connector.get: + connector_id: test-connector + + - match: { filtering.0.domain: DEFAULT } + - match: { filtering.0.active.advanced_snippet.created_at: "2023-05-25T12:30:00.000Z" } + - match: { filtering.0.active.advanced_snippet.value.some_filtering_key: "some_filtering_value" } + - match: { filtering.0.active.rules.0.id: "RULE-ACTIVE-0" } + - match: { filtering.0.draft.rules.0.id: "RULE-DRAFT-0" } + + - match: { filtering.1.domain: TEST } + - match: { filtering.1.active.advanced_snippet.created_at: "2021-05-25T12:30:00.000Z" } + - match: { filtering.1.active.rules.0.id: "RULE-ACTIVE-1" } + - match: { filtering.1.draft.rules.0.id: "RULE-DRAFT-1" } + +--- +"Update Connector Filtering with value literal - Wrong advanced snippet value": + - do: + catch: "bad_request" + connector.update_filtering: + connector_id: test-connector + body: + filtering: + - active: + advanced_snippet: + created_at: "2023-05-25T12:30:00.000Z" + updated_at: "2023-05-25T12:30:00.000Z" + value: "string literal" + rules: + - created_at: "2023-05-25T12:30:00.000Z" + field: _ + id: RULE-ACTIVE-0 + order: 0 + policy: include + rule: regex + updated_at: "2023-05-25T12:30:00.000Z" + value: ".*" + validation: + errors: [] + state: valid + domain: DEFAULT + draft: + advanced_snippet: + created_at: "2023-05-25T12:30:00.000Z" + updated_at: "2023-05-25T12:30:00.000Z" + value: {} + rules: + - created_at: "2023-05-25T12:30:00.000Z" + field: _ + id: RULE-DRAFT-0 + order: 0 + policy: include + rule: regex + updated_at: "2023-05-25T12:30:00.000Z" + value: ".*" + validation: + errors: [] + state: valid + - active: + advanced_snippet: + created_at: "2021-05-25T12:30:00.000Z" + updated_at: "2021-05-25T12:30:00.000Z" + value: {} + rules: + - created_at: "2021-05-25T12:30:00.000Z" + field: _ + id: RULE-ACTIVE-1 + order: 0 + policy: include + rule: regex + updated_at: "2021-05-25T12:30:00.000Z" + value: ".*" + validation: + errors: [] + state: valid + domain: TEST + draft: + advanced_snippet: + created_at: "2021-05-25T12:30:00.000Z" + updated_at: "2021-05-25T12:30:00.000Z" + value: {} + rules: + - created_at: "2021-05-25T12:30:00.000Z" + field: _ + id: RULE-DRAFT-1 + order: 0 + policy: exclude + rule: regex + updated_at: "2021-05-25T12:30:00.000Z" + value: ".*" + validation: + errors: [] + state: valid + --- "Update Connector Filtering - Connector doesn't exist": - do: diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorFilteringTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorFilteringTests.java index 20c2200b26f2b..8c1cdcb418142 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorFilteringTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorFilteringTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.search.SearchModule; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentParseException; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentType; import org.junit.Before; @@ -110,7 +111,7 @@ public void testToXContent() throws IOException { } - public void testToXContent_WithAdvancedSnippetPopulated() throws IOException { + public void testToXContent_WithAdvancedSnippetPopulatedWithAValueArray() throws IOException { String content = XContentHelper.stripWhitespace(""" { "active": { @@ -177,6 +178,129 @@ public void testToXContent_WithAdvancedSnippetPopulated() throws IOException { } + public void testToXContent_WithAdvancedSnippetPopulatedWithAValueObject() throws IOException { + String content = XContentHelper.stripWhitespace(""" + { + "active": { + "advanced_snippet": { + "created_at": "2023-11-09T15:13:08.231Z", + "updated_at": "2023-11-09T15:13:08.231Z", + "value": { + "service": "Incident", + "query": "user_nameSTARTSWITHa" + } + }, + "rules": [ + { + "created_at": "2023-11-09T15:13:08.231Z", + "field": "_", + "id": "DEFAULT", + "order": 0, + "policy": "include", + "rule": "regex", + "updated_at": "2023-11-09T15:13:08.231Z", + "value": ".*" + } + ], + "validation": { + "errors": [], + "state": "valid" + } + }, + "domain": "DEFAULT", + "draft": { + "advanced_snippet": { + "created_at": "2023-11-09T15:13:08.231Z", + "updated_at": "2023-11-09T15:13:08.231Z", + "value": {} + }, + "rules": [ + { + "created_at": "2023-11-09T15:13:08.231Z", + "field": "_", + "id": "DEFAULT", + "order": 0, + "policy": "include", + "rule": "regex", + "updated_at": "2023-11-09T15:13:08.231Z", + "value": ".*" + } + ], + "validation": { + "errors": [], + "state": "valid" + } + } + } + """); + + ConnectorFiltering filtering = ConnectorFiltering.fromXContentBytes(new BytesArray(content), XContentType.JSON); + boolean humanReadable = true; + BytesReference originalBytes = toShuffledXContent(filtering, XContentType.JSON, ToXContent.EMPTY_PARAMS, humanReadable); + ConnectorFiltering parsed; + try (XContentParser parser = createParser(XContentType.JSON.xContent(), originalBytes)) { + parsed = ConnectorFiltering.fromXContent(parser); + } + assertToXContentEquivalent(originalBytes, toXContent(parsed, XContentType.JSON, humanReadable), XContentType.JSON); + + } + + public void testToXContent_WithAdvancedSnippetPopulatedWithAValueLiteral_ExpectParseException() throws IOException { + String content = XContentHelper.stripWhitespace(""" + { + "active": { + "advanced_snippet": { + "created_at": "2023-11-09T15:13:08.231Z", + "updated_at": "2023-11-09T15:13:08.231Z", + "value": "string literal" + }, + "rules": [ + { + "created_at": "2023-11-09T15:13:08.231Z", + "field": "_", + "id": "DEFAULT", + "order": 0, + "policy": "include", + "rule": "regex", + "updated_at": "2023-11-09T15:13:08.231Z", + "value": ".*" + } + ], + "validation": { + "errors": [], + "state": "valid" + } + }, + "domain": "DEFAULT", + "draft": { + "advanced_snippet": { + "created_at": "2023-11-09T15:13:08.231Z", + "updated_at": "2023-11-09T15:13:08.231Z", + "value": {} + }, + "rules": [ + { + "created_at": "2023-11-09T15:13:08.231Z", + "field": "_", + "id": "DEFAULT", + "order": 0, + "policy": "include", + "rule": "regex", + "updated_at": "2023-11-09T15:13:08.231Z", + "value": ".*" + } + ], + "validation": { + "errors": [], + "state": "valid" + } + } + } + """); + + assertThrows(XContentParseException.class, () -> ConnectorFiltering.fromXContentBytes(new BytesArray(content), XContentType.JSON)); + } + private void assertTransportSerialization(ConnectorFiltering testInstance) throws IOException { ConnectorFiltering deserializedInstance = copyInstance(testInstance); assertNotSame(testInstance, deserializedInstance); From d7fbabc7565c295cd1639fe1b1f49e006c9647cf Mon Sep 17 00:00:00 2001 From: Nikolaj Volgushev Date: Thu, 22 Feb 2024 12:20:22 +0100 Subject: [PATCH 138/250] Consistently use `PATH_RESTRICTED` (#105630) Follow up to: https://github.com/elastic/elasticsearch/pull/105336 --- .../java/org/elasticsearch/rest/RestRequest.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/rest/RestRequest.java b/server/src/main/java/org/elasticsearch/rest/RestRequest.java index e3e27ddf5cf5b..66ba0c743813e 100644 --- a/server/src/main/java/org/elasticsearch/rest/RestRequest.java +++ b/server/src/main/java/org/elasticsearch/rest/RestRequest.java @@ -48,11 +48,7 @@ public class RestRequest implements ToXContent.Params, Traceable { - @Deprecated() - // TODO remove once Serverless is updated - public static final String RESPONSE_RESTRICTED = "responseRestricted"; - // TODO rename to `pathRestricted` once Serverless is updated - public static final String PATH_RESTRICTED = "responseRestricted"; + public static final String PATH_RESTRICTED = "pathRestricted"; // tchar pattern as defined by RFC7230 section 3.2.6 private static final Pattern TCHAR_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+\\-.\\^_`|~]+"); @@ -629,12 +625,6 @@ public void markPathRestricted(String restriction) { consumedParams.add(PATH_RESTRICTED); } - @Deprecated() - // TODO remove once Serverless is updated - public void markResponseRestricted(String restriction) { - markPathRestricted(restriction); - } - @Override public String getSpanId() { return "rest-" + getRequestId(); From a9783c4f7f04ce2d4552861ecc2296380ea13026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 22 Feb 2024 13:50:06 +0100 Subject: [PATCH 139/250] [DOCS] Updates the list of community contributed clients. (#105623) --- docs/community-clients/index.asciidoc | 53 +++++++++++++++++---------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/docs/community-clients/index.asciidoc b/docs/community-clients/index.asciidoc index e15e766ffec0c..ebde04b02f18a 100644 --- a/docs/community-clients/index.asciidoc +++ b/docs/community-clients/index.asciidoc @@ -43,12 +43,12 @@ a number of clients that have been contributed by the community for various lang [[b4j]] == B4J * https://www.b4x.com/android/forum/threads/server-jelasticsearch-search-and-text-analytics.73335/[jElasticsearch]: - B4J client based on the official Java REST client. + B4J client based on the official Java REST client. **- Last release more than a year ago** [[cpp]] == C++ * https://github.com/seznam/elasticlient[elasticlient]: simple library for - simplified work with Elasticsearch in C++. + simplified work with Elasticsearch in C++. **- Last commit more than a year ago** [[clojure]] == Clojure @@ -57,7 +57,7 @@ a number of clients that have been contributed by the community for various lang Clojure client, based on the new official low-level REST client. * https://github.com/clojurewerkz/elastisch[Elastisch]: - Clojure client. + Clojure client. **- Last commit more than a year ago** [[coldfusion]] == ColdFusion (CFML) @@ -71,17 +71,17 @@ a number of clients that have been contributed by the community for various lang == Erlang * https://github.com/tsloughter/erlastic_search[erlastic_search]: - Erlang client using HTTP. + Erlang client using HTTP. **- Last commit more than a year ago** * https://github.com/datahogs/tirexs[Tirexs]: An https://github.com/elixir-lang/elixir[Elixir] based API/DSL, inspired by https://github.com/karmi/tire[Tire]. Ready to use in pure Erlang - environment. + environment. **- Last commit more than a year ago** * https://github.com/sashman/elasticsearch_elixir_bulk_processor[Elixir Bulk Processor]: Dynamically configurable Elixir port of the {client}/java-api/current/java-docs-bulk-processor.html[Bulk Processor]. - Implemented using GenStages to handle back pressure. + Implemented using GenStages to handle back pressure. **- Last commit more than a year ago** [[go]] == Go @@ -90,13 +90,13 @@ Also see the {client}/go-api/current/index.html[official Elasticsearch Go client]. * https://github.com/mattbaird/elastigo[elastigo]: - Go client. + Go client. **- Last commit more than a year ago** * https://github.com/olivere/elastic[elastic]: - Elasticsearch client for Google Go. + Elasticsearch client for Google Go. **- Last commit more than a year ago** * https://github.com/softctrl/elk[elk]: - Golang lib for Elasticsearch client. + Golang lib for Elasticsearch client. **- Last commit more than a year ago** [[haskell]] @@ -114,7 +114,7 @@ client]. Java Rest client with comprehensive Query DSL API. * https://github.com/searchbox-io/Jest[Jest]: - Java Rest client. + Java Rest client. ** - No longer maintained** [[javascript]] == JavaScript @@ -133,19 +133,19 @@ Elasticsearch client inspired by the {client}/ruby-api/current/index.html[offici * https://github.com/mbuhot/eskotlin[ES Kotlin]: Elasticsearch Query DSL for kotlin based on the - {client}/java-api/current/index.html[official Elasticsearch Java client]. + {client}/java-api/current/index.html[official Elasticsearch Java client]. **- Last commit more than a year ago** * https://github.com/jillesvangurp/es-kotlin-wrapper-client[ES Kotlin Wrapper Client]: Kotlin extension functions and abstractions for the {client}/java-api/current/index.html[official Elasticsearch high-level client]. Aims to reduce the amount of boilerplate needed to do searches, bulk - indexing and other common things users do with the client. + indexing and other common things users do with the client. **- No longer maintained** [[lua]] == Lua * https://github.com/DhavalKapil/elasticsearch-lua[elasticsearch-lua]: - Lua client for Elasticsearch + Lua client for Elasticsearch **- Last commit more than a year ago** [[dotnet]] == .NET @@ -158,7 +158,8 @@ See the {client}/net-api/current/index.html[official Elasticsearch .NET client]. Also see the {client}/perl-api/current/index.html[official Elasticsearch Perl client]. -* https://metacpan.org/pod/Elastijk[Elastijk]: A low-level, minimal HTTP client. +* https://metacpan.org/pod/Elastijk[Elastijk]: A low-level, minimal HTTP client. +**- Last commit more than a year ago** [[php]] @@ -171,11 +172,13 @@ client]. PHP client. * https://github.com/nervetattoo/elasticsearch[elasticsearch]: PHP client. +**- Last commit more than a year ago** * https://github.com/madewithlove/elasticsearcher[elasticsearcher]: Agnostic lightweight package on top of the Elasticsearch PHP client. Its main goal is to allow for easier structuring of queries and indices in your application. It does not want to hide or replace functionality of the Elasticsearch PHP client. +**- Last commit more than a year ago** [[python]] == Python @@ -191,9 +194,11 @@ client]. * https://github.com/ropensci/elasticdsl[elasticdsl]: A high-level R DSL for Elasticsearch, wrapping the elastic R client. + **- No longer maintained** * https://github.com/uptake/uptasticsearch[uptasticsearch]: - An R client tailored to data science workflows. + An R client tailored to data science workflows. + **- Last commit more than a year ago** [[ruby]] == Ruby @@ -202,6 +207,7 @@ Also see the {client}/ruby-api/current/index.html[official Elasticsearch Ruby cl * https://github.com/printercu/elastics-rb[elastics]: Tiny client with built-in zero-downtime migrations and ActiveRecord integration. + **- Last commit more than a year ago** * https://github.com/toptal/chewy[chewy]: An ODM and wrapper for the official Elasticsearch client. @@ -219,10 +225,12 @@ Also see the {client}/rust-api/current/index.html[official Elasticsearch Rust client]. * https://github.com/benashford/rs-es[rs-es]: - A REST API client with a strongly-typed Query DSL. + A REST API client with a strongly-typed Query DSL. + **- Last commit more than a year ago** * https://github.com/elastic-rs/elastic[elastic]: A modular REST API client that supports freeform queries. + **- Last commit more than a year ago** [[scala]] == Scala @@ -231,19 +239,23 @@ client]. Scala DSL. * https://github.com/gphat/wabisabi[wabisabi]: - Asynchronous REST API Scala client. + Asynchronous REST API Scala client. **- No longer maintained** * https://github.com/workday/escalar[escalar]: - Type-safe Scala wrapper for the REST API. + Type-safe Scala wrapper for the REST API. + **- Last commit more than a year ago** * https://github.com/SumoLogic/elasticsearch-client[elasticsearch-client]: Scala DSL that uses the REST API. Akka and AWS helpers included. + **- No longer maintained** + [[smalltalk]] == Smalltalk * https://github.com/newapplesho/elasticsearch-smalltalk[elasticsearch-smalltalk]: - Pharo Smalltalk client for Elasticsearch. + Pharo Smalltalk client for Elasticsearch. + **- Last commit more than a year ago** [[swift]] == Swift @@ -254,4 +266,5 @@ client]. == Vert.x * https://github.com/reactiverse/elasticsearch-client[elasticsearch-client]: - An Elasticsearch client for Eclipse Vert.x. + An Elasticsearch client for Eclipse Vert.x + **- Last commit more than a year ago** \ No newline at end of file From 268ba12b7be5c85878ce4d441c7cf1845cf6d789 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Thu, 22 Feb 2024 12:52:24 +0000 Subject: [PATCH 140/250] [ML] Fix AutodetectMemoryLimitIT.testManyDistinctOverFields (#105727) It seems that the changes of https://github.com/elastic/ml-cpp/pull/2585 combined with the randomness of the test could cause it to fail very occasionally, and by a tiny percentage over the expected upper bound. This change reenables the test by very slightly increasing the upper bound. Fixes #105347 --- .../xpack/ml/integration/AutodetectMemoryLimitIT.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/AutodetectMemoryLimitIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/AutodetectMemoryLimitIT.java index f8542d316dac5..ea97e08dce990 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/AutodetectMemoryLimitIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/AutodetectMemoryLimitIT.java @@ -181,7 +181,6 @@ public void testTooManyByAndOverFields() throws Exception { assertThat(modelSizeStats.getMemoryStatus(), equalTo(ModelSizeStats.MemoryStatus.HARD_LIMIT)); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105347") public void testManyDistinctOverFields() throws Exception { Detector.Builder detector = new Detector.Builder("sum", "value"); detector.setOverFieldName("user"); @@ -226,7 +225,7 @@ public void testManyDistinctOverFields() throws Exception { // Assert we haven't violated the limit too much GetJobsStatsAction.Response.JobStats jobStats = getJobStats(job.getId()).get(0); ModelSizeStats modelSizeStats = jobStats.getModelSizeStats(); - assertThat(modelSizeStats.getModelBytes(), lessThan(120000000L)); + assertThat(modelSizeStats.getModelBytes(), lessThan(120500000L)); assertThat(modelSizeStats.getModelBytes(), greaterThan(90000000L)); assertThat(modelSizeStats.getMemoryStatus(), equalTo(ModelSizeStats.MemoryStatus.HARD_LIMIT)); } From 8daa5eba341b5af9450679d3aa8aeae07efa20e2 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Thu, 22 Feb 2024 12:53:41 +0000 Subject: [PATCH 141/250] [ML] Unmute model deployment BWC tests (#105728) The failure of #103808 might be fixed by the upgrade to PyTorch 2.1.2. It's not good that we have this entire suite muted, so I'll try unmuting for a bit and see if we get any repeat failures. --- .../xpack/restart/MLModelDeploymentFullClusterRestartIT.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/qa/full-cluster-restart/src/javaRestTest/java/org/elasticsearch/xpack/restart/MLModelDeploymentFullClusterRestartIT.java b/x-pack/qa/full-cluster-restart/src/javaRestTest/java/org/elasticsearch/xpack/restart/MLModelDeploymentFullClusterRestartIT.java index b3a0d91d583a9..484e2ed3ac9c3 100644 --- a/x-pack/qa/full-cluster-restart/src/javaRestTest/java/org/elasticsearch/xpack/restart/MLModelDeploymentFullClusterRestartIT.java +++ b/x-pack/qa/full-cluster-restart/src/javaRestTest/java/org/elasticsearch/xpack/restart/MLModelDeploymentFullClusterRestartIT.java @@ -10,7 +10,6 @@ import com.carrotsearch.randomizedtesting.annotations.Name; import org.apache.http.util.EntityUtils; -import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.client.Request; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; @@ -37,7 +36,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; -@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/103808") public class MLModelDeploymentFullClusterRestartIT extends AbstractXpackFullClusterRestartTestCase { // See PyTorchModelIT for how this model was created From c9989708910eca52a99628024b8bd4291dc97393 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Thu, 22 Feb 2024 14:21:59 +0100 Subject: [PATCH 142/250] HNWS -> HNSW --- docs/changelog/105578.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog/105578.yaml b/docs/changelog/105578.yaml index 5b7ebd250be0e..d7420096cb178 100644 --- a/docs/changelog/105578.yaml +++ b/docs/changelog/105578.yaml @@ -8,6 +8,6 @@ highlight: title: "New Lucene 9.10 release" body: |- - https://github.com/apache/lucene/pull/13090: Prevent humongous allocations in ScalarQuantizer when building quantiles. - - https://github.com/apache/lucene/pull/12962: Speedup concurrent multi-segment HNWS graph search - - https://github.com/apache/lucene/pull/13033: PointRangeQuery now exits earlier on segments whose values don't intersect with the query range. When a PointRangeQuery is a required clause of a boolean query, this helps save work on other required clauses of the same boolean query. - - https://github.com/apache/lucene/pull/13026: Propagate minimum competitive score in ReqOptSumScorer. + - https://github.com/apache/lucene/pull/12962: Speedup concurrent multi-segment HNSW graph search + - https://github.com/apache/lucene/pull/13033: Range queries on numeric/date/ip fields now exit earlier on segments whose values don't intersect with the query range. This should especially help when there are other required clauses in the `bool` query and when the range filter is narrow, e.g. filtering on the last 5 minutes. + - https://github.com/apache/lucene/pull/13026: `bool` queries that mix `filter` and `should` clauses will now propagate minimum competitive scores through the `should` clauses. This should yield speedups when sorting by descending score. From d046a5c15c65f5980a2f33688dc40569c36045a5 Mon Sep 17 00:00:00 2001 From: Tim Grein Date: Thu, 22 Feb 2024 15:03:10 +0100 Subject: [PATCH 143/250] [Connectors Secrets API] Add missing public method docs (#105737) --- .../secrets/ConnectorSecretsIndexService.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsIndexService.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsIndexService.java index cc25b8e5317d4..ba9a3e78281dd 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsIndexService.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/ConnectorSecretsIndexService.java @@ -76,6 +76,12 @@ public static SystemIndexDescriptor getSystemIndexDescriptor() { .build(); } + /** + * Gets the secret from the underlying index with the specified id. + * + * @param id The id of the secret. + * @param listener The action listener to invoke on response/failure. + */ public void getSecret(String id, ActionListener listener) { clientWithOrigin.prepareGet(CONNECTOR_SECRETS_INDEX_NAME, id).execute(listener.delegateFailureAndWrap((delegate, getResponse) -> { if (getResponse.isSourceEmpty()) { @@ -86,6 +92,12 @@ public void getSecret(String id, ActionListener list })); } + /** + * Creates a secret in the underlying index with an auto-generated doc ID. + * + * @param request Request for creating the secret. + * @param listener The action listener to invoke on response/failure. + */ public void createSecret(PostConnectorSecretRequest request, ActionListener listener) { try { clientWithOrigin.prepareIndex(CONNECTOR_SECRETS_INDEX_NAME) @@ -100,6 +112,12 @@ public void createSecret(PostConnectorSecretRequest request, ActionListener listener) { String connectorSecretId = request.id(); @@ -119,6 +137,12 @@ public void createSecretWithDocId(PutConnectorSecretRequest request, ActionListe } } + /** + * Deletes the secret in the underlying index with the specified doc ID. + * + * @param id The id of the secret to delete. + * @param listener The action listener to invoke on response/failure. + */ public void deleteSecret(String id, ActionListener listener) { try { clientWithOrigin.prepareDelete(CONNECTOR_SECRETS_INDEX_NAME, id) From 98a7ca558fdee4c18c15ed8fe89163331c4ee817 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Thu, 22 Feb 2024 14:28:52 +0000 Subject: [PATCH 144/250] [ML] Unmute KDETests.testCdfAndSf (#105735) This test was supposed to be fixed by #102878, however, the test was not unmuted in that PR. Relates #102876 --- .../org/elasticsearch/xpack/ml/aggs/changepoint/KDETests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/changepoint/KDETests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/changepoint/KDETests.java index e4d30912050e3..46b563a15c89c 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/changepoint/KDETests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/aggs/changepoint/KDETests.java @@ -23,7 +23,6 @@ public void testEmpty() { assertThat(kde.data(), equalTo(new double[0])); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/102876") public void testCdfAndSf() { double[] data = DoubleStream.generate(() -> randomDoubleBetween(0.0, 100.0, true)).limit(101).toArray(); From 6073e748a3f5d7b77385a1b5326e54f3c4b5caf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Thu, 22 Feb 2024 15:54:03 +0100 Subject: [PATCH 145/250] [DOCS] Adds more detail on disk usage of kNN quantized vectors (#105724) Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Co-authored-by: Benjamin Trent --- docs/reference/how-to/knn-search.asciidoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/reference/how-to/knn-search.asciidoc b/docs/reference/how-to/knn-search.asciidoc index 15e3ff7c38e86..bfe99ad615c47 100644 --- a/docs/reference/how-to/knn-search.asciidoc +++ b/docs/reference/how-to/knn-search.asciidoc @@ -17,7 +17,9 @@ The default <> is `float`. But this can be automatically quantized during index time through <>. Quantization will reduce the required memory by 4x, but it will also reduce the precision of the vectors and -increase disk usage for the field (by up to 25%). +increase disk usage for the field (by up to 25%). Increased disk usage is a +result of {es} storing both the quantized and the unquantized vectors. +For example, when quantizing 40GB of floating point vectors an extra 10GB of data will be stored for the quantized vectors. The total disk usage amounts to 50GB, but the memory usage for fast search will be reduced to 10GB. For `float` vectors with `dim` greater than or equal to `384`, using a <> index is highly recommended. From b650051595fa2551158bb2f3c7b9f2bdbea1fa9b Mon Sep 17 00:00:00 2001 From: Volodymyr Krasnikov <129072588+volodk85@users.noreply.github.com> Date: Thu, 22 Feb 2024 07:01:39 -0800 Subject: [PATCH 146/250] =?UTF-8?q?Update=20PersistentTasksService=20start?= =?UTF-8?q?/stop=20methods=20with=20optional=20par=E2=80=A6=20(#105676)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Overload PersistentTasksService start/stop methods with optional parameter * Fix calls to PersistentTasksService start/stop methods * missed test file * plus IT test --- .../geoip/GeoIpDownloaderTaskExecutor.java | 3 +- ...PersistentTaskInitializationFailureIT.java | 1 + .../PersistentTasksExecutorFullRestartIT.java | 2 +- .../persistent/PersistentTasksExecutorIT.java | 26 +++++----- .../decider/EnableAssignmentDeciderIT.java | 1 + .../TransportPostFeatureUpgradeAction.java | 1 + .../selection/HealthNodeTaskExecutor.java | 1 + .../persistent/AllocatedPersistentTask.java | 5 +- .../PersistentTasksNodeService.java | 3 +- .../persistent/PersistentTasksService.java | 52 +++++++++++++++++-- .../HealthNodeTaskExecutorTests.java | 2 + .../PersistentTasksNodeServiceTests.java | 18 ++++++- .../action/TransportPauseFollowAction.java | 2 +- .../action/TransportResumeFollowAction.java | 8 ++- .../downsample/TransportDownsampleAction.java | 1 + ...rtCancelJobModelSnapshotUpgradeAction.java | 2 +- .../ml/action/TransportCloseJobAction.java | 4 +- .../action/TransportDeleteDatafeedAction.java | 2 +- .../ml/action/TransportDeleteJobAction.java | 2 +- .../ml/action/TransportOpenJobAction.java | 3 +- ...ransportStartDataFrameAnalyticsAction.java | 2 + .../action/TransportStartDatafeedAction.java | 3 +- ...TransportStopDataFrameAnalyticsAction.java | 4 +- .../action/TransportStopDatafeedAction.java | 4 +- ...ransportUpgradeJobModelSnapshotAction.java | 29 ++++++----- .../action/TransportPutRollupJobAction.java | 1 + .../action/PutJobStateMachineTests.java | 10 ++-- .../xpack/shutdown/NodeShutdownTasksIT.java | 2 +- .../action/TransportStartTransformAction.java | 3 +- .../action/TransportStopTransformAction.java | 4 +- 30 files changed, 145 insertions(+), 56 deletions(-) diff --git a/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderTaskExecutor.java b/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderTaskExecutor.java index 615d1c37bf0cf..299e55d4d60a8 100644 --- a/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderTaskExecutor.java +++ b/modules/ingest-geoip/src/main/java/org/elasticsearch/ingest/geoip/GeoIpDownloaderTaskExecutor.java @@ -345,6 +345,7 @@ private void startTask(Runnable onFailure) { GEOIP_DOWNLOADER, GEOIP_DOWNLOADER, new GeoIpTaskParams(), + null, ActionListener.wrap(r -> logger.debug("Started geoip downloader task"), e -> { Throwable t = e instanceof RemoteTransportException ? e.getCause() : e; if (t instanceof ResourceAlreadyExistsException == false) { @@ -366,7 +367,7 @@ private void stopTask(Runnable onFailure) { } } ); - persistentTasksService.sendRemoveRequest(GEOIP_DOWNLOADER, ActionListener.runAfter(listener, () -> { + persistentTasksService.sendRemoveRequest(GEOIP_DOWNLOADER, null, ActionListener.runAfter(listener, () -> { IndexAbstraction databasesAbstraction = clusterService.state().metadata().getIndicesLookup().get(DATABASES_INDEX); if (databasesAbstraction != null) { // regardless of whether DATABASES_INDEX is an alias, resolve it to a concrete index diff --git a/server/src/internalClusterTest/java/org/elasticsearch/persistent/PersistentTaskInitializationFailureIT.java b/server/src/internalClusterTest/java/org/elasticsearch/persistent/PersistentTaskInitializationFailureIT.java index b8d9d4a184f06..ec193a37eeab7 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/persistent/PersistentTaskInitializationFailureIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/persistent/PersistentTaskInitializationFailureIT.java @@ -49,6 +49,7 @@ public void testPersistentTasksThatFailDuringInitializationAreRemovedFromCluster UUIDs.base64UUID(), FailingInitializationPersistentTaskExecutor.TASK_NAME, new FailingInitializationTaskParams(), + null, startPersistentTaskFuture ); startPersistentTaskFuture.actionGet(); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/persistent/PersistentTasksExecutorFullRestartIT.java b/server/src/internalClusterTest/java/org/elasticsearch/persistent/PersistentTasksExecutorFullRestartIT.java index d1c72a9650b85..73c9495a2cd2f 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/persistent/PersistentTasksExecutorFullRestartIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/persistent/PersistentTasksExecutorFullRestartIT.java @@ -44,7 +44,7 @@ public void testFullClusterRestart() throws Exception { PlainActionFuture> future = new PlainActionFuture<>(); futures.add(future); taskIds[i] = UUIDs.base64UUID(); - service.sendStartRequest(taskIds[i], TestPersistentTasksExecutor.NAME, new TestParams("Blah"), future); + service.sendStartRequest(taskIds[i], TestPersistentTasksExecutor.NAME, new TestParams("Blah"), null, future); } for (int i = 0; i < numberOfTasks; i++) { diff --git a/server/src/internalClusterTest/java/org/elasticsearch/persistent/PersistentTasksExecutorIT.java b/server/src/internalClusterTest/java/org/elasticsearch/persistent/PersistentTasksExecutorIT.java index 3cc90a6795e37..813c06d9f02f3 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/persistent/PersistentTasksExecutorIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/persistent/PersistentTasksExecutorIT.java @@ -68,7 +68,7 @@ public static class WaitForPersistentTaskFuture> future = new PlainActionFuture<>(); - persistentTasksService.sendStartRequest(UUIDs.base64UUID(), TestPersistentTasksExecutor.NAME, new TestParams("Blah"), future); + persistentTasksService.sendStartRequest(UUIDs.base64UUID(), TestPersistentTasksExecutor.NAME, new TestParams("Blah"), null, future); long allocationId = future.get().getAllocationId(); waitForTaskToStart(); TaskInfo firstRunningTask = clusterAdmin().prepareListTasks() @@ -99,7 +99,7 @@ public void testPersistentActionCompletion() throws Exception { PersistentTasksService persistentTasksService = internalCluster().getInstance(PersistentTasksService.class); PlainActionFuture> future = new PlainActionFuture<>(); String taskId = UUIDs.base64UUID(); - persistentTasksService.sendStartRequest(taskId, TestPersistentTasksExecutor.NAME, new TestParams("Blah"), future); + persistentTasksService.sendStartRequest(taskId, TestPersistentTasksExecutor.NAME, new TestParams("Blah"), null, future); long allocationId = future.get().getAllocationId(); waitForTaskToStart(); TaskInfo firstRunningTask = clusterAdmin().prepareListTasks() @@ -118,7 +118,7 @@ public void testPersistentActionCompletion() throws Exception { logger.info("Simulating errant completion notification"); // try sending completion request with incorrect allocation id PlainActionFuture> failedCompletionNotificationFuture = new PlainActionFuture<>(); - persistentTasksService.sendCompletionRequest(taskId, Long.MAX_VALUE, null, null, failedCompletionNotificationFuture); + persistentTasksService.sendCompletionRequest(taskId, Long.MAX_VALUE, null, null, null, failedCompletionNotificationFuture); assertFutureThrows(failedCompletionNotificationFuture, ResourceNotFoundException.class); // Make sure that the task is still running assertThat( @@ -140,7 +140,7 @@ public void testPersistentActionWithNoAvailableNode() throws Exception { PlainActionFuture> future = new PlainActionFuture<>(); TestParams testParams = new TestParams("Blah"); testParams.setExecutorNodeAttr("test"); - persistentTasksService.sendStartRequest(UUIDs.base64UUID(), TestPersistentTasksExecutor.NAME, testParams, future); + persistentTasksService.sendStartRequest(UUIDs.base64UUID(), TestPersistentTasksExecutor.NAME, testParams, null, future); String taskId = future.get().getId(); Settings nodeSettings = Settings.builder().put(nodeSettings(0, Settings.EMPTY)).put("node.attr.test_attr", "test").build(); @@ -164,7 +164,7 @@ public void testPersistentActionWithNoAvailableNode() throws Exception { // Remove the persistent task PlainActionFuture> removeFuture = new PlainActionFuture<>(); - persistentTasksService.sendRemoveRequest(taskId, removeFuture); + persistentTasksService.sendRemoveRequest(taskId, null, removeFuture); assertEquals(removeFuture.get().getId(), taskId); } @@ -181,7 +181,7 @@ public void testPersistentActionWithNonClusterStateCondition() throws Exception PersistentTasksService persistentTasksService = internalCluster().getInstance(PersistentTasksService.class); PlainActionFuture> future = new PlainActionFuture<>(); TestParams testParams = new TestParams("Blah"); - persistentTasksService.sendStartRequest(UUIDs.base64UUID(), TestPersistentTasksExecutor.NAME, testParams, future); + persistentTasksService.sendStartRequest(UUIDs.base64UUID(), TestPersistentTasksExecutor.NAME, testParams, null, future); String taskId = future.get().getId(); assertThat(clusterAdmin().prepareListTasks().setActions(TestPersistentTasksExecutor.NAME + "[c]").get().getTasks(), empty()); @@ -196,14 +196,14 @@ public void testPersistentActionWithNonClusterStateCondition() throws Exception // Remove the persistent task PlainActionFuture> removeFuture = new PlainActionFuture<>(); - persistentTasksService.sendRemoveRequest(taskId, removeFuture); + persistentTasksService.sendRemoveRequest(taskId, null, removeFuture); assertEquals(removeFuture.get().getId(), taskId); } public void testPersistentActionStatusUpdate() throws Exception { PersistentTasksService persistentTasksService = internalCluster().getInstance(PersistentTasksService.class); PlainActionFuture> future = new PlainActionFuture<>(); - persistentTasksService.sendStartRequest(UUIDs.base64UUID(), TestPersistentTasksExecutor.NAME, new TestParams("Blah"), future); + persistentTasksService.sendStartRequest(UUIDs.base64UUID(), TestPersistentTasksExecutor.NAME, new TestParams("Blah"), null, future); String taskId = future.get().getId(); waitForTaskToStart(); TaskInfo firstRunningTask = clusterAdmin().prepareListTasks() @@ -249,7 +249,7 @@ public void testPersistentActionStatusUpdate() throws Exception { assertFutureThrows(future1, IllegalStateException.class, "timed out after 10ms"); PlainActionFuture> failedUpdateFuture = new PlainActionFuture<>(); - persistentTasksService.sendUpdateStateRequest(taskId, -2, new State("should fail"), failedUpdateFuture); + persistentTasksService.sendUpdateStateRequest(taskId, -2, new State("should fail"), null, failedUpdateFuture); assertFutureThrows( failedUpdateFuture, ResourceNotFoundException.class, @@ -274,11 +274,11 @@ public void testCreatePersistentTaskWithDuplicateId() throws Exception { PersistentTasksService persistentTasksService = internalCluster().getInstance(PersistentTasksService.class); PlainActionFuture> future = new PlainActionFuture<>(); String taskId = UUIDs.base64UUID(); - persistentTasksService.sendStartRequest(taskId, TestPersistentTasksExecutor.NAME, new TestParams("Blah"), future); + persistentTasksService.sendStartRequest(taskId, TestPersistentTasksExecutor.NAME, new TestParams("Blah"), null, future); future.get(); PlainActionFuture> future2 = new PlainActionFuture<>(); - persistentTasksService.sendStartRequest(taskId, TestPersistentTasksExecutor.NAME, new TestParams("Blah"), future2); + persistentTasksService.sendStartRequest(taskId, TestPersistentTasksExecutor.NAME, new TestParams("Blah"), null, future2); assertFutureThrows(future2, ResourceAlreadyExistsException.class); waitForTaskToStart(); @@ -314,7 +314,7 @@ public void testUnassignRunningPersistentTask() throws Exception { PlainActionFuture> future = new PlainActionFuture<>(); TestParams testParams = new TestParams("Blah"); testParams.setExecutorNodeAttr("test"); - persistentTasksService.sendStartRequest(UUIDs.base64UUID(), TestPersistentTasksExecutor.NAME, testParams, future); + persistentTasksService.sendStartRequest(UUIDs.base64UUID(), TestPersistentTasksExecutor.NAME, testParams, null, future); PersistentTask task = future.get(); String taskId = task.getId(); @@ -365,7 +365,7 @@ public void testAbortLocally() throws Exception { persistentTasksClusterService.setRecheckInterval(TimeValue.timeValueMillis(1)); PersistentTasksService persistentTasksService = internalCluster().getInstance(PersistentTasksService.class); PlainActionFuture> future = new PlainActionFuture<>(); - persistentTasksService.sendStartRequest(UUIDs.base64UUID(), TestPersistentTasksExecutor.NAME, new TestParams("Blah"), future); + persistentTasksService.sendStartRequest(UUIDs.base64UUID(), TestPersistentTasksExecutor.NAME, new TestParams("Blah"), null, future); String taskId = future.get().getId(); long allocationId = future.get().getAllocationId(); waitForTaskToStart(); diff --git a/server/src/internalClusterTest/java/org/elasticsearch/persistent/decider/EnableAssignmentDeciderIT.java b/server/src/internalClusterTest/java/org/elasticsearch/persistent/decider/EnableAssignmentDeciderIT.java index d9aa15ed6e2f5..e7d23f97fc992 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/persistent/decider/EnableAssignmentDeciderIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/persistent/decider/EnableAssignmentDeciderIT.java @@ -51,6 +51,7 @@ public void testEnableAssignmentAfterRestart() throws Exception { "task_" + i, TestPersistentTasksExecutor.NAME, new TestParams(randomAlphaOfLength(10)), + null, ActionListener.running(latch::countDown) ); } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/migration/TransportPostFeatureUpgradeAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/migration/TransportPostFeatureUpgradeAction.java index bd5114bf91ed2..281e26a44f335 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/migration/TransportPostFeatureUpgradeAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/migration/TransportPostFeatureUpgradeAction.java @@ -95,6 +95,7 @@ protected void masterOperation( SYSTEM_INDEX_UPGRADE_TASK_NAME, SYSTEM_INDEX_UPGRADE_TASK_NAME, new SystemIndexMigrationTaskParams(), + null, ActionListener.wrap(startedTask -> { listener.onResponse(new PostFeatureUpgradeResponse(true, featuresToMigrate, null, null)); }, ex -> { diff --git a/server/src/main/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutor.java b/server/src/main/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutor.java index f183d8c7f1a82..209bb45891dea 100644 --- a/server/src/main/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutor.java +++ b/server/src/main/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutor.java @@ -164,6 +164,7 @@ void startTask(ClusterChangedEvent event) { TASK_NAME, TASK_NAME, new HealthNodeTaskParams(), + null, ActionListener.wrap(r -> logger.debug("Created the health node task"), e -> { Throwable t = e instanceof RemoteTransportException ? e.getCause() : e; if (t instanceof ResourceAlreadyExistsException == false) { diff --git a/server/src/main/java/org/elasticsearch/persistent/AllocatedPersistentTask.java b/server/src/main/java/org/elasticsearch/persistent/AllocatedPersistentTask.java index 0c003cda697d1..895fe65b92246 100644 --- a/server/src/main/java/org/elasticsearch/persistent/AllocatedPersistentTask.java +++ b/server/src/main/java/org/elasticsearch/persistent/AllocatedPersistentTask.java @@ -64,7 +64,7 @@ public void updatePersistentTaskState( final PersistentTaskState state, final ActionListener> listener ) { - persistentTasksService.sendUpdateStateRequest(persistentTaskId, allocationId, state, listener); + persistentTasksService.sendUpdateStateRequest(persistentTaskId, allocationId, state, null, listener); } public String getPersistentTaskId() { @@ -200,7 +200,8 @@ private void completeAndNotifyIfNeeded(@Nullable Exception failure, @Nullable St getAllocationId(), failure, localAbortReason, - new ActionListener>() { + null, + new ActionListener<>() { @Override public void onResponse(PersistentTasksCustomMetadata.PersistentTask persistentTask) { logger.trace("notification for task [{}] with id [{}] was successful", getAction(), getPersistentTaskId()); diff --git a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksNodeService.java b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksNodeService.java index 63c97685c913e..b1deee6b21e0e 100644 --- a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksNodeService.java +++ b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksNodeService.java @@ -265,6 +265,7 @@ private void notifyMasterOfFailedTask( taskInProgress.getAllocationId(), originalException, null, + null, new ActionListener<>() { @Override public void onResponse(PersistentTask persistentTask) { @@ -300,7 +301,7 @@ private void cancelTask(Long allocationId) { if (task.markAsCancelled()) { // Cancel the local task using the task manager String reason = "task has been removed, cancelling locally"; - persistentTasksService.sendCancelRequest(task.getId(), reason, new ActionListener<>() { + persistentTasksService.sendCancelRequest(task.getId(), reason, null, new ActionListener<>() { @Override public void onResponse(ListTasksResponse cancelTasksResponse) { logger.trace( diff --git a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksService.java b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksService.java index 227569341919a..9fcf34048c030 100644 --- a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksService.java +++ b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksService.java @@ -52,15 +52,32 @@ public PersistentTasksService(ClusterService clusterService, ThreadPool threadPo /** * Notifies the master node to create new persistent task and to assign it to a node. */ + @Deprecated(forRemoval = true) public void sendStartRequest( final String taskId, final String taskName, final Params taskParams, final ActionListener> listener + ) { + sendStartRequest(taskId, taskName, taskParams, null, listener); + } + + /** + * Notifies the master node to create new persistent task and to assign it to a node. Accepts operation timeout as optional parameter + */ + public void sendStartRequest( + final String taskId, + final String taskName, + final Params taskParams, + final @Nullable TimeValue timeout, + final ActionListener> listener ) { @SuppressWarnings("unchecked") final ActionListener> wrappedListener = listener.map(t -> (PersistentTask) t); StartPersistentTaskAction.Request request = new StartPersistentTaskAction.Request(taskId, taskName, taskParams); + if (timeout != null) { + request.masterNodeTimeout(timeout); + } execute(request, StartPersistentTaskAction.INSTANCE, wrappedListener); } @@ -70,12 +87,14 @@ public void sendStartRequest( * At most one of {@code failure} and {@code localAbortReason} may be * provided. When both {@code failure} and {@code localAbortReason} are * {@code null}, the persistent task is considered as successfully completed. + * Accepts operation timeout as optional parameter */ public void sendCompletionRequest( final String taskId, final long taskAllocationId, final @Nullable Exception taskFailure, final @Nullable String localAbortReason, + final @Nullable TimeValue timeout, final ActionListener> listener ) { CompletionPersistentTaskAction.Request request = new CompletionPersistentTaskAction.Request( @@ -84,16 +103,27 @@ public void sendCompletionRequest( taskFailure, localAbortReason ); + if (timeout != null) { + request.masterNodeTimeout(timeout); + } execute(request, CompletionPersistentTaskAction.INSTANCE, listener); } /** - * Cancels a locally running task using the Task Manager API + * Cancels a locally running task using the Task Manager API. Accepts operation timeout as optional parameter */ - void sendCancelRequest(final long taskId, final String reason, final ActionListener listener) { + void sendCancelRequest( + final long taskId, + final String reason, + final @Nullable TimeValue timeout, + final ActionListener listener + ) { CancelTasksRequest request = new CancelTasksRequest(); request.setTargetTaskId(new TaskId(clusterService.localNode().getId(), taskId)); request.setReason(reason); + if (timeout != null) { + request.setTimeout(timeout); + } try { client.admin().cluster().cancelTasks(request, listener); } catch (Exception e) { @@ -105,12 +135,14 @@ void sendCancelRequest(final long taskId, final String reason, final ActionListe * Notifies the master node that the state of a persistent task has changed. *

    * Persistent task implementers shouldn't call this method directly and use - * {@link AllocatedPersistentTask#updatePersistentTaskState} instead + * {@link AllocatedPersistentTask#updatePersistentTaskState} instead. + * Accepts operation timeout as optional parameter */ void sendUpdateStateRequest( final String taskId, final long taskAllocationID, final PersistentTaskState taskState, + final @Nullable TimeValue timeout, final ActionListener> listener ) { UpdatePersistentTaskStatusAction.Request request = new UpdatePersistentTaskStatusAction.Request( @@ -118,14 +150,24 @@ void sendUpdateStateRequest( taskAllocationID, taskState ); + if (timeout != null) { + request.masterNodeTimeout(timeout); + } execute(request, UpdatePersistentTaskStatusAction.INSTANCE, listener); } /** - * Notifies the master node to remove a persistent task from the cluster state + * Notifies the master node to remove a persistent task from the cluster state. Accepts operation timeout as optional parameter */ - public void sendRemoveRequest(final String taskId, final ActionListener> listener) { + public void sendRemoveRequest( + final String taskId, + final @Nullable TimeValue timeout, + final ActionListener> listener + ) { RemovePersistentTaskAction.Request request = new RemovePersistentTaskAction.Request(taskId); + if (timeout != null) { + request.masterNodeTimeout(timeout); + } execute(request, RemovePersistentTaskAction.INSTANCE, listener); } diff --git a/server/src/test/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutorTests.java b/server/src/test/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutorTests.java index 5460a45569d71..72a9a36f26e70 100644 --- a/server/src/test/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutorTests.java +++ b/server/src/test/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutorTests.java @@ -102,6 +102,7 @@ public void testTaskCreation() throws Exception { eq("health-node"), eq("health-node"), eq(new HealthNodeTaskParams()), + eq(null), any() ) ); @@ -120,6 +121,7 @@ public void testSkippingTaskCreationIfItExists() { eq("health-node"), eq("health-node"), eq(new HealthNodeTaskParams()), + eq(null), any() ); } diff --git a/server/src/test/java/org/elasticsearch/persistent/PersistentTasksNodeServiceTests.java b/server/src/test/java/org/elasticsearch/persistent/PersistentTasksNodeServiceTests.java index 4bc37ea380bfd..8178505470d9a 100644 --- a/server/src/test/java/org/elasticsearch/persistent/PersistentTasksNodeServiceTests.java +++ b/server/src/test/java/org/elasticsearch/persistent/PersistentTasksNodeServiceTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.persistent.PersistentTasksCustomMetadata.Assignment; import org.elasticsearch.persistent.PersistentTasksCustomMetadata.PersistentTask; import org.elasticsearch.persistent.TestPersistentTasksPlugin.TestParams; @@ -258,7 +259,12 @@ public void testTaskCancellation() { when(client.settings()).thenReturn(Settings.EMPTY); PersistentTasksService persistentTasksService = new PersistentTasksService(null, null, client) { @Override - void sendCancelRequest(final long taskId, final String reason, final ActionListener listener) { + void sendCancelRequest( + final long taskId, + final String reason, + final TimeValue timeout, + final ActionListener listener + ) { capturedTaskId.set(taskId); capturedListener.set(listener); } @@ -269,6 +275,7 @@ public void sendCompletionRequest( final long taskAllocationId, final Exception taskFailure, final String localAbortReason, + final TimeValue timeout, final ActionListener> listener ) { fail("Shouldn't be called during Cluster State cancellation"); @@ -348,7 +355,12 @@ public void testTaskLocalAbort() { when(client.settings()).thenReturn(Settings.EMPTY); PersistentTasksService persistentTasksService = new PersistentTasksService(null, null, client) { @Override - void sendCancelRequest(final long taskId, final String reason, final ActionListener listener) { + void sendCancelRequest( + final long taskId, + final String reason, + final TimeValue timeout, + final ActionListener listener + ) { fail("Shouldn't be called during local abort"); } @@ -358,6 +370,7 @@ public void sendCompletionRequest( final long taskAllocationId, final Exception taskFailure, final String localAbortReason, + final TimeValue timeout, final ActionListener> listener ) { assertThat(taskId, not(nullValue())); @@ -466,6 +479,7 @@ public void sendCompletionRequest( long taskAllocationId, Exception taskFailure, String localAbortReason, + TimeValue timeout, ActionListener> listener ) { assertThat(taskFailure, instanceOf(RuntimeException.class)); diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportPauseFollowAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportPauseFollowAction.java index 6989abdf1de01..99c532f3b077f 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportPauseFollowAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportPauseFollowAction.java @@ -98,7 +98,7 @@ protected void masterOperation( final ResponseHandler responseHandler = new ResponseHandler(shardFollowTaskIds.size(), listener); for (String taskId : shardFollowTaskIds) { final int taskSlot = i++; - persistentTasksService.sendRemoveRequest(taskId, responseHandler.getActionListener(taskSlot)); + persistentTasksService.sendRemoveRequest(taskId, null, responseHandler.getActionListener(taskSlot)); } } diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportResumeFollowAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportResumeFollowAction.java index 2e8ee39111ab7..f13ac2f2845b0 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportResumeFollowAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportResumeFollowAction.java @@ -202,7 +202,13 @@ void start( followIndexMetadata, filteredHeaders ); - persistentTasksService.sendStartRequest(taskId, ShardFollowTask.NAME, shardFollowTask, handler.getActionListener(shardId)); + persistentTasksService.sendStartRequest( + taskId, + ShardFollowTask.NAME, + shardFollowTask, + null, + handler.getActionListener(shardId) + ); } } diff --git a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java index 74e77c2896588..5debe5d2edfc9 100644 --- a/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java +++ b/x-pack/plugin/downsample/src/main/java/org/elasticsearch/xpack/downsample/TransportDownsampleAction.java @@ -448,6 +448,7 @@ public void onFailure(Exception e) { persistentTaskId, DownsampleShardTask.TASK_NAME, params, + null, ActionListener.wrap( startedTask -> persistentTasksService.waitForPersistentTaskCondition( startedTask.getId(), diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCancelJobModelSnapshotUpgradeAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCancelJobModelSnapshotUpgradeAction.java index 1d6692f533b9c..41b146f1d9adb 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCancelJobModelSnapshotUpgradeAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCancelJobModelSnapshotUpgradeAction.java @@ -103,7 +103,7 @@ private void removePersistentTasks( final AtomicArray failures = new AtomicArray<>(numberOfTasks); for (PersistentTasksCustomMetadata.PersistentTask task : upgradeTasksToCancel) { - persistentTasksService.sendRemoveRequest(task.getId(), new ActionListener<>() { + persistentTasksService.sendRemoveRequest(task.getId(), null, new ActionListener<>() { @Override public void onResponse(PersistentTasksCustomMetadata.PersistentTask task) { if (counter.incrementAndGet() == numberOfTasks) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java index 02801864a3e78..6b605e0438b43 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportCloseJobAction.java @@ -209,6 +209,7 @@ protected void doExecute(Task task, CloseJobAction.Request request, ActionListen // these persistent tasks to disappear. persistentTasksService.sendRemoveRequest( jobTask.getId(), + null, ActionListener.wrap( r -> logger.trace( () -> format("[%s] removed task to close unassigned job", resolvedJobId) @@ -519,7 +520,7 @@ private void forceCloseJob( PersistentTasksCustomMetadata.PersistentTask jobTask = MlTasks.getJobTask(jobId, tasks); if (jobTask != null) { auditor.info(jobId, Messages.JOB_AUDIT_FORCE_CLOSING); - persistentTasksService.sendRemoveRequest(jobTask.getId(), new ActionListener<>() { + persistentTasksService.sendRemoveRequest(jobTask.getId(), null, new ActionListener<>() { @Override public void onResponse(PersistentTasksCustomMetadata.PersistentTask task) { if (counter.incrementAndGet() == numberOfJobs) { @@ -590,6 +591,7 @@ private void normalCloseJob( PersistentTasksCustomMetadata.PersistentTask jobTask = MlTasks.getJobTask(jobId, tasks); persistentTasksService.sendRemoveRequest( jobTask.getId(), + null, ActionListener.wrap(r -> logger.trace("[{}] removed persistent task for relocated job", jobId), e -> { if (ExceptionsHelper.unwrapCause(e) instanceof ResourceNotFoundException) { logger.debug("[{}] relocated job task already removed", jobId); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDatafeedAction.java index 49c6021a6ed8b..cddddc8d3c245 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDatafeedAction.java @@ -103,7 +103,7 @@ private void removeDatafeedTask(DeleteDatafeedAction.Request request, ClusterSta if (datafeedTask == null) { listener.onResponse(true); } else { - persistentTasksService.sendRemoveRequest(datafeedTask.getId(), new ActionListener<>() { + persistentTasksService.sendRemoveRequest(datafeedTask.getId(), null, new ActionListener<>() { @Override public void onResponse(PersistentTasksCustomMetadata.PersistentTask persistentTask) { listener.onResponse(Boolean.TRUE); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteJobAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteJobAction.java index f694e85144b48..19f99a329d309 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteJobAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteJobAction.java @@ -291,7 +291,7 @@ private void removePersistentTask(String jobId, ClusterState currentState, Actio if (jobTask == null) { listener.onResponse(null); } else { - persistentTasksService.sendRemoveRequest(jobTask.getId(), listener.safeMap(task -> true)); + persistentTasksService.sendRemoveRequest(jobTask.getId(), null, listener.safeMap(task -> true)); } } 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 c527c00a738a2..52e9f93d7d31f 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 @@ -166,6 +166,7 @@ public void onFailure(Exception e) { MlTasks.jobTaskId(jobParams.getJobId()), MlTasks.JOB_TASK_NAME, jobParams, + null, waitForJobToStart ), listener::onFailure @@ -324,7 +325,7 @@ private void cancelJobStart( Exception exception, ActionListener listener ) { - persistentTasksService.sendRemoveRequest(persistentTask.getId(), new ActionListener<>() { + persistentTasksService.sendRemoveRequest(persistentTask.getId(), null, new ActionListener<>() { @Override public void onResponse(PersistentTasksCustomMetadata.PersistentTask task) { // We succeeded in cancelling the persistent task, but the diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java index 5351023a803e7..05f3d6311404a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDataFrameAnalyticsAction.java @@ -211,6 +211,7 @@ public void onFailure(Exception e) { MlTasks.dataFrameAnalyticsTaskId(request.getId()), MlTasks.DATA_FRAME_ANALYTICS_TASK_NAME, taskParams, + null, waitForAnalyticsToStart ); }, listener::onFailure); @@ -602,6 +603,7 @@ private void cancelAnalyticsStart( ) { persistentTasksService.sendRemoveRequest( persistentTask.getId(), + null, new ActionListener>() { @Override public void onResponse(PersistentTasksCustomMetadata.PersistentTask task) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java index e7d5d956bb1b0..2067bae048561 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStartDatafeedAction.java @@ -345,6 +345,7 @@ private void createDataExtractor( MlTasks.datafeedTaskId(params.getDatafeedId()), MlTasks.DATAFEED_TASK_NAME, params, + null, listener ), listener::onFailure @@ -407,7 +408,7 @@ private void cancelDatafeedStart( Exception exception, ActionListener listener ) { - persistentTasksService.sendRemoveRequest(persistentTask.getId(), new ActionListener<>() { + persistentTasksService.sendRemoveRequest(persistentTask.getId(), null, new ActionListener<>() { @Override public void onResponse(PersistentTasksCustomMetadata.PersistentTask task) { // We succeeded in cancelling the persistent task, but the diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDataFrameAnalyticsAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDataFrameAnalyticsAction.java index 42d36006acbde..9ad0213bf7ee5 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDataFrameAnalyticsAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDataFrameAnalyticsAction.java @@ -260,7 +260,7 @@ private void forceStop( for (String analyticsId : nonStoppedAnalytics) { PersistentTasksCustomMetadata.PersistentTask analyticsTask = MlTasks.getDataFrameAnalyticsTask(analyticsId, tasks); if (analyticsTask != null) { - persistentTasksService.sendRemoveRequest(analyticsTask.getId(), ActionListener.wrap(removedTask -> { + persistentTasksService.sendRemoveRequest(analyticsTask.getId(), null, ActionListener.wrap(removedTask -> { auditor.info(analyticsId, Messages.DATA_FRAME_ANALYTICS_AUDIT_FORCE_STOPPED); if (counter.incrementAndGet() == nonStoppedAnalytics.size()) { sendResponseOrFailure(request.getId(), listener, failures); @@ -329,7 +329,7 @@ private String[] findAllocatedNodesAndRemoveUnassignedTasks(List analyti // This means the task has not been assigned to a node yet so // we can stop it by removing its persistent task. // The listener is a no-op as we're already going to wait for the task to be removed. - persistentTasksService.sendRemoveRequest(task.getId(), ActionListener.noop()); + persistentTasksService.sendRemoveRequest(task.getId(), null, ActionListener.noop()); } } return nodes.toArray(new String[0]); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java index 41359f5fcc166..8ba8132ecafa2 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportStopDatafeedAction.java @@ -252,6 +252,7 @@ private void normalStopDatafeed( // already waits for these persistent tasks to disappear. persistentTasksService.sendRemoveRequest( datafeedTask.getId(), + null, ActionListener.wrap( r -> auditDatafeedStopped(datafeedTask), e -> logger.error("[" + datafeedId + "] failed to remove task to stop unassigned datafeed", e) @@ -278,6 +279,7 @@ private void normalStopDatafeed( PersistentTasksCustomMetadata.PersistentTask datafeedTask = MlTasks.getDatafeedTask(datafeedId, tasks); persistentTasksService.sendRemoveRequest( datafeedTask.getId(), + null, ActionListener.wrap(r -> auditDatafeedStopped(datafeedTask), e -> { if (ExceptionsHelper.unwrapCause(e) instanceof ResourceNotFoundException) { logger.debug("[{}] relocated datafeed task already removed", datafeedId); @@ -381,7 +383,7 @@ private void forceStopDatafeed( for (String datafeedId : notStoppedDatafeeds) { PersistentTasksCustomMetadata.PersistentTask datafeedTask = MlTasks.getDatafeedTask(datafeedId, tasks); if (datafeedTask != null) { - persistentTasksService.sendRemoveRequest(datafeedTask.getId(), ActionListener.wrap(persistentTask -> { + persistentTasksService.sendRemoveRequest(datafeedTask.getId(), null, ActionListener.wrap(persistentTask -> { // For force stop, only audit here if the datafeed was unassigned at the time of the stop, hence inactive. // If the datafeed was active then it audits itself on being cancelled. if (PersistentTasksClusterService.needsReassignment(datafeedTask.getAssignment(), nodes)) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportUpgradeJobModelSnapshotAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportUpgradeJobModelSnapshotAction.java index 15c1d53f7bdf8..dea6c53d39ab4 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportUpgradeJobModelSnapshotAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportUpgradeJobModelSnapshotAction.java @@ -164,6 +164,7 @@ protected void masterOperation(Task task, Request request, ClusterState state, A MlTasks.snapshotUpgradeTaskId(params.getJobId(), params.getSnapshotId()), MlTasks.JOB_SNAPSHOT_UPGRADE_TASK_NAME, params, + null, waitForJobToStart ); }, listener::onFailure); @@ -290,18 +291,22 @@ private void cancelJobStart( Exception exception, ActionListener listener ) { - persistentTasksService.sendRemoveRequest(persistentTask.getId(), ActionListener.wrap(t -> listener.onFailure(exception), e -> { - logger.error( - () -> format( - "[%s] [%s] Failed to cancel persistent task that could not be assigned due to %s", - persistentTask.getParams().getJobId(), - persistentTask.getParams().getSnapshotId(), - exception.getMessage() - ), - e - ); - listener.onFailure(exception); - })); + persistentTasksService.sendRemoveRequest( + persistentTask.getId(), + null, + ActionListener.wrap(t -> listener.onFailure(exception), e -> { + logger.error( + () -> format( + "[%s] [%s] Failed to cancel persistent task that could not be assigned due to %s", + persistentTask.getParams().getJobId(), + persistentTask.getParams().getSnapshotId(), + exception.getMessage() + ), + e + ); + listener.onFailure(exception); + }) + ); } } 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 41c2f855ff8c9..e66bb35cce1cf 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 @@ -303,6 +303,7 @@ static void startPersistentTask( job.getConfig().getId(), RollupField.TASK_NAME, job, + null, ActionListener.wrap(rollupConfigPersistentTask -> waitForRollupStarted(job, listener, persistentTasksService), e -> { if (e instanceof ResourceAlreadyExistsException) { e = new ElasticsearchStatusException( diff --git a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/PutJobStateMachineTests.java b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/PutJobStateMachineTests.java index dd6f5173cb6ba..fc5805d7ed9d1 100644 --- a/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/PutJobStateMachineTests.java +++ b/x-pack/plugin/rollup/src/test/java/org/elasticsearch/xpack/rollup/action/PutJobStateMachineTests.java @@ -374,10 +374,11 @@ public void testTaskAlreadyExists() { doAnswer(invocation -> { requestCaptor.getValue().onFailure(new ResourceAlreadyExistsException(job.getConfig().getRollupIndex())); return null; - }).when(tasksService).sendStartRequest(eq(job.getConfig().getId()), eq(RollupField.TASK_NAME), eq(job), requestCaptor.capture()); + }).when(tasksService) + .sendStartRequest(eq(job.getConfig().getId()), eq(RollupField.TASK_NAME), eq(job), eq(null), requestCaptor.capture()); TransportPutRollupJobAction.startPersistentTask(job, testListener, tasksService); - verify(tasksService).sendStartRequest(eq(job.getConfig().getId()), eq(RollupField.TASK_NAME), eq(job), any()); + verify(tasksService).sendStartRequest(eq(job.getConfig().getId()), eq(RollupField.TASK_NAME), eq(job), eq(null), any()); } @SuppressWarnings({ "unchecked", "rawtypes" }) @@ -401,7 +402,8 @@ public void testStartTask() { ); requestCaptor.getValue().onResponse(response); return null; - }).when(tasksService).sendStartRequest(eq(job.getConfig().getId()), eq(RollupField.TASK_NAME), eq(job), requestCaptor.capture()); + }).when(tasksService) + .sendStartRequest(eq(job.getConfig().getId()), eq(RollupField.TASK_NAME), eq(job), eq(null), requestCaptor.capture()); ArgumentCaptor requestCaptor2 = ArgumentCaptor.forClass( PersistentTasksService.WaitForPersistentTaskListener.class @@ -413,7 +415,7 @@ public void testStartTask() { }).when(tasksService).waitForPersistentTaskCondition(eq(job.getConfig().getId()), any(), any(), requestCaptor2.capture()); TransportPutRollupJobAction.startPersistentTask(job, testListener, tasksService); - verify(tasksService).sendStartRequest(eq(job.getConfig().getId()), eq(RollupField.TASK_NAME), eq(job), any()); + verify(tasksService).sendStartRequest(eq(job.getConfig().getId()), eq(RollupField.TASK_NAME), eq(job), eq(null), any()); verify(tasksService).waitForPersistentTaskCondition(eq(job.getConfig().getId()), any(), any(), any()); } diff --git a/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownTasksIT.java b/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownTasksIT.java index 584ee628e81e5..7c32311237c57 100644 --- a/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownTasksIT.java +++ b/x-pack/plugin/shutdown/src/internalClusterTest/java/org/elasticsearch/xpack/shutdown/NodeShutdownTasksIT.java @@ -188,7 +188,7 @@ protected void nodeOperation(AllocatedPersistentTask task, TestTaskParams params private void startTask() { logger.info("--> sending start request"); - persistentTasksService.sendStartRequest("task_id", "task_name", new TestTaskParams(), ActionListener.wrap(r -> {}, e -> { + persistentTasksService.sendStartRequest("task_id", "task_name", new TestTaskParams(), null, ActionListener.wrap(r -> {}, e -> { if (e instanceof ResourceAlreadyExistsException == false) { logger.error("failed to create task", e); fail("failed to create task"); diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStartTransformAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStartTransformAction.java index db24470433003..825d0b8d12119 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStartTransformAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStartTransformAction.java @@ -159,6 +159,7 @@ protected void masterOperation( transformTask.getId(), TransformTaskParams.NAME, transformTask, + null, newPersistentTaskActionListener ); } else { @@ -286,7 +287,7 @@ protected ClusterBlockException checkBlock(StartTransformAction.Request request, } private void cancelTransformTask(String taskId, String transformId, Exception exception, Consumer onFailure) { - persistentTasksService.sendRemoveRequest(taskId, new ActionListener<>() { + persistentTasksService.sendRemoveRequest(taskId, null, new ActionListener<>() { @Override public void onResponse(PersistentTasksCustomMetadata.PersistentTask task) { // We succeeded in canceling the persistent task, but the diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java index 54d33f0df3638..a309aaa2e4e0e 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/action/TransportStopTransformAction.java @@ -513,7 +513,7 @@ private ActionListener cancelTransformTasksWithNoAssignment( ); for (String unassignedTaskId : transformNodeAssignments.getWaitingForAssignment()) { - persistentTasksService.sendRemoveRequest(unassignedTaskId, groupedListener); + persistentTasksService.sendRemoveRequest(unassignedTaskId, null, groupedListener); } }, e -> { @@ -525,7 +525,7 @@ private ActionListener cancelTransformTasksWithNoAssignment( ); for (String unassignedTaskId : transformNodeAssignments.getWaitingForAssignment()) { - persistentTasksService.sendRemoveRequest(unassignedTaskId, groupedListener); + persistentTasksService.sendRemoveRequest(unassignedTaskId, null, groupedListener); } }); return doExecuteListener; From c5b705dfb413769d0fb19c9dbd61442bbf02c472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Thu, 22 Feb 2024 16:09:18 +0100 Subject: [PATCH 147/250] Refactor `node_selector`s: remove redundant ones and change to `current` (#103889) * Remove seemingly redundant node_selector * Remove other un-needed selectors, transformed others in version skips --- .../test/cat.aliases/10_basic.yml | 16 +------- .../indices.get_field_mapping/10_basic.yml | 6 +-- .../test/indices.open/10_basic.yml | 4 +- .../test/indices.put_mapping/10_basic.yml | 4 +- .../test/mixed_cluster/110_enrich.yml | 3 -- .../test/mixed_cluster/120_api_key.yml | 25 +++++++++++- .../test/mixed_cluster/140_user_profile.yml | 38 ++++++++++++++++--- 7 files changed, 63 insertions(+), 33 deletions(-) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cat.aliases/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cat.aliases/10_basic.yml index 96998a2a6218e..9566f6f036c3f 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cat.aliases/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/cat.aliases/10_basic.yml @@ -23,12 +23,9 @@ "Help (pre 7.4.0)": - skip: version: "7.4.0 - " - features: node_selector reason: "is_write_index is shown in cat.aliases starting version 7.4.0" - do: - node_selector: - version: " - 7.3.99" cat.aliases: help: true @@ -85,7 +82,6 @@ "Simple alias (pre 7.4.0)": - skip: version: "7.4.0 - " - features: node_selector reason: "is_write_index is shown in cat.aliases starting version 7.4.0" - do: @@ -98,8 +94,6 @@ name: test_alias - do: - node_selector: - version: " - 7.3.99" cat.aliases: {} - match: @@ -156,7 +150,6 @@ "Complex alias (pre 7.4.0)": - skip: version: "7.4.0 - " - features: node_selector reason: "is_write_index is shown in cat.aliases starting version 7.4.0" - do: @@ -179,8 +172,6 @@ term: foo: bar - do: - node_selector: - version: " - 7.3.99" cat.aliases: {} - match: @@ -317,7 +308,6 @@ "Column headers (pre 7.4.0)": - skip: version: "7.4.0 - " - features: node_selector reason: "is_write_index is shown in cat.aliases starting version 7.4.0" - do: @@ -330,8 +320,6 @@ name: test_1 - do: - node_selector: - version: " - 7.3.99" cat.aliases: v: true @@ -422,7 +410,7 @@ "Alias against closed index (pre 7.4.0)": - skip: version: "7.4.0 - " - features: ["node_selector", "allowed_warnings"] + features: ["allowed_warnings"] reason: "is_write_index is shown in cat.aliases starting version 7.4.0" - do: @@ -439,8 +427,6 @@ - "the default value for the ?wait_for_active_shards parameter will change from '0' to 'index-setting' in version 8; specify '?wait_for_active_shards=index-setting' to adopt the future default behaviour, or '?wait_for_active_shards=0' to preserve today's behaviour" - do: - node_selector: - version: " - 7.3.99" cat.aliases: {} - match: diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.get_field_mapping/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.get_field_mapping/10_basic.yml index 350e9ff37f43b..1878ae0997649 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.get_field_mapping/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.get_field_mapping/10_basic.yml @@ -55,12 +55,12 @@ setup: "Get field mapping with local parameter should fail": - skip: - features: ["warnings", "node_selector"] + features: ["warnings"] + version: " - 7.99.99" + reason: "local parameter for get field mapping API was allowed before v8" - do: catch: bad_request - node_selector: - version: "8.0.0 - " indices.get_field_mapping: fields: text local: true diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.open/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.open/10_basic.yml index 93447612406b9..76dfa552b5630 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.open/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/indices.open/10_basic.yml @@ -127,7 +127,7 @@ - skip: version: " - 7.99.99" reason: "required deprecation warning is only emitted in 8.0 and later" - features: ["warnings", "node_selector"] + features: ["warnings"] - do: indices.create: @@ -140,7 +140,5 @@ indices.close: index: "index_*" wait_for_active_shards: index-setting - node_selector: - version: "8.0.0 - " warnings: - "?wait_for_active_shards=index-setting is now the default behaviour; the 'index-setting' value for this parameter should no longer be used since it will become unsupported in version 9" 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 db3d2f349dcef..fc747f401b11d 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 @@ -87,7 +87,7 @@ --- "Put mappings with explicit _doc type bwc": - skip: - version: "8.0.0 - " + version: "8.0.0 - " #TODO: add "mixed" to skip test for mixed cluster/upgrade tests reason: "old deprecation message for pre 8.0" features: "node_selector" - do: @@ -96,7 +96,7 @@ - do: node_selector: - version: " - 7.99.99" + version: " - 7.99.99" #TODO: OR replace with "non_current" here catch: bad_request indices.put_mapping: index: test_index diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/110_enrich.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/110_enrich.yml index e072e034aebf6..2bafa1142683a 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/110_enrich.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/110_enrich.yml @@ -3,11 +3,8 @@ - skip: version: " - 7.8.99" reason: "Privilege change of enrich stats is backported to 7.9.0" - features: node_selector - do: - node_selector: - version: "7.9.0 - " enrich.stats: {} - length: { coordinator_stats: 3 } diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key.yml index 334a85625a328..8cb9b33a1d0fe 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/120_api_key.yml @@ -23,14 +23,35 @@ --- -"Create API key with metadata in a mixed cluster": +"Create API key with metadata in a mixed cluster (pre 7.13.0)": - skip: features: [headers, node_selector] + version: "7.13.0 - " + reason: "Support metadata on API keys introduced in 7.13.0" - do: node_selector: - version: " 7.13.0 - " + version: "current" + security.create_api_key: + body: > + { + "name": "my-mixed-api-key-2", + "metadata": {"foo": "bar"} + } + - match: { name: "my-mixed-api-key-2" } + - is_true: id + - is_true: api_key + +--- +"Create API key with metadata in a mixed cluster": + + - skip: + features: [headers] + version: " - 7.12.99" + reason: "Support metadata on API keys introduced in 7.13.0" + + - do: security.create_api_key: body: > { diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/140_user_profile.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/140_user_profile.yml index c667c9266b8d5..486f067310511 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/140_user_profile.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/mixed_cluster/140_user_profile.yml @@ -1,14 +1,14 @@ --- -"Test User Profile feature will work in a mixed cluster": +"Test User Profile feature will work in a mixed cluster (pre 8.5.0)": - skip: features: node_selector - version: " - 7.99.99" - reason: "https://github.com/elastic/elasticsearch/issues/86373" + version: " - 7.99.99, 8.5.0 - " + reason: "Does not work pre 8.0 (#86373) and response format changed after 8.5 (#89023)" - do: node_selector: - version: " 8.5.0 - " + version: "current" security.activate_user_profile: body: > { @@ -22,7 +22,35 @@ - do: node_selector: - version: " 8.5.0 - " + version: "current" + security.get_user_profile: + uid: "$profile_uid" + + - length: { profiles : 1 } + - set: { profiles.0 : profile } + - match: { $profile.uid : "$profile_uid" } + - match: { $profile.user.username : "test_user" } + +--- +"Test User Profile feature will work in a mixed cluster": + + - skip: + version: " - 8.4.99" + reason: "response format is changed to support multiple UIDs #89023" + + - do: + security.activate_user_profile: + body: > + { + "grant_type": "password", + "username": "test_user", + "password" : "x-pack-test-password" + } + - is_true: uid + - match: { "user.username" : "test_user" } + - set: { uid: profile_uid } + + - do: security.get_user_profile: uid: "$profile_uid" From f396321d0826f3619df5ec30787e839f28256cb7 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 22 Feb 2024 16:10:03 +0100 Subject: [PATCH 148/250] Reduce InternalAutoDateHistogram in a streaming fashion (#105740) --- .../histogram/InternalAutoDateHistogram.java | 157 +++++++++--------- 1 file changed, 80 insertions(+), 77 deletions(-) diff --git a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/histogram/InternalAutoDateHistogram.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/histogram/InternalAutoDateHistogram.java index f0dfad88c87b4..f0f7984079d97 100644 --- a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/histogram/InternalAutoDateHistogram.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/histogram/InternalAutoDateHistogram.java @@ -7,12 +7,13 @@ */ package org.elasticsearch.aggregations.bucket.histogram; -import org.apache.lucene.util.PriorityQueue; import org.elasticsearch.TransportVersions; import org.elasticsearch.aggregations.bucket.histogram.AutoDateHistogramAggregationBuilder.RoundingInfo; import org.elasticsearch.common.Rounding; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.util.LongObjectPagedHashMap; +import org.elasticsearch.core.Releasables; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.AggregationReduceContext; import org.elasticsearch.search.aggregations.AggregatorReducer; @@ -20,7 +21,7 @@ import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; import org.elasticsearch.search.aggregations.KeyComparable; -import org.elasticsearch.search.aggregations.bucket.IteratorAndCurrent; +import org.elasticsearch.search.aggregations.bucket.MultiBucketAggregatorsReducer; import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; import org.elasticsearch.search.aggregations.bucket.histogram.Histogram; @@ -286,78 +287,6 @@ public Bucket createBucket(InternalAggregations aggregations, Bucket prototype) return new Bucket(prototype.key, prototype.docCount, prototype.format, aggregations); } - /** - * This method works almost exactly the same as - * InternalDateHistogram#reduceBuckets(List, ReduceContext), the different - * here is that we need to round all the keys we see using the highest level - * rounding returned across all the shards so the resolution of the buckets - * is the same and they can be reduced together. - */ - private BucketReduceResult reduceBuckets(List aggregations, AggregationReduceContext reduceContext) { - - // First we need to find the highest level rounding used across all the - // shards - int reduceRoundingIdx = 0; - long min = Long.MAX_VALUE; - long max = Long.MIN_VALUE; - for (InternalAutoDateHistogram agg : aggregations) { - reduceRoundingIdx = Math.max(agg.bucketInfo.roundingIdx, reduceRoundingIdx); - if (false == agg.buckets.isEmpty()) { - min = Math.min(min, agg.buckets.get(0).key); - max = Math.max(max, agg.buckets.get(agg.buckets.size() - 1).key); - } - } - Rounding.Prepared reduceRounding = prepare(reduceRoundingIdx, min, max); - - final PriorityQueue> pq = new PriorityQueue<>(aggregations.size()) { - @Override - protected boolean lessThan(IteratorAndCurrent a, IteratorAndCurrent b) { - return a.current().key < b.current().key; - } - }; - for (InternalAutoDateHistogram histogram : aggregations) { - if (histogram.buckets.isEmpty() == false) { - pq.add(new IteratorAndCurrent<>(histogram.buckets.iterator())); - } - } - - List reducedBuckets = new ArrayList<>(); - if (pq.size() > 0) { - // list of buckets coming from different shards that have the same key - List currentBuckets = new ArrayList<>(); - long key = reduceRounding.round(pq.top().current().key); - - do { - final IteratorAndCurrent top = pq.top(); - - if (reduceRounding.round(top.current().key) != key) { - // the key changes, reduce what we already buffered and reset the buffer for current buckets - final Bucket reduced = reduceBucket(currentBuckets, reduceContext); - reducedBuckets.add(reduced); - currentBuckets.clear(); - key = reduceRounding.round(top.current().key); - } - - currentBuckets.add(top.current()); - - if (top.hasNext()) { - top.next(); - assert top.current().key > key : "shards must return data sorted by key"; - pq.updateTop(); - } else { - pq.pop(); - } - } while (pq.size() > 0); - - if (currentBuckets.isEmpty() == false) { - final Bucket reduced = reduceBucket(currentBuckets, reduceContext); - reducedBuckets.add(reduced); - } - } - - return mergeBucketsIfNeeded(new BucketReduceResult(reducedBuckets, reduceRoundingIdx, 1, reduceRounding, min, max), reduceContext); - } - private BucketReduceResult mergeBucketsIfNeeded(BucketReduceResult firstPassResult, AggregationReduceContext reduceContext) { int idx = firstPassResult.roundingIdx; RoundingInfo info = bucketInfo.roundingInfos[idx]; @@ -505,19 +434,87 @@ static int getAppropriateRounding(long minKey, long maxKey, int roundingIdx, Rou return currentRoundingIdx - 1; } + /** + * This method works almost exactly the same as + * InternalDateHistogram#reduceBuckets(List, ReduceContext), the different + * here is that we need to round all the keys we see using the highest level + * rounding returned across all the shards so the resolution of the buckets + * is the same and they can be reduced together. + */ @Override protected AggregatorReducer getLeaderReducer(AggregationReduceContext reduceContext, int size) { return new AggregatorReducer() { - final List aggregations = new ArrayList<>(size); + private final LongObjectPagedHashMap bucketsReducer = new LongObjectPagedHashMap<>( + getBuckets().size(), + reduceContext.bigArrays() + ); + int reduceRoundingIdx = 0; + long min = Long.MAX_VALUE; + long max = Long.MIN_VALUE; @Override public void accept(InternalAggregation aggregation) { - aggregations.add((InternalAutoDateHistogram) aggregation); + final InternalAutoDateHistogram histogram = (InternalAutoDateHistogram) aggregation; + reduceRoundingIdx = Math.max(histogram.bucketInfo.roundingIdx, reduceRoundingIdx); + if (false == histogram.buckets.isEmpty()) { + min = Math.min(min, histogram.buckets.get(0).key); + max = Math.max(max, histogram.buckets.get(histogram.buckets.size() - 1).key); + for (Bucket bucket : histogram.buckets) { + MultiBucketAggregatorsReducer reducer = bucketsReducer.get(bucket.key); + if (reducer == null) { + reducer = new MultiBucketAggregatorsReducer(reduceContext, size); + bucketsReducer.put(bucket.key, reducer); + } + reducer.accept(bucket); + } + } } @Override public InternalAggregation get() { - BucketReduceResult reducedBucketsResult = reduceBuckets(aggregations, reduceContext); + // First we need to find the highest level rounding used across all the + // shards + final Rounding.Prepared reduceRounding = prepare(reduceRoundingIdx, min, max); + + final long[] keys = new long[(int) bucketsReducer.size()]; + { + // fill the array and sort it + final int[] index = new int[] { 0 }; + bucketsReducer.iterator().forEachRemaining(c -> keys[index[0]++] = c.key); + Arrays.sort(keys); + } + + final List reducedBuckets = new ArrayList<>(); + if (keys.length > 0) { + // list of buckets coming from different shards that have the same key + MultiBucketAggregatorsReducer currentReducer = null; + long key = reduceRounding.round(keys[0]); + for (long top : keys) { + if (reduceRounding.round(top) != key) { + assert currentReducer != null; + // the key changes, reduce what we already buffered and reset the buffer for current buckets + reducedBuckets.add(createBucket(key, currentReducer.getDocCount(), currentReducer.get())); + currentReducer = null; + key = reduceRounding.round(top); + } + + final MultiBucketAggregatorsReducer nextReducer = bucketsReducer.get(top); + if (currentReducer == null) { + currentReducer = nextReducer; + } else { + currentReducer.accept(createBucket(key, nextReducer.getDocCount(), nextReducer.get())); + } + } + + if (currentReducer != null) { + reducedBuckets.add(createBucket(key, currentReducer.getDocCount(), currentReducer.get())); + } + } + + BucketReduceResult reducedBucketsResult = mergeBucketsIfNeeded( + new BucketReduceResult(reducedBuckets, reduceRoundingIdx, 1, reduceRounding, min, max), + reduceContext + ); if (reduceContext.isFinalReduce()) { // adding empty buckets if needed @@ -546,6 +543,12 @@ public InternalAggregation get() { reducedBucketsResult.innerInterval ); } + + @Override + public void close() { + bucketsReducer.iterator().forEachRemaining(c -> Releasables.close(c.value)); + Releasables.close(bucketsReducer); + } }; } From b2d25b72db8c58f50a74d062eb58b2be68e39a98 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Thu, 22 Feb 2024 10:23:11 -0500 Subject: [PATCH 149/250] Removing set once within services (#105713) --- .../xpack/inference/InferencePlugin.java | 10 +- .../inference/services/SenderService.java | 21 +-- .../services/cohere/CohereService.java | 3 +- .../huggingface/HuggingFaceBaseService.java | 3 +- .../huggingface/HuggingFaceService.java | 3 +- .../elser/HuggingFaceElserService.java | 3 +- .../services/openai/OpenAiService.java | 3 +- .../services/SenderServiceTests.java | 7 +- .../services/cohere/CohereServiceTests.java | 166 ++++------------- .../HuggingFaceBaseServiceTests.java | 5 +- .../huggingface/HuggingFaceServiceTests.java | 130 +++---------- .../services/openai/OpenAiServiceTests.java | 176 ++++-------------- 12 files changed, 115 insertions(+), 415 deletions(-) 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 eb43941cdcac2..eb11ebe782216 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 @@ -161,6 +161,8 @@ public Collection createComponents(PluginServices services) { inferenceServices.add(this::getInferenceServiceFactories); var factoryContext = new InferenceServiceExtension.InferenceServiceFactoryContext(services.client()); + // This must be done after the HttpRequestSenderFactory is created so that the services can get the + // reference correctly var inferenceRegistry = new InferenceServiceRegistryImpl(inferenceServices, factoryContext); inferenceRegistry.init(services.client()); inferenceServiceRegistry.set(inferenceRegistry); @@ -178,10 +180,10 @@ public void loadExtensions(ExtensionLoader loader) { public List getInferenceServiceFactories() { return List.of( ElserInternalService::new, - context -> new HuggingFaceElserService(httpFactory, serviceComponents), - context -> new HuggingFaceService(httpFactory, serviceComponents), - context -> new OpenAiService(httpFactory, serviceComponents), - context -> new CohereService(httpFactory, serviceComponents), + context -> new HuggingFaceElserService(httpFactory.get(), serviceComponents.get()), + context -> new HuggingFaceService(httpFactory.get(), serviceComponents.get()), + context -> new OpenAiService(httpFactory.get(), serviceComponents.get()), + context -> new CohereService(httpFactory.get(), serviceComponents.get()), TextEmbeddingInternalService::new ); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/SenderService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/SenderService.java index fa329e37fb0e2..96378b109ae2d 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/SenderService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/SenderService.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.services; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.action.ActionListener; import org.elasticsearch.core.IOUtils; import org.elasticsearch.inference.ChunkedInferenceServiceResults; @@ -23,24 +22,23 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; public abstract class SenderService implements InferenceService { - private final SetOnce factory; - private final SetOnce serviceComponents; - private final AtomicReference sender = new AtomicReference<>(); + private final Sender sender; + private final ServiceComponents serviceComponents; - public SenderService(SetOnce factory, SetOnce serviceComponents) { - this.factory = Objects.requireNonNull(factory); + public SenderService(HttpRequestSenderFactory factory, ServiceComponents serviceComponents) { + Objects.requireNonNull(factory); + sender = factory.createSender(name()); this.serviceComponents = Objects.requireNonNull(serviceComponents); } protected Sender getSender() { - return sender.get(); + return sender; } protected ServiceComponents getServiceComponents() { - return serviceComponents.get(); + return serviceComponents; } @Override @@ -98,12 +96,11 @@ protected void doStart(Model model, ActionListener listener) { } private void init() { - sender.updateAndGet(current -> Objects.requireNonNullElseGet(current, () -> factory.get().createSender(name()))); - sender.get().start(); + sender.start(); } @Override public void close() throws IOException { - IOUtils.closeWhileHandlingException(sender.get()); + IOUtils.closeWhileHandlingException(sender); } } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java index 35b245e9a657a..9502acdaf93e5 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/cohere/CohereService.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.services.cohere; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; @@ -43,7 +42,7 @@ public class CohereService extends SenderService { public static final String NAME = "cohere"; - public CohereService(SetOnce factory, SetOnce serviceComponents) { + public CohereService(HttpRequestSenderFactory factory, ServiceComponents serviceComponents) { super(factory, serviceComponents); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseService.java index 25ba29fddd14e..9cd8c285b406e 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseService.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.services.huggingface; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.action.ActionListener; import org.elasticsearch.inference.ChunkedInferenceServiceResults; import org.elasticsearch.inference.ChunkingOptions; @@ -33,7 +32,7 @@ public abstract class HuggingFaceBaseService extends SenderService { - public HuggingFaceBaseService(SetOnce factory, SetOnce serviceComponents) { + public HuggingFaceBaseService(HttpRequestSenderFactory factory, ServiceComponents serviceComponents) { super(factory, serviceComponents); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceService.java index 60f947e22da95..2d2f4667478d5 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceService.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.services.huggingface; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; @@ -27,7 +26,7 @@ public class HuggingFaceService extends HuggingFaceBaseService { public static final String NAME = "hugging_face"; - public HuggingFaceService(SetOnce factory, SetOnce serviceComponents) { + public HuggingFaceService(HttpRequestSenderFactory factory, ServiceComponents serviceComponents) { super(factory, serviceComponents); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/elser/HuggingFaceElserService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/elser/HuggingFaceElserService.java index 68407d8a2e029..3cc2ca5ed60a5 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/elser/HuggingFaceElserService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/huggingface/elser/HuggingFaceElserService.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.services.huggingface.elser; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; @@ -24,7 +23,7 @@ public class HuggingFaceElserService extends HuggingFaceBaseService { public static final String NAME = "hugging_face_elser"; - public HuggingFaceElserService(SetOnce factory, SetOnce serviceComponents) { + public HuggingFaceElserService(HttpRequestSenderFactory factory, ServiceComponents serviceComponents) { super(factory, serviceComponents); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/OpenAiService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/OpenAiService.java index 03781450fc08c..5062bba8e7eac 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/OpenAiService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/openai/OpenAiService.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.services.openai; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; @@ -46,7 +45,7 @@ public class OpenAiService extends SenderService { public static final String NAME = "openai"; - public OpenAiService(SetOnce factory, SetOnce serviceComponents) { + public OpenAiService(HttpRequestSenderFactory factory, ServiceComponents serviceComponents) { super(factory, serviceComponents); } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/SenderServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/SenderServiceTests.java index 22a7224d73549..82d53cfb09037 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/SenderServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/SenderServiceTests.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.services; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.TransportVersion; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; @@ -61,7 +60,7 @@ public void testStart_InitializesTheSender() throws IOException { var factory = mock(HttpRequestSenderFactory.class); when(factory.createSender(anyString())).thenReturn(sender); - try (var service = new TestSenderService(new SetOnce<>(factory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new TestSenderService(factory, createWithEmptySettings(threadPool))) { PlainActionFuture listener = new PlainActionFuture<>(); service.start(mock(Model.class), listener); @@ -81,7 +80,7 @@ public void testStart_CallingStartTwiceKeepsSameSenderReference() throws IOExcep var factory = mock(HttpRequestSenderFactory.class); when(factory.createSender(anyString())).thenReturn(sender); - try (var service = new TestSenderService(new SetOnce<>(factory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new TestSenderService(factory, createWithEmptySettings(threadPool))) { PlainActionFuture listener = new PlainActionFuture<>(); service.start(mock(Model.class), listener); listener.actionGet(TIMEOUT); @@ -99,7 +98,7 @@ public void testStart_CallingStartTwiceKeepsSameSenderReference() throws IOExcep } private static final class TestSenderService extends SenderService { - TestSenderService(SetOnce factory, SetOnce serviceComponents) { + TestSenderService(HttpRequestSenderFactory factory, ServiceComponents serviceComponents) { super(factory, serviceComponents); } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceTests.java index f9b76dfcf2528..9c2722e68efd6 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/cohere/CohereServiceTests.java @@ -10,7 +10,6 @@ package org.elasticsearch.xpack.inference.services.cohere; import org.apache.http.HttpHeaders; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.action.ActionListener; @@ -93,12 +92,7 @@ public void shutdown() throws IOException { } public void testParseRequestConfig_CreatesACohereEmbeddingsModel() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { ActionListener modelListener = ActionListener.wrap(model -> { MatcherAssert.assertThat(model, instanceOf(CohereEmbeddingsModel.class)); @@ -130,12 +124,7 @@ public void testParseRequestConfig_CreatesACohereEmbeddingsModel() throws IOExce } public void testParseRequestConfig_ThrowsUnsupportedModelType() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var failureListener = getModelListenerForException( ElasticsearchStatusException.class, @@ -164,12 +153,7 @@ private static ActionListener getModelListenerForException(Class excep } public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInConfig() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var config = getRequestConfigMap( CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap("url", null, null), getTaskSettingsMapEmpty(), @@ -186,12 +170,7 @@ public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInConfig() throws I } public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInServiceSettingsMap() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var serviceSettings = CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap("url", "model", null); serviceSettings.put("extra_key", "value"); @@ -206,12 +185,7 @@ public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInServiceSettingsMa } public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInTaskSettingsMap() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var taskSettingsMap = getTaskSettingsMap(InputType.INGEST, null); taskSettingsMap.put("extra_key", "value"); @@ -231,12 +205,7 @@ public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInTaskSettingsMap() } public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInSecretSettingsMap() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var secretSettingsMap = getSecretSettingsMap("secret"); secretSettingsMap.put("extra_key", "value"); @@ -255,12 +224,7 @@ public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInSecretSettingsMap } public void testParseRequestConfig_CreatesACohereEmbeddingsModelWithoutUrl() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var modelListener = ActionListener.wrap((model) -> { MatcherAssert.assertThat(model, instanceOf(CohereEmbeddingsModel.class)); @@ -286,12 +250,7 @@ public void testParseRequestConfig_CreatesACohereEmbeddingsModelWithoutUrl() thr } public void testParsePersistedConfigWithSecrets_CreatesACohereEmbeddingsModel() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var persistedConfig = getPersistedConfigMap( CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap("url", "model", null), getTaskSettingsMap(null, null), @@ -316,12 +275,7 @@ public void testParsePersistedConfigWithSecrets_CreatesACohereEmbeddingsModel() } public void testParsePersistedConfigWithSecrets_ThrowsErrorTryingToParseInvalidModel() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var persistedConfig = getPersistedConfigMap( CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap("url", null, null), getTaskSettingsMapEmpty(), @@ -346,12 +300,7 @@ public void testParsePersistedConfigWithSecrets_ThrowsErrorTryingToParseInvalidM } public void testParsePersistedConfigWithSecrets_CreatesACohereEmbeddingsModelWithoutUrl() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var persistedConfig = getPersistedConfigMap( CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap(null, null, null), getTaskSettingsMap(InputType.INGEST, null), @@ -375,12 +324,7 @@ public void testParsePersistedConfigWithSecrets_CreatesACohereEmbeddingsModelWit } public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExistsInConfig() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var persistedConfig = getPersistedConfigMap( CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap("url", "model", CohereEmbeddingType.INT8), getTaskSettingsMap(InputType.SEARCH, CohereTruncation.NONE), @@ -410,12 +354,7 @@ public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExists } public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExistsInSecretsSettings() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var secretSettingsMap = getSecretSettingsMap("secret"); secretSettingsMap.put("extra_key", "value"); @@ -442,12 +381,7 @@ public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExists } public void testParsePersistedConfigWithSecrets_NotThrowWhenAnExtraKeyExistsInSecrets() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var persistedConfig = getPersistedConfigMap( CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap("url", "model", null), getTaskSettingsMap(null, null), @@ -473,12 +407,7 @@ public void testParsePersistedConfigWithSecrets_NotThrowWhenAnExtraKeyExistsInSe } public void testParsePersistedConfigWithSecrets_NotThrowWhenAnExtraKeyExistsInServiceSettings() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var serviceSettingsMap = CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap("url", null, null); serviceSettingsMap.put("extra_key", "value"); @@ -501,12 +430,7 @@ public void testParsePersistedConfigWithSecrets_NotThrowWhenAnExtraKeyExistsInSe } public void testParsePersistedConfigWithSecrets_NotThrowWhenAnExtraKeyExistsInTaskSettings() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var taskSettingsMap = getTaskSettingsMap(InputType.SEARCH, null); taskSettingsMap.put("extra_key", "value"); @@ -534,12 +458,7 @@ public void testParsePersistedConfigWithSecrets_NotThrowWhenAnExtraKeyExistsInTa } public void testParsePersistedConfig_CreatesACohereEmbeddingsModel() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var persistedConfig = getPersistedConfigMap( CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap("url", "model", null), getTaskSettingsMap(null, CohereTruncation.NONE) @@ -558,12 +477,7 @@ public void testParsePersistedConfig_CreatesACohereEmbeddingsModel() throws IOEx } public void testParsePersistedConfig_ThrowsErrorTryingToParseInvalidModel() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var persistedConfig = getPersistedConfigMap( CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap("url", null, null), getTaskSettingsMapEmpty() @@ -582,12 +496,7 @@ public void testParsePersistedConfig_ThrowsErrorTryingToParseInvalidModel() thro } public void testParsePersistedConfig_CreatesACohereEmbeddingsModelWithoutUrl() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var persistedConfig = getPersistedConfigMap( CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap(null, "model", CohereEmbeddingType.FLOAT), getTaskSettingsMap(null, null) @@ -607,12 +516,7 @@ public void testParsePersistedConfig_CreatesACohereEmbeddingsModelWithoutUrl() t } public void testParsePersistedConfig_DoesNotThrowWhenAnExtraKeyExistsInConfig() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var persistedConfig = getPersistedConfigMap( CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap("url", null, null), getTaskSettingsMapEmpty() @@ -631,12 +535,7 @@ public void testParsePersistedConfig_DoesNotThrowWhenAnExtraKeyExistsInConfig() } public void testParsePersistedConfig_NotThrowWhenAnExtraKeyExistsInServiceSettings() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var serviceSettingsMap = CohereEmbeddingsServiceSettingsTests.getServiceSettingsMap("url", null, null); serviceSettingsMap.put("extra_key", "value"); @@ -654,12 +553,7 @@ public void testParsePersistedConfig_NotThrowWhenAnExtraKeyExistsInServiceSettin } public void testParsePersistedConfig_NotThrowWhenAnExtraKeyExistsInTaskSettings() throws IOException { - try ( - var service = new CohereService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createCohereService()) { var taskSettingsMap = getTaskSettingsMap(InputType.INGEST, null); taskSettingsMap.put("extra_key", "value"); @@ -688,7 +582,7 @@ public void testInfer_ThrowsErrorWhenModelIsNotCohereModel() throws IOException var mockModel = getInvalidModel("model_id", "service_name"); - try (var service = new CohereService(new SetOnce<>(factory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new CohereService(factory, createWithEmptySettings(threadPool))) { PlainActionFuture listener = new PlainActionFuture<>(); service.infer(mockModel, List.of(""), new HashMap<>(), InputType.INGEST, listener); @@ -710,7 +604,7 @@ public void testInfer_ThrowsErrorWhenModelIsNotCohereModel() throws IOException public void testInfer_SendsRequest() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new CohereService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new CohereService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -770,7 +664,7 @@ public void testInfer_SendsRequest() throws IOException { public void testCheckModelConfig_UpdatesDimensions() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new CohereService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new CohereService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -833,7 +727,7 @@ public void testCheckModelConfig_UpdatesDimensions() throws IOException { public void testInfer_UnauthorisedResponse() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new CohereService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new CohereService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -864,7 +758,7 @@ public void testInfer_UnauthorisedResponse() throws IOException { public void testInfer_SetsInputTypeToIngest_FromInferParameter_WhenTaskSettingsAreEmpty() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new CohereService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new CohereService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -925,7 +819,7 @@ public void testInfer_SetsInputTypeToIngestFromInferParameter_WhenModelSettingIs throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new CohereService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new CohereService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -991,7 +885,7 @@ public void testInfer_SetsInputTypeToIngestFromInferParameter_WhenModelSettingIs public void testInfer_DoesNotSetInputType_WhenNotPresentInTaskSettings_AndUnspecifiedIsPassedInRequest() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new CohereService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new CohereService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -1062,6 +956,10 @@ private Map getRequestConfigMap( ); } + private CohereService createCohereService() { + return new CohereService(mock(HttpRequestSenderFactory.class), createWithEmptySettings(threadPool)); + } + private PeristedConfig getPersistedConfigMap( Map serviceSettings, Map taskSettings, diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseServiceTests.java index dcf8b3a900a22..345aa1a80e5bd 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceBaseServiceTests.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.inference.services.huggingface; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.TransportVersion; import org.elasticsearch.action.support.PlainActionFuture; @@ -63,7 +62,7 @@ public void testInfer_ThrowsErrorWhenModelIsNotHuggingFaceModel() throws IOExcep var mockModel = getInvalidModel("model_id", "service_name"); - try (var service = new TestService(new SetOnce<>(factory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new TestService(factory, createWithEmptySettings(threadPool))) { PlainActionFuture listener = new PlainActionFuture<>(); service.infer(mockModel, List.of(""), new HashMap<>(), InputType.INGEST, listener); @@ -84,7 +83,7 @@ public void testInfer_ThrowsErrorWhenModelIsNotHuggingFaceModel() throws IOExcep private static final class TestService extends HuggingFaceBaseService { - TestService(SetOnce factory, SetOnce serviceComponents) { + TestService(HttpRequestSenderFactory factory, ServiceComponents serviceComponents) { super(factory, serviceComponents); } diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceServiceTests.java index b34a8ad8a3d65..23d6bd17e48d1 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/huggingface/HuggingFaceServiceTests.java @@ -10,7 +10,6 @@ package org.elasticsearch.xpack.inference.services.huggingface; import org.apache.http.HttpHeaders; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; @@ -83,12 +82,7 @@ public void shutdown() throws IOException { } public void testParseRequestConfig_CreatesAnEmbeddingsModel() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { ActionListener modelVerificationActionListener = ActionListener.wrap((model) -> { assertThat(model, instanceOf(HuggingFaceEmbeddingsModel.class)); @@ -109,12 +103,7 @@ public void testParseRequestConfig_CreatesAnEmbeddingsModel() throws IOException } public void testParseRequestConfig_CreatesAnElserModel() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { ActionListener modelVerificationActionListener = ActionListener.wrap((model) -> { assertThat(model, instanceOf(HuggingFaceElserModel.class)); @@ -134,12 +123,7 @@ public void testParseRequestConfig_CreatesAnElserModel() throws IOException { } public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInConfig() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var config = getRequestConfigMap(getServiceSettingsMap("url"), getSecretSettingsMap("secret")); config.put("extra_key", "value"); @@ -159,12 +143,7 @@ public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInConfig() throws I } public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInServiceSettingsMap() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var serviceSettings = getServiceSettingsMap("url"); serviceSettings.put("extra_key", "value"); @@ -186,12 +165,7 @@ public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInServiceSettingsMa } public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInSecretSettingsMap() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var secretSettingsMap = getSecretSettingsMap("secret"); secretSettingsMap.put("extra_key", "value"); @@ -213,12 +187,7 @@ public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInSecretSettingsMap } public void testParsePersistedConfigWithSecrets_CreatesAnEmbeddingsModel() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var persistedConfig = getPersistedConfigMap(getServiceSettingsMap("url"), getSecretSettingsMap("secret")); var model = service.parsePersistedConfigWithSecrets( @@ -237,12 +206,7 @@ public void testParsePersistedConfigWithSecrets_CreatesAnEmbeddingsModel() throw } public void testParsePersistedConfigWithSecrets_CreatesAnElserModel() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var persistedConfig = getPersistedConfigMap(getServiceSettingsMap("url"), getSecretSettingsMap("secret")); var model = service.parsePersistedConfigWithSecrets( @@ -261,12 +225,7 @@ public void testParsePersistedConfigWithSecrets_CreatesAnElserModel() throws IOE } public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExistsInConfig() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var persistedConfig = getPersistedConfigMap(getServiceSettingsMap("url"), getSecretSettingsMap("secret")); persistedConfig.config().put("extra_key", "value"); @@ -286,12 +245,7 @@ public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExists } public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExistsInSecretsSettings() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var secretSettingsMap = getSecretSettingsMap("secret"); secretSettingsMap.put("extra_key", "value"); @@ -313,12 +267,7 @@ public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExists } public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExistsInSecrets() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var persistedConfig = getPersistedConfigMap(getServiceSettingsMap("url"), getSecretSettingsMap("secret")); persistedConfig.secrets.put("extra_key", "value"); @@ -338,12 +287,7 @@ public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExists } public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExistsInServiceSettings() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var serviceSettingsMap = getServiceSettingsMap("url"); serviceSettingsMap.put("extra_key", "value"); @@ -365,12 +309,7 @@ public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExists } public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExistsInTaskSettings() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var taskSettingsMap = new HashMap(); taskSettingsMap.put("extra_key", "value"); @@ -392,12 +331,7 @@ public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExists } public void testParsePersistedConfig_CreatesAnEmbeddingsModel() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var persistedConfig = getPersistedConfigMap(getServiceSettingsMap("url")); var model = service.parsePersistedConfig("id", TaskType.TEXT_EMBEDDING, persistedConfig.config()); @@ -411,12 +345,7 @@ public void testParsePersistedConfig_CreatesAnEmbeddingsModel() throws IOExcepti } public void testParsePersistedConfig_CreatesAnElserModel() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var persistedConfig = getPersistedConfigMap(getServiceSettingsMap("url")); var model = service.parsePersistedConfig("id", TaskType.SPARSE_EMBEDDING, persistedConfig.config()); @@ -430,12 +359,7 @@ public void testParsePersistedConfig_CreatesAnElserModel() throws IOException { } public void testParsePersistedConfig_DoesNotThrowWhenAnExtraKeyExistsInConfig() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var persistedConfig = getPersistedConfigMap(getServiceSettingsMap("url")); persistedConfig.config().put("extra_key", "value"); @@ -450,12 +374,7 @@ public void testParsePersistedConfig_DoesNotThrowWhenAnExtraKeyExistsInConfig() } public void testParsePersistedConfig_DoesNotThrowWhenAnExtraKeyExistsInServiceSettings() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var serviceSettingsMap = getServiceSettingsMap("url"); serviceSettingsMap.put("extra_key", "value"); @@ -472,12 +391,7 @@ public void testParsePersistedConfig_DoesNotThrowWhenAnExtraKeyExistsInServiceSe } public void testParsePersistedConfig_DoesNotThrowWhenAnExtraKeyExistsInTaskSettings() throws IOException { - try ( - var service = new HuggingFaceService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createHuggingFaceService()) { var taskSettingsMap = new HashMap(); taskSettingsMap.put("extra_key", "value"); @@ -496,7 +410,7 @@ public void testParsePersistedConfig_DoesNotThrowWhenAnExtraKeyExistsInTaskSetti public void testInfer_SendsEmbeddingsRequest() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new HuggingFaceService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new HuggingFaceService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -534,7 +448,7 @@ public void testInfer_SendsEmbeddingsRequest() throws IOException { public void testInfer_SendsElserRequest() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new HuggingFaceService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new HuggingFaceService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ [ @@ -576,7 +490,7 @@ public void testInfer_SendsElserRequest() throws IOException { public void testCheckModelConfig_IncludesMaxTokens() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new HuggingFaceService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new HuggingFaceService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -598,6 +512,10 @@ public void testCheckModelConfig_IncludesMaxTokens() throws IOException { } } + private HuggingFaceService createHuggingFaceService() { + return new HuggingFaceService(mock(HttpRequestSenderFactory.class), createWithEmptySettings(threadPool)); + } + private Map getRequestConfigMap(Map serviceSettings, Map secretSettings) { var builtServiceSettings = new HashMap<>(); builtServiceSettings.putAll(serviceSettings); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/OpenAiServiceTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/OpenAiServiceTests.java index b3d9a98bad189..e97040ed7d795 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/OpenAiServiceTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/openai/OpenAiServiceTests.java @@ -10,7 +10,6 @@ package org.elasticsearch.xpack.inference.services.openai; import org.apache.http.HttpHeaders; -import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.action.ActionListener; @@ -93,12 +92,7 @@ public void shutdown() throws IOException { } public void testParseRequestConfig_CreatesAnOpenAiEmbeddingsModel() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { ActionListener modelVerificationListener = ActionListener.wrap(model -> { assertThat(model, instanceOf(OpenAiEmbeddingsModel.class)); @@ -125,12 +119,7 @@ public void testParseRequestConfig_CreatesAnOpenAiEmbeddingsModel() throws IOExc } public void testParseRequestConfig_ThrowsUnsupportedModelType() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { ActionListener modelVerificationListener = ActionListener.wrap( model -> fail("Expected exception, but got model: " + model), exception -> { @@ -154,12 +143,7 @@ public void testParseRequestConfig_ThrowsUnsupportedModelType() throws IOExcepti } public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInConfig() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var config = getRequestConfigMap( getServiceSettingsMap("model", "url", "org"), getTaskSettingsMap("user"), @@ -183,12 +167,7 @@ public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInConfig() throws I } public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInServiceSettingsMap() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var serviceSettings = getServiceSettingsMap("model", "url", "org"); serviceSettings.put("extra_key", "value"); @@ -206,12 +185,7 @@ public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInServiceSettingsMa } public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInTaskSettingsMap() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var taskSettingsMap = getTaskSettingsMap("user"); taskSettingsMap.put("extra_key", "value"); @@ -229,12 +203,7 @@ public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInTaskSettingsMap() } public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInSecretSettingsMap() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var secretSettingsMap = getSecretSettingsMap("secret"); secretSettingsMap.put("extra_key", "value"); @@ -252,13 +221,7 @@ public void testParseRequestConfig_ThrowsWhenAnExtraKeyExistsInSecretSettingsMap } public void testParseRequestConfig_CreatesAnOpenAiEmbeddingsModelWithoutUserUrlOrganization() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { - + try (var service = createOpenAiService()) { ActionListener modelVerificationListener = ActionListener.wrap(model -> { assertThat(model, instanceOf(OpenAiEmbeddingsModel.class)); @@ -281,12 +244,7 @@ public void testParseRequestConfig_CreatesAnOpenAiEmbeddingsModelWithoutUserUrlO } public void testParseRequestConfig_MovesModel() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { ActionListener modelVerificationListener = ActionListener.wrap(model -> { assertThat(model, instanceOf(OpenAiEmbeddingsModel.class)); @@ -313,12 +271,7 @@ public void testParseRequestConfig_MovesModel() throws IOException { } public void testParsePersistedConfigWithSecrets_CreatesAnOpenAiEmbeddingsModel() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var persistedConfig = getPersistedConfigMap( getServiceSettingsMap("model", "url", "org", 100, false), getTaskSettingsMap("user"), @@ -344,12 +297,7 @@ public void testParsePersistedConfigWithSecrets_CreatesAnOpenAiEmbeddingsModel() } public void testParsePersistedConfigWithSecrets_ThrowsErrorTryingToParseInvalidModel() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var persistedConfig = getPersistedConfigMap( getServiceSettingsMap("model", "url", "org"), getTaskSettingsMap("user"), @@ -374,12 +322,7 @@ public void testParsePersistedConfigWithSecrets_ThrowsErrorTryingToParseInvalidM } public void testParsePersistedConfigWithSecrets_CreatesAnOpenAiEmbeddingsModelWithoutUserUrlOrganization() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var persistedConfig = getPersistedConfigMap( getServiceSettingsMap("model", null, null, null, true), getTaskSettingsMap(null), @@ -405,12 +348,7 @@ public void testParsePersistedConfigWithSecrets_CreatesAnOpenAiEmbeddingsModelWi } public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExistsInConfig() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var persistedConfig = getPersistedConfigMap( getServiceSettingsMap("model", "url", "org", null, true), getTaskSettingsMap("user"), @@ -438,12 +376,7 @@ public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExists } public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExistsInSecretsSettings() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var secretSettingsMap = getSecretSettingsMap("secret"); secretSettingsMap.put("extra_key", "value"); @@ -472,12 +405,7 @@ public void testParsePersistedConfigWithSecrets_DoesNotThrowWhenAnExtraKeyExists } public void testParsePersistedConfigWithSecrets_NotThrowWhenAnExtraKeyExistsInSecrets() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var persistedConfig = getPersistedConfigMap( getServiceSettingsMap("model", "url", "org", null, true), getTaskSettingsMap("user"), @@ -505,12 +433,7 @@ public void testParsePersistedConfigWithSecrets_NotThrowWhenAnExtraKeyExistsInSe } public void testParsePersistedConfigWithSecrets_NotThrowWhenAnExtraKeyExistsInServiceSettings() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var serviceSettingsMap = getServiceSettingsMap("model", "url", "org", null, true); serviceSettingsMap.put("extra_key", "value"); @@ -535,12 +458,7 @@ public void testParsePersistedConfigWithSecrets_NotThrowWhenAnExtraKeyExistsInSe } public void testParsePersistedConfigWithSecrets_NotThrowWhenAnExtraKeyExistsInTaskSettings() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var taskSettingsMap = getTaskSettingsMap("user"); taskSettingsMap.put("extra_key", "value"); @@ -569,12 +487,7 @@ public void testParsePersistedConfigWithSecrets_NotThrowWhenAnExtraKeyExistsInTa } public void testParsePersistedConfig_CreatesAnOpenAiEmbeddingsModel() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var persistedConfig = getPersistedConfigMap( getServiceSettingsMap("model", "url", "org", null, true), getTaskSettingsMap("user") @@ -594,12 +507,7 @@ public void testParsePersistedConfig_CreatesAnOpenAiEmbeddingsModel() throws IOE } public void testParsePersistedConfig_ThrowsErrorTryingToParseInvalidModel() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var persistedConfig = getPersistedConfigMap(getServiceSettingsMap("model", "url", "org"), getTaskSettingsMap("user")); var thrownException = expectThrows( @@ -615,12 +523,7 @@ public void testParsePersistedConfig_ThrowsErrorTryingToParseInvalidModel() thro } public void testParsePersistedConfig_CreatesAnOpenAiEmbeddingsModelWithoutUserUrlOrganization() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var persistedConfig = getPersistedConfigMap(getServiceSettingsMap("model", null, null, null, true), getTaskSettingsMap(null)); var model = service.parsePersistedConfig("id", TaskType.TEXT_EMBEDDING, persistedConfig.config()); @@ -637,12 +540,7 @@ public void testParsePersistedConfig_CreatesAnOpenAiEmbeddingsModelWithoutUserUr } public void testParsePersistedConfig_DoesNotThrowWhenAnExtraKeyExistsInConfig() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var persistedConfig = getPersistedConfigMap( getServiceSettingsMap("model", "url", "org", null, true), getTaskSettingsMap("user") @@ -663,12 +561,7 @@ public void testParsePersistedConfig_DoesNotThrowWhenAnExtraKeyExistsInConfig() } public void testParsePersistedConfig_NotThrowWhenAnExtraKeyExistsInServiceSettings() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var serviceSettingsMap = getServiceSettingsMap("model", "url", "org", null, true); serviceSettingsMap.put("extra_key", "value"); @@ -688,12 +581,7 @@ public void testParsePersistedConfig_NotThrowWhenAnExtraKeyExistsInServiceSettin } public void testParsePersistedConfig_NotThrowWhenAnExtraKeyExistsInTaskSettings() throws IOException { - try ( - var service = new OpenAiService( - new SetOnce<>(mock(HttpRequestSenderFactory.class)), - new SetOnce<>(createWithEmptySettings(threadPool)) - ) - ) { + try (var service = createOpenAiService()) { var taskSettingsMap = getTaskSettingsMap("user"); taskSettingsMap.put("extra_key", "value"); @@ -721,7 +609,7 @@ public void testInfer_ThrowsErrorWhenModelIsNotOpenAiModel() throws IOException var mockModel = getInvalidModel("model_id", "service_name"); - try (var service = new OpenAiService(new SetOnce<>(factory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new OpenAiService(factory, createWithEmptySettings(threadPool))) { PlainActionFuture listener = new PlainActionFuture<>(); service.infer(mockModel, List.of(""), new HashMap<>(), InputType.INGEST, listener); @@ -743,7 +631,7 @@ public void testInfer_ThrowsErrorWhenModelIsNotOpenAiModel() throws IOException public void testInfer_SendsRequest() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new OpenAiService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new OpenAiService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -791,7 +679,7 @@ public void testInfer_SendsRequest() throws IOException { public void testCheckModelConfig_IncludesMaxTokens() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new OpenAiService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new OpenAiService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -832,7 +720,7 @@ public void testCheckModelConfig_IncludesMaxTokens() throws IOException { public void testCheckModelConfig_ThrowsIfEmbeddingSizeDoesNotMatchValueSetByUser() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new OpenAiService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new OpenAiService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -883,7 +771,7 @@ public void testCheckModelConfig_ReturnsModelWithDimensionsSetTo2_AndDocProductS throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new OpenAiService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new OpenAiService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -941,7 +829,7 @@ public void testCheckModelConfig_ReturnsModelWithSameDimensions_AndDocProductSet throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new OpenAiService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new OpenAiService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -1000,7 +888,7 @@ public void testCheckModelConfig_ReturnsModelWithSameDimensions_AndDocProductSet public void testCheckModelConfig_ReturnsNewModelReference_AndDoesNotSendDimensionsField_WhenNotSetByUser() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new OpenAiService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new OpenAiService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -1066,7 +954,7 @@ public void testCheckModelConfig_ReturnsNewModelReference_AndDoesNotSendDimensio public void testInfer_UnauthorisedResponse() throws IOException { var senderFactory = new HttpRequestSenderFactory(threadPool, clientManager, mockClusterServiceEmpty(), Settings.EMPTY); - try (var service = new OpenAiService(new SetOnce<>(senderFactory), new SetOnce<>(createWithEmptySettings(threadPool)))) { + try (var service = new OpenAiService(senderFactory, createWithEmptySettings(threadPool))) { String responseJson = """ { @@ -1118,6 +1006,10 @@ public void testMoveModelFromTaskToServiceSettings_AlreadyMoved() { assertEquals("model", serviceSettings.get(ServiceFields.MODEL_ID)); } + private OpenAiService createOpenAiService() { + return new OpenAiService(mock(HttpRequestSenderFactory.class), createWithEmptySettings(threadPool)); + } + private Map getRequestConfigMap( Map serviceSettings, Map taskSettings, From 5b826819e83b8ca5919a1e489d276a811e8affb5 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Thu, 22 Feb 2024 07:52:36 -0800 Subject: [PATCH 150/250] Upgrade jna to 5.12.1 (#105717) This commit upgrades jna to 5.12.1, which supports better control over releasing native memory. relates #105715 --- build-tools-internal/version.properties | 2 +- docs/changelog/105717.yaml | 5 +++++ gradle/verification-metadata.xml | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/105717.yaml diff --git a/build-tools-internal/version.properties b/build-tools-internal/version.properties index d1d0da4b1c262..7a6e5ac125aff 100644 --- a/build-tools-internal/version.properties +++ b/build-tools-internal/version.properties @@ -13,7 +13,7 @@ supercsv = 2.4.0 log4j = 2.19.0 slf4j = 2.0.6 ecsLogging = 1.2.0 -jna = 5.10.0 +jna = 5.12.1 netty = 4.1.107.Final commons_lang3 = 3.9 google_oauth_client = 1.34.1 diff --git a/docs/changelog/105717.yaml b/docs/changelog/105717.yaml new file mode 100644 index 0000000000000..c75bc4fe65798 --- /dev/null +++ b/docs/changelog/105717.yaml @@ -0,0 +1,5 @@ +pr: 105717 +summary: Upgrade jna to 5.12.1 +area: Infra/Core +type: upgrade +issues: [] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index d7e4e1c723a24..07b35f8b3e345 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1594,6 +1594,11 @@ + + + + + From 39dd09bb3d2b6ae4d925d03c6f9b3070438e3b9a Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 22 Feb 2024 16:58:59 +0100 Subject: [PATCH 151/250] Grow buckets on FilterByFilterAggregator eagerly (#105703) --- .../bucket/filter/FilterByFilterAggregator.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterByFilterAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterByFilterAggregator.java index 3a2cce587f34f..59b591094ae3a 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterByFilterAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/filter/FilterByFilterAggregator.java @@ -296,13 +296,15 @@ class MatchCollector implements LeafCollector { @Override public void collect(int docId) throws IOException { - collectBucket(subCollector, docId, filterOrd); + collectExistingBucket(subCollector, docId, filterOrd); } @Override - public void setScorer(Scorable scorer) throws IOException {} + public void setScorer(Scorable scorer) {} } MatchCollector collector = new MatchCollector(); + // create the buckets so we can call collectExistingBucket + grow(filters().size() + 1); filters().get(0).collect(aggCtx.getLeafReaderContext(), collector, live); for (int filterOrd = 1; filterOrd < filters().size(); filterOrd++) { collector.subCollector = collectableSubAggregators.getLeafCollector(aggCtx); From b752169ee988b0f4812b4501828282f658bbb231 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Thu, 22 Feb 2024 16:15:36 +0000 Subject: [PATCH 152/250] Use hamcrest regex matcher rather than our own (#104457) The difference is that our matcher uses .find() to search for a regex match anywhere in the string, whereas the hamcrest one uses .matches() to check the whole string against the regex. This leads to more specific regex checks. I've left our own one for YAML tests, as that way we don't need to mangle the regex to add .* either side, which might be confusing in test failures. --- .../plugins/cli/InstallPluginActionTests.java | 4 +- .../rest/Netty4BadRequestIT.java | 5 +- .../common/logging/EvilLoggerTests.java | 37 ++++---- .../common/logging/CustomLoggingConfigIT.java | 7 +- .../threadpool/SimpleThreadPoolIT.java | 4 +- .../common/logging/HeaderWarningTests.java | 5 +- .../index/shard/IndexShardTests.java | 4 +- .../indices/IndicesServiceTests.java | 4 +- .../plugins/IndexStorePluginTests.java | 4 +- .../test/rest/yaml/section/DoSection.java | 2 +- .../rest/yaml/section/MatchAssertion.java | 2 +- .../test/rest/yaml/section}/RegexMatcher.java | 8 +- .../xpack/deprecation/DeprecationHttpIT.java | 6 +- .../xpack/sql/qa/cli/LenientTestCase.java | 7 +- .../xpack/sql/qa/cli/SelectTestCase.java | 7 +- .../xpack/sql/qa/cli/ShowTestCase.java | 91 ++++++++++--------- 16 files changed, 100 insertions(+), 97 deletions(-) rename test/{framework/src/main/java/org/elasticsearch/test/hamcrest => yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section}/RegexMatcher.java (86%) diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java index c088e89338e74..3dc7af07d4d83 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java @@ -101,13 +101,13 @@ import java.util.zip.ZipOutputStream; import static org.elasticsearch.snapshots.AbstractSnapshotIntegTestCase.forEachFileRecursively; -import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.matchesRegex; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; @@ -1286,7 +1286,7 @@ public void testInvalidShaFileMismatchFilename() throws Exception { ) ); assertEquals(ExitCodes.IO_ERROR, e.exitCode); - assertThat(e, hasToString(matches("checksum file at \\[.*\\] is not for this plugin"))); + assertThat(e, hasToString(matchesRegex(".*checksum file at \\[.*\\] is not for this plugin.*"))); } public void testInvalidShaFileContainingExtraLine() throws Exception { diff --git a/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4BadRequestIT.java b/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4BadRequestIT.java index 31e8c4765d4f2..a7bc031448087 100644 --- a/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4BadRequestIT.java +++ b/modules/transport-netty4/src/javaRestTest/java/org/elasticsearch/rest/Netty4BadRequestIT.java @@ -22,12 +22,13 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.regex.Pattern; import static org.elasticsearch.rest.RestStatus.BAD_REQUEST; -import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.matchesRegex; public class Netty4BadRequestIT extends ESRestTestCase { @@ -63,7 +64,7 @@ public void testBadRequest() throws IOException { ); assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(BAD_REQUEST.getStatus())); assertThat(e, hasToString(containsString("too_long_http_line_exception"))); - assertThat(e, hasToString(matches("An HTTP line is larger than \\d+ bytes"))); + assertThat(e, hasToString(matchesRegex(Pattern.compile(".*An HTTP line is larger than \\d+ bytes.*", Pattern.DOTALL)))); } public void testInvalidParameterValue() throws IOException { diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerTests.java index 1ff9397bc8b08..956d3c0e104ae 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerTests.java @@ -26,7 +26,6 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.node.Node; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.hamcrest.RegexMatcher; import java.io.IOException; import java.io.PrintWriter; @@ -48,7 +47,9 @@ import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.matchesRegex; import static org.hamcrest.Matchers.startsWith; public class EvilLoggerTests extends ESTestCase { @@ -82,14 +83,14 @@ public void testLocationInfoTest() throws IOException { + System.getProperty("es.logs.cluster_name") + ".log"; final List events = Files.readAllLines(PathUtils.get(path)); - assertThat(events.size(), equalTo(5)); + assertThat(events, hasSize(5)); final String location = "org.elasticsearch.common.logging.EvilLoggerTests.testLocationInfoTest"; // the first message is a warning for unsupported configuration files - assertLogLine(events.get(0), Level.ERROR, location, "This is an error message"); - assertLogLine(events.get(1), Level.WARN, location, "This is a warning message"); - assertLogLine(events.get(2), Level.INFO, location, "This is an info message"); - assertLogLine(events.get(3), Level.DEBUG, location, "This is a debug message"); - assertLogLine(events.get(4), Level.TRACE, location, "This is a trace message"); + assertLogLine(events.get(0), Level.ERROR, location, ".*This is an error message"); + assertLogLine(events.get(1), Level.WARN, location, ".*This is a warning message"); + assertLogLine(events.get(2), Level.INFO, location, ".*This is an info message"); + assertLogLine(events.get(3), Level.DEBUG, location, ".*This is a debug message"); + assertLogLine(events.get(4), Level.TRACE, location, ".*This is a trace message"); } public void testConcurrentDeprecationLogger() throws IOException, BrokenBarrierException, InterruptedException { @@ -166,14 +167,14 @@ public void testConcurrentDeprecationLogger() throws IOException, BrokenBarrierE matcher.matches(); return Integer.parseInt(matcher.group(1)); })); - assertThat(deprecationEvents.size(), equalTo(128)); + assertThat(deprecationEvents, hasSize(128)); for (int i = 0; i < 128; i++) { assertLogLine( deprecationEvents.get(i), DeprecationLogger.CRITICAL, "org.elasticsearch.common.logging.DeprecationLogger.lambda\\$doPrivilegedLog\\$0", - "This is a maybe logged deprecation message" + i + ".*This is a maybe logged deprecation message" + i + ".*" ); } @@ -201,12 +202,12 @@ public void testDeprecatedSettings() throws IOException { + "_deprecation.log"; final List deprecationEvents = Files.readAllLines(PathUtils.get(deprecationPath)); if (iterations > 0) { - assertThat(deprecationEvents.size(), equalTo(1)); + assertThat(deprecationEvents, hasSize(1)); assertLogLine( deprecationEvents.get(0), DeprecationLogger.CRITICAL, "org.elasticsearch.common.logging.DeprecationLogger.lambda\\$doPrivilegedLog\\$0", - "\\[deprecated.foo\\] setting was deprecated in Elasticsearch and will be removed in a future release." + ".*\\[deprecated.foo\\] setting was deprecated in Elasticsearch and will be removed in a future release..*" ); } } @@ -246,7 +247,7 @@ public void testPrefixLogger() throws IOException { e.printStackTrace(pw); final int stackTraceLength = sw.toString().split(System.getProperty("line.separator")).length; final int expectedLogLines = 3; - assertThat(events.size(), equalTo(expectedLogLines + stackTraceLength)); + assertThat(events, hasSize(expectedLogLines + stackTraceLength)); for (int i = 0; i < expectedLogLines; i++) { assertThat("Contents of [" + path + "] are wrong", events.get(i), startsWith("[" + getTestName() + "]" + prefix + " test")); } @@ -287,8 +288,8 @@ public void testNoNodeNameInPatternWarning() throws IOException { + System.getProperty("es.logs.cluster_name") + ".log"; final List events = Files.readAllLines(PathUtils.get(path)); - assertThat(events.size(), equalTo(2)); - final String location = "org.elasticsearch.common.logging.LogConfigurator"; + assertThat(events, hasSize(2)); + final String location = "org.elasticsearch.common.logging.LogConfigurator.*"; // the first message is a warning for unsupported configuration files assertLogLine( events.get(0), @@ -324,12 +325,14 @@ private void setupLogging(final String config, final Settings settings) throws I LogConfigurator.configure(environment, true); } + private static final Pattern LOG_LINE = Pattern.compile("\\[(.*)]\\[(.*)\\(.*\\)] (.*)"); + private void assertLogLine(final String logLine, final Level level, final String location, final String message) { - final Matcher matcher = Pattern.compile("\\[(.*)\\]\\[(.*)\\(.*\\)\\] (.*)").matcher(logLine); + Matcher matcher = LOG_LINE.matcher(logLine); assertTrue(logLine, matcher.matches()); assertThat(matcher.group(1), equalTo(level.toString())); - assertThat(matcher.group(2), RegexMatcher.matches(location)); - assertThat(matcher.group(3), RegexMatcher.matches(message)); + assertThat(matcher.group(2), matchesRegex(location)); + assertThat(matcher.group(3), matchesRegex(message)); } } diff --git a/qa/logging-config/src/javaRestTest/java/org/elasticsearch/common/logging/CustomLoggingConfigIT.java b/qa/logging-config/src/javaRestTest/java/org/elasticsearch/common/logging/CustomLoggingConfigIT.java index 1736533aa526e..4ec12ed135d65 100644 --- a/qa/logging-config/src/javaRestTest/java/org/elasticsearch/common/logging/CustomLoggingConfigIT.java +++ b/qa/logging-config/src/javaRestTest/java/org/elasticsearch/common/logging/CustomLoggingConfigIT.java @@ -8,7 +8,6 @@ package org.elasticsearch.common.logging; import org.elasticsearch.core.SuppressForbidden; -import org.elasticsearch.test.hamcrest.RegexMatcher; import org.elasticsearch.test.rest.ESRestTestCase; import org.hamcrest.Matchers; @@ -22,6 +21,8 @@ import java.security.PrivilegedAction; import java.util.List; +import static org.hamcrest.Matchers.matchesRegex; + /** * This test verifies that Elasticsearch can startup successfully with a custom logging config using variables introduced in * ESJsonLayout @@ -35,14 +36,14 @@ public class CustomLoggingConfigIT extends ESRestTestCase { public void testSuccessfulStartupWithCustomConfig() throws Exception { assertBusy(() -> { List lines = readAllLines(getPlaintextLogFile()); - assertThat(lines, Matchers.hasItem(RegexMatcher.matches(NODE_STARTED))); + assertThat(lines, Matchers.hasItem(matchesRegex(NODE_STARTED))); }); } public void testParseAllV7JsonLines() throws Exception { assertBusy(() -> { List lines = readAllLines(getJSONLogFile()); - assertThat(lines, Matchers.hasItem(RegexMatcher.matches(NODE_STARTED))); + assertThat(lines, Matchers.hasItem(matchesRegex(NODE_STARTED))); }); } diff --git a/server/src/internalClusterTest/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java b/server/src/internalClusterTest/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java index 422aa757656ac..c9c648e57169a 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java @@ -19,7 +19,6 @@ import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.ESIntegTestCase.ClusterScope; import org.elasticsearch.test.ESIntegTestCase.Scope; -import org.elasticsearch.test.hamcrest.RegexMatcher; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; @@ -36,6 +35,7 @@ import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.in; +import static org.hamcrest.Matchers.matchesRegex; @ClusterScope(scope = Scope.TEST, numDataNodes = 0, numClientNodes = 0) public class SimpleThreadPoolIT extends ESIntegTestCase { @@ -107,7 +107,7 @@ public void testThreadNames() throws Exception { + "|" + Pattern.quote(ESIntegTestCase.TEST_CLUSTER_NODE_PREFIX) + ")"; - assertThat(threadName, RegexMatcher.matches("\\[" + nodePrefix + "\\d+\\]")); + assertThat(threadName, matchesRegex("elasticsearch\\[" + nodePrefix + "\\d+\\].*")); } } diff --git a/server/src/test/java/org/elasticsearch/common/logging/HeaderWarningTests.java b/server/src/test/java/org/elasticsearch/common/logging/HeaderWarningTests.java index 997b076b328d9..3b4834d7ad0b4 100644 --- a/server/src/test/java/org/elasticsearch/common/logging/HeaderWarningTests.java +++ b/server/src/test/java/org/elasticsearch/common/logging/HeaderWarningTests.java @@ -12,7 +12,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.hamcrest.RegexMatcher; import org.hamcrest.core.IsSame; import java.io.IOException; @@ -26,10 +25,10 @@ import java.util.stream.IntStream; import static org.elasticsearch.common.logging.HeaderWarning.WARNING_HEADER_PATTERN; -import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.matchesRegex; import static org.hamcrest.Matchers.not; /** @@ -37,7 +36,7 @@ */ public class HeaderWarningTests extends ESTestCase { - private static final RegexMatcher warningValueMatcher = matches(WARNING_HEADER_PATTERN.pattern()); + private static final org.hamcrest.Matcher warningValueMatcher = matchesRegex(WARNING_HEADER_PATTERN); private final HeaderWarning logger = new HeaderWarning(); diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index 9e1bd96f4a3b8..b83334ec68fdd 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -178,7 +178,6 @@ import static org.elasticsearch.common.lucene.Lucene.cleanLuceneIndex; import static org.elasticsearch.index.IndexSettings.INDEX_TRANSLOG_FLUSH_THRESHOLD_SIZE_SETTING; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; -import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -195,6 +194,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.matchesRegex; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -1431,7 +1431,7 @@ public void onFailure(Exception e) { * the race, then the other thread lost the race and only one operation should have been executed. */ assertThat(e, instanceOf(IllegalStateException.class)); - assertThat(e, hasToString(matches("operation primary term \\[\\d+\\] is too old"))); + assertThat(e, hasToString(matchesRegex(".*operation primary term \\[\\d+\\] is too old.*"))); assertThat(counter.get(), equalTo(1L)); } else { assertThat(counter.get(), equalTo(2L)); diff --git a/server/src/test/java/org/elasticsearch/indices/IndicesServiceTests.java b/server/src/test/java/org/elasticsearch/indices/IndicesServiceTests.java index 846625fc4f790..60545ac71b2bf 100644 --- a/server/src/test/java/org/elasticsearch/indices/IndicesServiceTests.java +++ b/server/src/test/java/org/elasticsearch/indices/IndicesServiceTests.java @@ -61,7 +61,6 @@ import org.elasticsearch.search.internal.AliasFilter; import org.elasticsearch.test.ESSingleNodeTestCase; import org.elasticsearch.test.IndexSettingsModule; -import org.elasticsearch.test.hamcrest.RegexMatcher; import java.io.IOException; import java.util.ArrayList; @@ -89,6 +88,7 @@ import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.matchesRegex; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; @@ -606,7 +606,7 @@ public void testConflictingEngineFactories() { ); final String pattern = ".*multiple engine factories provided for \\[foobar/.*\\]: \\[.*FooEngineFactory\\],\\[.*BarEngineFactory\\].*"; - assertThat(e, hasToString(new RegexMatcher(pattern))); + assertThat(e, hasToString(matchesRegex(pattern))); } public void testBuildAliasFilter() { diff --git a/server/src/test/java/org/elasticsearch/plugins/IndexStorePluginTests.java b/server/src/test/java/org/elasticsearch/plugins/IndexStorePluginTests.java index 7d695a238f242..5a2d9480a95e9 100644 --- a/server/src/test/java/org/elasticsearch/plugins/IndexStorePluginTests.java +++ b/server/src/test/java/org/elasticsearch/plugins/IndexStorePluginTests.java @@ -21,9 +21,9 @@ import java.util.Collections; import java.util.Map; -import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.matchesRegex; public class IndexStorePluginTests extends ESTestCase { @@ -112,7 +112,7 @@ public void testDuplicateIndexStoreFactories() { assertThat( e, hasToString( - matches( + matchesRegex( "java.lang.IllegalStateException: Duplicate key store \\(attempted merging values " + "org.elasticsearch.index.store.FsDirectoryFactory@[\\w\\d]+ " + "and org.elasticsearch.index.store.FsDirectoryFactory@[\\w\\d]+\\)" diff --git a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java index 00b92eac40d7f..4155472b42640 100644 --- a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java +++ b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java @@ -50,7 +50,7 @@ import static java.util.Collections.unmodifiableList; import static java.util.stream.Collectors.toCollection; import static org.elasticsearch.core.Tuple.tuple; -import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; +import static org.elasticsearch.test.rest.yaml.section.RegexMatcher.matches; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; diff --git a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/MatchAssertion.java b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/MatchAssertion.java index 4ecf86081574e..34fa178a1853f 100644 --- a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/MatchAssertion.java +++ b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/MatchAssertion.java @@ -21,7 +21,7 @@ import static org.elasticsearch.test.ListMatcher.matchesList; import static org.elasticsearch.test.MapMatcher.assertMap; import static org.elasticsearch.test.MapMatcher.matchesMap; -import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; +import static org.elasticsearch.test.rest.yaml.section.RegexMatcher.matches; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertNotNull; diff --git a/test/framework/src/main/java/org/elasticsearch/test/hamcrest/RegexMatcher.java b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/RegexMatcher.java similarity index 86% rename from test/framework/src/main/java/org/elasticsearch/test/hamcrest/RegexMatcher.java rename to test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/RegexMatcher.java index 295f817b96afa..b7b1946a82b9b 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/hamcrest/RegexMatcher.java +++ b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/RegexMatcher.java @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -package org.elasticsearch.test.hamcrest; +package org.elasticsearch.test.rest.yaml.section; import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; @@ -16,17 +16,17 @@ /** * Matcher that supports regular expression and allows to provide optional flags */ -public class RegexMatcher extends TypeSafeMatcher { +class RegexMatcher extends TypeSafeMatcher { private final String regex; private final Pattern pattern; - public RegexMatcher(String regex) { + RegexMatcher(String regex) { this.regex = regex; this.pattern = Pattern.compile(regex); } - public RegexMatcher(String regex, int flag) { + RegexMatcher(String regex, int flag) { this.regex = regex; this.pattern = Pattern.compile(regex, flag); } diff --git a/x-pack/plugin/deprecation/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/deprecation/DeprecationHttpIT.java b/x-pack/plugin/deprecation/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/deprecation/DeprecationHttpIT.java index 99ffb4d11660e..73c2fb607eb17 100644 --- a/x-pack/plugin/deprecation/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/deprecation/DeprecationHttpIT.java +++ b/x-pack/plugin/deprecation/qa/rest/src/javaRestTest/java/org/elasticsearch/xpack/deprecation/DeprecationHttpIT.java @@ -46,7 +46,6 @@ import static org.elasticsearch.common.logging.DeprecatedMessage.KEY_FIELD_NAME; import static org.elasticsearch.common.logging.DeprecatedMessage.X_OPAQUE_ID_FIELD_NAME; -import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -56,6 +55,7 @@ import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.matchesRegex; /** * Tests that deprecation message are returned via response headers, and can be indexed into a data stream. @@ -152,7 +152,7 @@ public void testDeprecatedSettingsReturnWarnings() throws Exception { final Response response = client().performRequest(request); final List deprecatedWarnings = getWarningHeaders(response.getHeaders()); - assertThat(deprecatedWarnings, everyItem(matches(HeaderWarning.WARNING_HEADER_PATTERN.pattern()))); + assertThat(deprecatedWarnings, everyItem(matchesRegex(HeaderWarning.WARNING_HEADER_PATTERN))); final List actualWarningValues = deprecatedWarnings.stream() .map(s -> HeaderWarning.extractWarningValueFromWarningHeader(s, true)) @@ -295,7 +295,7 @@ private void doTestDeprecationWarningsAppearInHeaders() throws Exception { headerMatchers.add(equalTo(TestDeprecationHeaderRestAction.DEPRECATED_USAGE)); } - assertThat(deprecatedWarnings, everyItem(matches(HeaderWarning.WARNING_HEADER_PATTERN.pattern()))); + assertThat(deprecatedWarnings, everyItem(matchesRegex(HeaderWarning.WARNING_HEADER_PATTERN))); final List actualWarningValues = deprecatedWarnings.stream() .map(s -> HeaderWarning.extractWarningValueFromWarningHeader(s, true)) .collect(Collectors.toList()); diff --git a/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/cli/LenientTestCase.java b/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/cli/LenientTestCase.java index ab63913760fea..90fcab839da90 100644 --- a/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/cli/LenientTestCase.java +++ b/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/cli/LenientTestCase.java @@ -6,20 +6,19 @@ */ package org.elasticsearch.xpack.sql.qa.cli; -import org.elasticsearch.test.hamcrest.RegexMatcher; - import java.io.IOException; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.matchesRegex; public abstract class LenientTestCase extends CliIntegrationTestCase { public void testLenientCommand() throws IOException { index("test", body -> body.field("name", "foo").field("tags", new String[] { "bar", "bar" })); assertEquals("[?1l>[?1000l[?2004llenient set to [90mtrue[0m", command("lenient = true")); - assertThat(command("SELECT * FROM test"), RegexMatcher.matches("\\s*name\\s*\\|\\s*tags\\s*")); + assertThat(command("SELECT * FROM test"), matchesRegex(".*\\s*name\\s*\\|\\s*tags\\s*.*")); assertThat(readLine(), containsString("----------")); - assertThat(readLine(), RegexMatcher.matches("\\s*foo\\s*\\|\\s*bar\\s*")); + assertThat(readLine(), matchesRegex(".*\\s*foo\\s*\\|\\s*bar\\s*.*")); assertEquals("", readLine()); } diff --git a/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/cli/SelectTestCase.java b/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/cli/SelectTestCase.java index d4e70378627bc..3d148aaf98bf4 100644 --- a/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/cli/SelectTestCase.java +++ b/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/cli/SelectTestCase.java @@ -6,11 +6,10 @@ */ package org.elasticsearch.xpack.sql.qa.cli; -import org.elasticsearch.test.hamcrest.RegexMatcher; - import java.io.IOException; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.matchesRegex; public abstract class SelectTestCase extends CliIntegrationTestCase { public void testSelect() throws IOException { @@ -32,9 +31,9 @@ public void testMultiLineSelect() throws IOException { public void testSelectWithWhere() throws IOException { index("test", body -> body.field("test_field", "test_value1").field("i", 1)); index("test", body -> body.field("test_field", "test_value2").field("i", 2)); - assertThat(command("SELECT * FROM test WHERE i = 2"), RegexMatcher.matches("\\s*i\\s*\\|\\s*test_field\\s*")); + assertThat(command("SELECT * FROM test WHERE i = 2"), matchesRegex(".*\\s*i\\s*\\|\\s*test_field\\s*.*")); assertThat(readLine(), containsString("----------")); - assertThat(readLine(), RegexMatcher.matches("\\s*2\\s*\\|\\s*test_value2\\s*")); + assertThat(readLine(), matchesRegex(".*\\s*2\\s*\\|\\s*test_value2\\s*.*")); assertEquals("", readLine()); } } diff --git a/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/cli/ShowTestCase.java b/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/cli/ShowTestCase.java index e83f1e0046c3b..44aadc3e76309 100644 --- a/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/cli/ShowTestCase.java +++ b/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/cli/ShowTestCase.java @@ -6,12 +6,13 @@ */ package org.elasticsearch.xpack.sql.qa.cli; -import org.elasticsearch.test.hamcrest.RegexMatcher; - import java.io.IOException; import java.util.regex.Pattern; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.matchesRegex; public abstract class ShowTestCase extends CliIntegrationTestCase { @@ -20,24 +21,24 @@ public abstract class ShowTestCase extends CliIntegrationTestCase { public void testShowTables() throws IOException { index("test1", body -> body.field("test_field", "test_value")); index("test2", body -> body.field("test_field", "test_value")); - assertThat(command("SHOW TABLES"), RegexMatcher.matches("\\s*name\\s*")); + assertThat(command("SHOW TABLES"), matchesRegex(".*\\s*name\\s*.*")); assertThat(readLine(), containsString(HEADER_SEPARATOR)); - assertThat(readLine(), RegexMatcher.matches("\\s*test[12]\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*test[12]\\s*")); - assertEquals("", readLine()); + assertThat(readLine(), matchesRegex(".*\\s*test[12]\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*test[12]\\s*.*")); + assertThat(readLine(), is(emptyString())); } public void testShowFunctions() throws IOException { - assertThat(command("SHOW FUNCTIONS"), RegexMatcher.matches("\\s*name\\s*\\|\\s*type\\s*")); + assertThat(command("SHOW FUNCTIONS"), matchesRegex(".*\\s*name\\s*\\|\\s*type\\s*.*")); assertThat(readLine(), containsString(HEADER_SEPARATOR)); - assertThat(readLine(), RegexMatcher.matches("\\s*AVG\\s*\\|\\s*AGGREGATE\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*COUNT\\s*\\|\\s*AGGREGATE\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*FIRST\\s*\\|\\s*AGGREGATE\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*FIRST_VALUE\\s*\\|\\s*AGGREGATE\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*LAST\\s*\\|\\s*AGGREGATE\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*LAST_VALUE\\s*\\|\\s*AGGREGATE\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*MAX\\s*\\|\\s*AGGREGATE\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*MIN\\s*\\|\\s*AGGREGATE\\s*")); + assertThat(readLine(), matchesRegex(".*\\s*AVG\\s*\\|\\s*AGGREGATE\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*COUNT\\s*\\|\\s*AGGREGATE\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*FIRST\\s*\\|\\s*AGGREGATE\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*FIRST_VALUE\\s*\\|\\s*AGGREGATE\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*LAST\\s*\\|\\s*AGGREGATE\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*LAST_VALUE\\s*\\|\\s*AGGREGATE\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*MAX\\s*\\|\\s*AGGREGATE\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*MIN\\s*\\|\\s*AGGREGATE\\s*.*")); String line = readLine(); Pattern aggregateFunction = Pattern.compile("\\s*[A-Z0-9_~]+\\s*\\|\\s*AGGREGATE\\s*"); while (aggregateFunction.matcher(line).matches()) { @@ -56,43 +57,43 @@ public void testShowFunctions() throws IOException { line = readLine(); } - assertThat(line, RegexMatcher.matches("\\s*SCORE\\s*\\|\\s*SCORE\\s*")); - assertEquals("", readLine()); + assertThat(line, matchesRegex(".*\\s*SCORE\\s*\\|\\s*SCORE\\s*.*")); + assertThat(readLine(), is(emptyString())); } public void testShowFunctionsLikePrefix() throws IOException { - assertThat(command("SHOW FUNCTIONS LIKE 'L%'"), RegexMatcher.matches("\\s*name\\s*\\|\\s*type\\s*")); + assertThat(command("SHOW FUNCTIONS LIKE 'L%'"), matchesRegex(".*\\s*name\\s*\\|\\s*type\\s*.*")); assertThat(readLine(), containsString(HEADER_SEPARATOR)); - assertThat(readLine(), RegexMatcher.matches("\\s*LAST\\s*\\|\\s*AGGREGATE\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*LAST_VALUE\\s*\\|\\s*AGGREGATE\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*LEAST\\s*\\|\\s*CONDITIONAL\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*LOG\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*LOG10\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*LCASE\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*LEFT\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*LENGTH\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*LOCATE\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*LTRIM\\s*\\|\\s*SCALAR\\s*")); - assertEquals("", readLine()); + assertThat(readLine(), matchesRegex(".*\\s*LAST\\s*\\|\\s*AGGREGATE\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*LAST_VALUE\\s*\\|\\s*AGGREGATE\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*LEAST\\s*\\|\\s*CONDITIONAL\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*LOG\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*LOG10\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*LCASE\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*LEFT\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*LENGTH\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*LOCATE\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*LTRIM\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), is(emptyString())); } public void testShowFunctionsLikeInfix() throws IOException { - assertThat(command("SHOW FUNCTIONS LIKE '%DAY%'"), RegexMatcher.matches("\\s*name\\s*\\|\\s*type\\s*")); + assertThat(command("SHOW FUNCTIONS LIKE '%DAY%'"), matchesRegex(".*\\s*name\\s*\\|\\s*type\\s*.*")); assertThat(readLine(), containsString(HEADER_SEPARATOR)); - assertThat(readLine(), RegexMatcher.matches("\\s*DAY\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*DAYNAME\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*DAYOFMONTH\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*DAYOFWEEK\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*DAYOFYEAR\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*DAY_NAME\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*DAY_OF_MONTH\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*DAY_OF_WEEK\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*DAY_OF_YEAR\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*HOUR_OF_DAY\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*ISODAYOFWEEK\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*ISO_DAY_OF_WEEK\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*MINUTE_OF_DAY\\s*\\|\\s*SCALAR\\s*")); - assertThat(readLine(), RegexMatcher.matches("\\s*TODAY\\s*\\|\\s*SCALAR\\s*")); - assertEquals("", readLine()); + assertThat(readLine(), matchesRegex(".*\\s*DAY\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*DAYNAME\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*DAYOFMONTH\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*DAYOFWEEK\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*DAYOFYEAR\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*DAY_NAME\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*DAY_OF_MONTH\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*DAY_OF_WEEK\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*DAY_OF_YEAR\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*HOUR_OF_DAY\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*ISODAYOFWEEK\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*ISO_DAY_OF_WEEK\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*MINUTE_OF_DAY\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), matchesRegex(".*\\s*TODAY\\s*\\|\\s*SCALAR\\s*.*")); + assertThat(readLine(), is(emptyString())); } } From 1cfa86ee130366cf7eb9d72d720389e1c6974f54 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 22 Feb 2024 08:26:31 -0800 Subject: [PATCH 153/250] [DOCS] Update anomaly detection jobs health rule details (#105716) --- .../ml-configuring-alerts.asciidoc | 144 +++++++----------- .../ml/images/ml-anomaly-alert-severity.png | Bin 250047 -> 247482 bytes .../ml/images/ml-health-check-action.png | Bin 125191 -> 137363 bytes .../ml/images/ml-health-check-config.png | Bin 230119 -> 229179 bytes 4 files changed, 59 insertions(+), 85 deletions(-) diff --git a/docs/reference/ml/anomaly-detection/ml-configuring-alerts.asciidoc b/docs/reference/ml/anomaly-detection/ml-configuring-alerts.asciidoc index bf98327807e70..2e678b929d296 100644 --- a/docs/reference/ml/anomaly-detection/ml-configuring-alerts.asciidoc +++ b/docs/reference/ml/anomaly-detection/ml-configuring-alerts.asciidoc @@ -5,8 +5,6 @@ :frontmatter-tags-content-type: [how-to] :frontmatter-tags-user-goals: [configure] -beta::[] - {kib} {alert-features} include support for {ml} rules, which run scheduled checks for anomalies in one or more {anomaly-jobs} or check the health of the job with certain conditions. If the conditions of the rule are met, an alert is @@ -83,73 +81,7 @@ TIP: You must also provide a _check interval_ that defines how often to evaluate the rule conditions. It is recommended to select an interval that is close to the bucket span of the job. -As the last step in the rule creation process, define its actions. - -[discrete] -[[anomaly-alert-actions]] -=== {anomaly-detect-cap} alert rule actions - -You can optionally send notifications when the rule conditions are met and when -they are no longer met. In particular, this rule type supports: - -* alert summaries -* actions that run when the anomaly score matches the conditions -* recovery actions that run when the conditions are no longer met - -Each action uses a connector, which stores connection information for a {kib} -service or supported third-party integration, depending on where you want to -send the notifications. For example, you can use a Slack connector to send a -message to a channel. Or you can use an index connector that writes an JSON -object to a specific index. For details about creating connectors, refer to -{kibana-ref}/action-types.html[Connectors]. - -After you select a connector, you must set the action frequency. You can choose -to create a summary of alerts on each check interval or on a custom interval. -For example, send slack notifications that summarize the new, ongoing, and -recovered alerts: - -[role="screenshot"] -image::images/ml-anomaly-alert-action-summary.png["Adding an alert summary action to the rule",500] -// NOTE: This is an autogenerated screenshot. Do not edit it directly. - -TIP: If you choose a custom action interval, it cannot be shorter than the -rule's check interval. - -Alternatively, you can set the action frequency such that actions run for each -alert. Choose how often the action runs (at each check interval, only when the -alert status changes, or at a custom action interval). You must also choose an -action group, which indicates whether the action runs when the anomaly score is -matched or when the alert is recovered. For example: - -[role="screenshot"] -image::images/ml-anomaly-alert-action-score-matched.png["Adding an action for each alert in the rule",500] -// NOTE: This is an autogenerated screenshot. Do not edit it directly. - -You can further refine the conditions under which actions run by specifying that -actions only run they match a KQL query or when an alert occurs within a -specific time frame. - -There is a set of variables that you can use to customize the notification -messages for each action. Click the icon above the message text box to get the -list of variables or refer to <>. - -[role="screenshot"] -image::images/ml-anomaly-alert-messages.png["Customizing your message",500] -// NOTE: This is an autogenerated screenshot. Do not edit it directly. - -After you save the configurations, the rule appears in the -*{stack-manage-app} > {rules-ui}* list; you can check its status and see the -overview of its configuration information. - -When an alert occurs, it is always the same name as the job ID of the associated -{anomaly-job} that triggered it. If necessary, you can snooze rules to prevent -them from generating actions. For more details, refer to -{kibana-ref}/create-and-manage-rules.html#controlling-rules[Snooze and disable rules]. - -You can also review how the alerts that are occured correlate with the -{anomaly-detect} results in the **Anomaly exloprer** by using the -**Anomaly timeline** swimlane and the **Alerts** panel. - +As the last step in the rule creation process, define its <>. [[creating-anomaly-jobs-health-rules]] == {anomaly-jobs-cap} health rules @@ -197,36 +129,78 @@ close to the bucket span of the job. As the last step in the rule creation process, define its actions. -[discrete] -[[anomaly-jobs-health-actions]] -=== {anomaly-jobs-cap} health rule actions +[[ml-configuring-alert-actions]] +== Actions You can optionally send notifications when the rule conditions are met and when -they are no longer met. In particular, this rule type supports: +they are no longer met. In particular, these rules support: -* actions that run when an issue is detected -* recovery actions that run when the rule conditions are no longer met +* alert summaries +* actions that run when the anomaly score matches the conditions (for {anomaly-detect} alert rules) +* actions that run when an issue is detected (for {anomaly-jobs} health rules) +* recovery actions that run when the conditions are no longer met + +Each action uses a connector, which stores connection information for a {kib} +service or supported third-party integration, depending on where you want to +send the notifications. For example, you can use a Slack connector to send a +message to a channel. Or you can use an index connector that writes a JSON +object to a specific index. For details about creating connectors, refer to +{kibana-ref}/action-types.html[Connectors]. -For each action, you must choose a connector, which provides connection -information for a {kib} service or third-party integration. You must set the -action frequency, which involves choosing how often to run the action (for -example, at each check interval, only when the alert status changes, or at a -custom action interval). You must also choose one of the action groups (for -example, the action runs when the issue is detected or when it is recovered). +After you select a connector, you must set the action frequency. You can choose +to create a summary of alerts on each check interval or on a custom interval. +For example, send slack notifications that summarize the new, ongoing, and +recovered alerts: + +[role="screenshot"] +image::images/ml-anomaly-alert-action-summary.png["Adding an alert summary action to the rule",500] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. + +TIP: If you choose a custom action interval, it cannot be shorter than the +rule's check interval. + +Alternatively, you can set the action frequency such that actions run for each +alert. Choose how often the action runs (at each check interval, only when the +alert status changes, or at a custom action interval). For {anomaly-detect} +alert rules, you must also choose whether the action runs when the anomaly score +matches the condition or when the alert recovers: + +[role="screenshot"] +image::images/ml-anomaly-alert-action-score-matched.png["Adding an action for each alert in the rule",500] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. + +In {anomaly-jobs} health rules, choose whether the action runs when the issue is +detected or when it is recovered: [role="screenshot"] image::images/ml-health-check-action.png["Adding an action for each alert in the rule",500] // NOTE: This is an autogenerated screenshot. Do not edit it directly. -You can pass rule values to an action to provide contextual details in the -notification messages. For the list of variables that you can include in the -message, click the icon above the message text box or refer to -<>. +You can further refine the rule by specifying that actions run only when they +match a KQL query or when an alert occurs within a specific time frame. + +There is a set of variables that you can use to customize the notification +messages for each action. Click the icon above the message text box to get the +list of variables or refer to <>. For example: + +[role="screenshot"] +image::images/ml-anomaly-alert-messages.png["Customizing your message",500] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. After you save the configurations, the rule appears in the *{stack-manage-app} > {rules-ui}* list; you can check its status and see the overview of its configuration information. +When an alert occurs for an {anomaly-detect} alert rule, it is always the same +name as the job ID of the associated {anomaly-job} that triggered it. You can +review how the alerts that are occured correlate with the {anomaly-detect} +results in the **Anomaly explorer** by using the **Anomaly timeline** swimlane +and the **Alerts** panel. + +If necessary, you can snooze rules to prevent them from generating actions. For +more details, refer to +{kibana-ref}/create-and-manage-rules.html#controlling-rules[Snooze and disable rules]. + [[action-variables]] == Action variables diff --git a/docs/reference/ml/images/ml-anomaly-alert-severity.png b/docs/reference/ml/images/ml-anomaly-alert-severity.png index c93aaf6175cf88cd3373d48565e26cbb60055a57..acbdce91e605aa2c7e5ea5fe42ab7d2c497294e9 100644 GIT binary patch literal 247482 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


    t?rMGx7$Wvv(d6@nO$vTp2|L*u?33$i!P_ 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
    =L%>&t>WjMoNgtoy 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

    w{hp_m@>3QBq?fztBe$ddPi%a9 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%0DGaZ^HDxuNttaiP=2 zp!d^LU=4nRgQc+Em-miDZb(50*i%KC z*lZZL3yqAzo=E#ufT5rxgdbOOVlkF@&-e|{opLa>!jqlQF>ec|s&f-y3|Skhj!Ro& zGM{TZ$-RMgXCdEP!!vH0#1%|d9}61w52KFdu-3z-B<=2;Kidy83cLAn>oy`cQE>C< zdbuxeiRp8qTUCx&RzAN=PKish)aJ7tRX{|!MyE9Hfg^O<$O;FEq{Vq<1-d6hQM6!u z{_`vCy8g{YO16sq6&jB({#Z!Y8-A=>WyxwF3QRsrcdf+!K;i|<_-lY@l2PkT7=%}q->^2EFFkZh;IWLnc$I0P8~Vg+wQscH!tUj3M!}W6QdrjAwR_&Au*R7SiMmg@|Ei^ac%_1&x$|K%@HjN z%oP_x5KG4qtNSFs>9rD;Ct|J$c~3l>Fy+rm#7TF*p^K{-Y!~j(Kl%JQ93B4ZSWzd< zXeRhOmDBYx1z~j)oKb^^+ePAo+v`g-=Wsz0VyJqhO@!iD`@9ex=+BnwUm=aUoyyhf z)NZB$$9QgGk5IRE_w&4*osZU%a}D}W_l!D*M}5TC4Ra#)<9%~NA8ot1+(r1Jjs;UA zFL;0E@_AIC=n=n;L2cyq7sC!1=a=*fs|N8eb!ywFwb?4q2hHJ5JIgy`7asR5#kG$K z_jW?=3(mXN2YGMbCC~)k+$F?(-rbw3KR2p;Oz=wW9P(B5Q#j-{Kve2%@)7&@XY^B> zI;B(|QM#7J)zQd|J}1ZiH4~J$64em<{e@r`)Mx4iI%{JJC6%I;S@0ydR3P%#VLfK{ zK?H?#MR*|RYxyU(;Op#V5S4tOati(4;^|vmZBWn?|8SFlvLg@X5CD_AxfSv1$Wt5X ztWtYykI&|;S@}dVjQ{3rh}Do@_u2qnyLjIYz0kYiCC$>9HoLm`{R)e5gwCfGdICDk zK`P?t9Vs7lw#i~xjLV9)A@T0FwSArLatMtr4dY^adRl6gEw~f4myocJs*~7;C#dgi z@C&s!DbTuVoDRm7q0-G#?HcSNjSF49juf~E=&*9e2Vt697V7qfskH&}&bOWgo+HJ5 ze2YKE*)DY^%`Jgvf(H~?cC5TrqgpT8xJuE?}0Ss{^^W$R&x}OiXpBc#ZLC?&&bZx zsBMO1JccuvGCUT3m2~^$gi6jLTi!Rx!cBR!EvsJDSTG$gYT;Y?2Ck79TH_}Pfi9zN zvm(c(L~Dml>$oJHv>M}_0>!c)^xaj)FKEu58o@Kn?T9-SqcS8`%1OC*hxfEpp}-v6a%S)E&dGK&wtU@vEp{G_5>8kb-0v=(})l&%&^?^zL5!%Znc4Z0>q5F? ziUq=&By2#kOh%g_qr5-hiWjoM?TR)c3^YW{G|_M%gqbXoRLQqi^V(>bDT zgLwsygIO*Pe#)f^b)cLpYaT{FnbN$fb%33o+D*Il0_ET5SDf3H{COo#K|_ z3|k?TG8V`D3RZy)8}|S)5w^J$qVR(Y0}TYGxT&x#$uvKoA?%a;Bqa}TZ}cbMTZT?9 zHw#3Q@ZyfSDeyvS2>i=kCGmEozPqxHrIs`AoS&HTY1hGa;;z+B%Ppfl`w{b^Chcly zmAcuUMCnkW?cZ;_sX-cMn=Y{ow)5txc>hqS$wh|8_SM!+^Oh1~{1(~Dcp)m$c6W}G zm2Z~HylNLcYM#qkvuMa54Z}?Fa^mP|dRS?&Ja_lqZViAV_;;`_RFLqg&^tS5C5~X0 zbz>~e!1h&?ef(DE6=L+7lTF~W6n zJz~@2^3P!}U!vRNwN%g%&qI8=;|cE{w~xQmd_BCN`akn@pkHGfDFgTds3 zG;Nj-mBAxPn0qjJ^7t!YF>+Oi5ohUBhdi*Q(ZWY-3LFob^u`?+Hkvw}>=F(bvQ`Vyx}W zr*!68C>X>YxZLU#T`q<7m39$vc|zU;(xiSC_euw3al#4&d_f&E70pklt+4Q8hjYMU zbY6<0MaPtloa5?6j#%@dg%6R{=_GBs8&;Q8E2)q=>$J*CfP-k)?3cfDSW2E@bLp|} zo~2MBBENyBb#=`ZbYT@Z|AMr?1bUJSh$7MBO1oqn3!6T^EO(zMHZHf3PUK0;XQyqZ zw|-NZ%q0dWi^CV#6z2W!t<2={x6m}dx&qMW! z!n$$wyb)`^BEQy`S4m{!^2)lwqQeIE4APak`|?%mM|^c+9hV9B8%{m#`XMj(OcX_z7Hn7OB{6MuaN7hJ*or)F( zs~v(UN2GY56I(Fb3C;$}g8sGY)iH~G(M$;v4m!C*$*rbbWrINk?rRgFNAEw1qnCVic<9|$qkdGo4i=Oj)k7;$|z}D z>zQ!-jkR#%FDtZkp5p!-H&zE}%Ayn$`0#rtDsXir5wFb0{Z);yJ+N5@TF53Un1)vg zpM%4U3hu{CFBzkf*iVJ>^El#OnHN979!i?@Hd1@;6%JUWD-|)}#Ed$Z zUTB+4?M{uD2_|h8(JJL2XC=4wjuX~5Z$#4<-U>uZ$bIY<`I7!aKw!_9Gzp^mJ)A`s z0;9P7@s;Rg3bkoVU8q&0=~x$54szQDrROzrG)}hkte4dg6f8vBd~U}*B3|+;7rY!& zil+|;r9)*~%b2~*TbMhH{PmX=_(FItE76uF!m0HiX5RvNlHf%%de5V-G3a2X3ZY2D zg!*H~a3*;O>2*Ps(oRNdAu2AZUz`1$!1(d~w16;TT>6JjGkHS~I^{4{b%@F138YkPd8(Aww+^(zxb=0&d z0ku4;^aNLaDsq6iL2>NGbesT^zwyJHr*tE68Z~Ph_MSFiiHk=#4}N$uMC=mQ>L2yC zEod6hbmut>Eat|`6M+Zb-U2mNi)TK!&!!_q(}RtWaDwF0#5WYFaLO%E zuVXFC;>Z`eb4sN=wQ5wx_hXrjOp>Z5mUNN2?x|$qf#T9~u`HL1xRU{-y4OpF_$$N~V?L>T5JASG(&05`8j78MJsjSM(RXMueX(}qzBg}nNkOYw zcU8<480>wf$vBvOs(w-4ne)h2wZ;)WyDIQ`vm0qn_bo=W*bqaXzp?46ZpRrc_euu`sF`$8F= z@KOcT%tuc>fVII8kYjx@Gpdb6D}rLe7_lkg^T>A}yY^WNlu5Q~XDZ)fXU@)H$8a9% zM>+2=a@(%F;H90YgI;Qu{Xg8jRajN)`Uk2=H%dq&C7l9Fhk$fXIwYl}y97j#?(XjH zZlt9Bk2Y!&-|F_x`4qYW@8|R1 zr?J>XUIJxhm0t2BMsM5no7ldUG>NF9MBD<5eMjyzMab8nl}m{GSPB;E7alINbzynw zmg!te?iB-b788Q;jC0(+$mat z<~R85>Vz*07|BNpYLgoW3w@w!qZ46=6tZ1)s3VUGIfmb8aIu**XFeP@tYU6nH6q%7 z&Mb|Y$rw1-VIZ^F6Foa2U4KShDCCo%ZY%MMO(@}Nu-n(`dj$8A82Sy$6lO9{>;k<- zvWCEpVG!QODf)fd3nNs#_T5msWQ)`k2|}*3(peD#Mim*sL-iBG7EdBp!gdz(&7X44 z_Kc@|1?67BAHe)^)4^mvM~=&)cVdDo4rColK4VwAW!;fKl)UWSCkJ5CB53Um|?jK^&+$?z3Ty)cfo^N7AA zWGX+JlA_!y>$I5sA-8II5zBR9qh(Rrv=)&;Af@@xJ*!-z^6HfN~sUd3n@+j|N zjYpcG`U#F}`ZGP!pr>=M12!Dl#zb*FpeuOjG_kh@$A#+JClnw&6cX4m{V^rmjwmNM zbmuwssWCxxTor=6xh6BmHGXz>F+`e5Df7*yG!kE~GwZhH;7+ThoEfG*0&fRR?X|<=b!h z8IZwknr<}=9(47ReVhy`!K+u^9hh2qin~?uCO?}s>b%nLlAEqg`{8Tp1)1#bZeHYU zzPVSbq$VmFpb~%odp2ZyLs)ul*ZSqphr0`REPTq8>p@q+o{9ica!8e9bs*7M@*9g0 zi1tZ}+qn{GFRaG1l^B;VJ_C)sH&t@4>>K4Gd9g=o zYyobB*UJ&#cyU)LV!S_?P_19v<$T*QP*!a@zDT|ux!0n(|yQR$<=j-!Ye>ChU zKU2S#K_wifMIrdb&5pmToipMj=E5{Rg~vYG5wp8BRTZ18WadGc3aD(X55!4PtH~e? z`Cjp~A4K2P5EiRgR(w6(KZ&-EvdFH*iuN5pxlNT%Cs`!l*ZY8SMpr5kMjDcL>6$h< z1sHY33X5vUl%|%!f{QS17wHKzgG4kPM8Q!M_fewy!t>IkBw!b{h&$)0m&3YIPp61eubmSgBj?g(y8U z1n|4vHyovta+3qNLd5P6{4E8Ky&GZnpi5!LtFQcW+xy4R>!PFvQF>ZmpBbZ-NwiMa zUZ$ZobosZ~VbiOeJA1p6^|tC#mA-i<5L>6OW3G@8-S8w{2?n?S|qQVp4&&9VGujp0E!TLTgJg};3a>If!NTuh(U*OHTGZVk6wS@Tyz z8H|=lcK!}9S=R5}PICvMJ#D@j8N#jO!CI{Zx-xC&U>eC~H45Y3!^7^vd$~&2<>~17 z-vAI5UHO=Ud}17J zigaAK=LIFq7zkR3uqeBXJ_K6%X}giXY`0$#_p%O0RvGQsu6Co)DWkjg9%Zi~9K1X_oAYxk^)4~I6r9fesDhtO04opSdgDl=iD4Cok0O{B)V7}=|EPyR?UJ}|NW>rqu!`}v!%?5@Kd96J2l=1 zS1I!-z4?G$1508w#o>5zvGCanp3%XCmi*>K`-*wCU6{kJ+NFdh{oU!0!Nbtpufc3a zS|GREUFkL`dA0>AmWLZ{9^6BFZBNosP0JyjxbXrb9MJ$w?dwk-$+IPFW7tsfy787T zF1pwBzHi_chouR@aY{cW4s_{ROy)Bt=nsAMEYs;^ieMNGGgh{c6UMDddrB;VA_AiW zj!TVc8PCaxpHH+OUd4GJ=_JkloJ%vKOEHlM*+jj@q?oR(-JOq=Q{?7dc?#FX-R?!k zougzOf63hsZbs5=7iP{glRvWnnq9B-wq-Q#w%P`MjIX9(8xcjqB(X;jSqg+f!F*TSGT)`a0j7A^E@e)oLC0O*zsyswsqV2i z>wv2YF&aLUO`N&ex7G9%TjKp#tb6%sW9|9YNG^THoJDAnpYu>;=*8>^C?fiJ zjwqOIO;js~T`wa-I)KP8GIe zQCNpM$Y#_RXE2~E86v_v( zMYwzP6R3VLZ^!rqQn)%y^yJcAmeZbe9F9++dt7dj`!?bT2A(lF#f_73{v3>+%)1LZ zYA<&j>Cx){ywof-TXm3K)8X?(JI0&B@6Kv>nprIRO`Sf(Tm44DSbvWHuzxIP=*NVq zaujWkw{t4iNWhz79O%iw?Yt>wsiQ23n8+O=&JY~`A4CpJ=V)IS!pKCkK4whWoh&ri zg~Os~oz6zR>f^@UM-D{+aYt&X_^_x1H1NTJI4K9p^e8oEX0v`XsnRK&^vcCrVQ})i zBwMVRHj3o)A$sjsJwT%S{rQd~qbG8BxgKDR#<^o9~d~O zc8a?;P83Wi@ChRX#haxwufyqBS$0?q3mWHw!4StADFJs)SP^I1=WAl*TCe=3OaIgz zX+B5!++(S|egZWtHXhzfV}gYef1L@1U$Z-QV5$MkDsHvCiS3c-uwo==Epv;JeGC)oazjlW#9Y zn)|HI@ExU^GFGmzXV9LXvs;z?iaosfa)EbY^cqTPl}n1r!(ngl5$3Z5@&F=ks#H|u zmxsZNjBDc*O#{&sQv7Gora7UZ=scf$SdkA{9C5mAP>kuMW-B-Dn_n0P7>irtDnh-p zHk|S&LjH;NVAI(V)%(U!IkHH-=2fmgk*h%4R6{O0mFaxT`TKEH2FL~Rg5NoUz6&MR zEw%@<64XauM;wvVPp6c!`*Av7_dla9o_`Oxa+JHcxDI4Mefx0ksMcnJgDAb~^#sOd zraa3_sp|ft=LGUS6xJ=(NAzFA{Oya_$%M^Rs8Dq+l3bf)uqK|UldgX(9s3fROay)xe`WH4Y^l?75vblegjp+O2bG5u*nhkRBTXhZBy($!>MJG zZ)^rvodX8Age32aj$n9`^P0JpleXK-HhLrGj1|5*`3YlTdASTO#Q#_f=If)nahH6= zNchbjeb3}WlVF;GVrmaxAs#wBsy4+gRD{Zl!fH+5;$xGDZcZkUJ}fg6KwYtRIx7h8 zzT$`ES%Q%z&XWhF#*2N}Uo7!V<9ijfj%umuRpr5Ah3H`LQJx(^2(MbJcmi~B4`@_+ zm9-fwbT=Xylt#^}qc2D8z#s%u!oH{CyA)qaMqUS}M7CV);YdFH3_XeSvQ_p22|83u zflgI{^26G5KTeCqc~8YIw?}&C&sTaFsMo^2pj>!Y@3p6&f$^cui}oN3uMu*pbB-U$ zA+R%f*VxyEOh`EfG&eO+t8MGB9@2KRJ4T%QFQ$tO-`S)z;|b{f>RV>e>yerqi;j2g zEO6Yb>NcEEyT1Y|tC15a)1>dyQaT}soaSB*@niz7-)WR+VwD)gS}mY}&UpX1BR(;M zKaR)zfphJ%znJ#;PoD!T-;>4I=hMBe3%xaj&DM`fqrPN=W8fm$61b$tO6e9ue`(Qn zt9smeb1<6iTBP$%##(2@mx76P6rcF)8rc$8eO-YZan1A=@UxmC*3qJ z*)>T2z%~BN)L>3{;?3N%d(yEO_IMF|g+`wz@Oqr~819`RZm1%T#K7Hv9Ux8tK7L(hrRw6`f$sVxa?tWtevKH6J0AKVNb(Bp=5) z+OYqACx&N)+R2HgEGgx6tIDAO82ZJRVr4An5lc&KwXymxn9` zy+1(()C{X7yXb#@>pyQtcZG_3CVL89E_3KYG>@0JiNGK;`9EEumc;OpbgJ^8#>d=PtSgS5lO^WhJuGlBmVQp2BnfKLZC^efaO8V1S6`yxeM41i(1CUlY0WYRC?)&v$3C zK+Ie?cijPl7DH5z7Z%7RXcN=L`Jawf*Vj*krNNjK*QHmeQJFgs0?s&Y%>QX1y_5os z$)(%9)9HUs;$K$5$`9ObR-5|Avj4Swf8LEN9FNY~i>r;1e*v$L-mAa7K#B!$&Zb55 z3cdP^5AqS*_!rBz^e7Zq+DH+V`JZd)3vcOs_91Z~Lo~S8OE{&^^A2wP$4JI#x|2el z_5(}^9vEIo)*oFId4}&ECgh9Szux~B(?5ByNCV2(E;tk;vn z@!0ak(%&av?g?>&5a6r*&Hk^mitJl<-0VtOUi-+NTtss<~#1q6Rj|ZNThhSp9SEe3nuffJd z+VKtL_|xUUj7|M~wv;-whqhal;_sm(YGBbkrs_xj`wIDqffIzVYz8!b%HKj>>g}n5 ziKNsD*^!v^$FWgH&%p?wsz`1$zT40@|91GDR+2K^bY@eiG>&H0>7b+Z`_?ROzsRV6 z*l!?PyV&#Cs80rQbjtmv*sMQ+5>3qoSJoOwl;T zzTq#Pzw-k=k{s!ju(}g$@f-UhjXo=%|Ln{Ex;%98!1Vz2mJH}MFjS!VO(t_`ELs+( zOu^YPuc?*-{eboFzK=ycL7hDBtk-gRBkAH;duoLc5#E_6(T2G&7kli=o??6nfcPM; z+j;{RUL+8Ibe zi;=(9b`O+}lC={A_Zy{)sI;=t}3J4C}SS#Xg}XrE0l;Jzp-| zK1~dxcGT`nh4?q*EuQNqF-1F5CDbuR4U^2kjVmFUm=0od@=|Mhi_FhEL5|M)W!a18 z*4vf9t)<9f(3ktLYY-~JSFDY;9z!KZDhA?;cq?5_3^(7kF)uroXf@IJd4DzkKPT3! z(&Cb~(xjJZ+AfNvTD8xB#C9yhVw0<|84!sSsw_s~X8su^d#{3#Bfl@FFf4S0)2l;5 zt_PXx7)MF~RNq@=%Awd|oqIP{k7K1F>hTdoubry)MzdAu4~5SdU9{V5+$-G>jm)2{ z_3kPETmhah#Hkx_@0?Xd1Do6+!UpOFy6#Y(menN=RSEKTa%b~zJeyoKqb!9BjDKEL zNKX&&zD!#~U(}^Ech8D#GF5~$?f9bJk&WdHk1`AGUV(c8%y>u%E!N}&Cd;_pgu*va zyo_q6aRb_=Drg!-C^SzCd|=PC!?UIEX(>cWZzz-4KT{is^HCo(bMeDu6F&N|cH|i{ z%P3`Jj^(#?Obtc(3t$PU(9{)_%Tm)hoXQe zB3JH2I3f~_FP1!n2l7J=jq#(==@8eQ{*TaK$Nk+)8unX1o2%ZKjPuiqLW*w%D5c}K ztphjtR#@3A?&JI2&#|aJWlN<{^F1`SB^QnTX)en@oGDvH!{z#1UaOvzx?ot+LK%eL zVn$uJ8UbFLfS11FcaeXj%mg3dAobX zLqjW8>nb)l|LZXj%0EECKUMV7^0Razd>ATWgA0|@Pd1S1)%sACO~3AdCDAFCN)Y?T zsXjorEz-NI9gf(Y`~{^Xvubev&Xh$if1fuGFysXCS8CRuKc{kN?|g9N_`}TdCH9&r z%C1M6M}Mk#fl>mIW_`Gzgne{pnaTZD-!wtY^<>amqF13#9%fzqBC>jod9I26BlAkh zmytOOv?JN$)Z|_T!ymU-0a5Md-;s$}239pvABbJwsv8c*?@XJ0_(AP+!cmmDVfB>+(56Jm6oO#z7pm-)rdY4zh;{-Kg_N zW~}_j6VtO+{0lbE(?r#rwt^ayeX4+n!!=;W9IaBfkYJWw`K}9?I|`?@Fjw)dqnJX? z2I=8hpQkQ2s0#)Y4%+?C(71J!fWt}qN261~DQ6+a_JoUoIFG8ldbHVMqf+y|VO;S| z1%7KXcG-E=>uTBn^dJB7m9*LdU&;D-9xIE@@1F7#zV-fTs~1_eTj8_9Iq$#g zvY7tH+|!y6N4b6lw6Rh5cY-;I4mGxF&8|_dr*gUBWaE9C!UjzWk`1|SoUmicLYxj; z6gC?Jm4z{C)sC7no+vO+=z^FC?Lo7#Rc?9XxsD8N2kIT+9ANRU+Q)}1;&ZAsq`kUp z3;Z6f^nyq!>G+lBb66;!gs^IJH5!S+73!ihk2zzZb@+ zn6NYXkRIh4z^6(I#0UG<=CrAn+xt9AQYIZdP`hKC`}lF8ue_xg@A`DNh_ce2-Q zUITZt)ik<);MadG%XFXo@$^LisYE=CuWKOeZ7Z-0 z+#*Ec5hCipV(lupo!4TKog8XaqX5*TD4ymzd2KVyLxww=gF_{d`qTO0j=ImsEOasW zC9e?dg)+8O(b;wBuMC*6%pCEvFV*sbPCwN9s_3NR;q34>pxwMvWkff0bX2O`sJ;T5X%Sk{-$R;IN(G9Mw^u?}<{-kO6637JGzRt2;!<=*ldsY2>QtG9 zPS=q7=8Zp*%0%k~s@KV4oy{h)qDqhAYF7Xs17TDYE+!|CK$B@IF2q(LmfOxCHhs0; zbF&*XP-6ZXxZFrOG?Z|+IHKxtTh=c;a&oniHWsBv7HJMZ(<^6^L--n`lU_D5J&)PE>G5zE zGWiPl9Hzt@UhBwtdcIma>ItZ~n4CmeDnbm>duEy_@a%?*hV)Sp9K~OH{+Jn{G#t;i zQ)rtLyr&V#ZTO6i()nWB5VGDwEQBQj`9?ni0Kky*7q6x1&USpdsy!!sT67~7r%OCq z78%5e6qJG18gb>6KM<{ybjl$~l z9qRjvBCFM!-pv5tZdMo?+5)D4nw|iqJ)J4BO3&%m!l3+dtRVb16yfMF9+!4Qk8)Z< ztKxZ(g*{(xw)I?~G1K$ejcNj5y0mGN$gQ5!nm>onK%T>uJ92g))-7(Z*xEg++F|P9H4hu(2z0%w|g4s$AaPw7T?1o9`!_ zaTc<&7!9q}(kH~%_4~2MyG*ogo)N+y>>eU%ZAYL;g! z;0A-ltE<$oQPL!zBaPCeRck3g>v0;3U@fPLs#Kz-TGwn)eVp{Xbdm+iXQtsGC^9DM z>?Njm^AM`xxr6rX1xLkD9E-?z-zQ7(v%BQe4_i6?CR5B8dtvJaB?omI$&*h9!r%g! zsgMJxedsP{U?Aoqt&Rcp$!V@%tw-Gpvyo%|URGQ3f$nOR?2b${1VJ~LU&l?M+UgHV zM7FY5=uPOU@GvB<6~jGx%^Nn;vyV5Ht!WgoS5`?IcD~28oWAMVKk=pJ=7aY`Q}zWb zgZi_@{eod%Q3JWqkILLm1&fk$2h9r!0V#KQ{D-UB1coivB|F1>9fi8f!H}}{ifGfN zKr};y`_w12MTYV&)$?Psn!^jTaEKHGxoiS;y#hbk?=8*wl&S+G>DAS!U-uj1yt_Cr z3#RVnFrUqj3i8`JIk-BZ(fVo7?=@&lOm#5ujsc`iD`3!~G3O_empAU(rK26lyewM5 zaLc-KyISY4-kr8W6Gj`_0vSDu1bGHtj>4s~9gm9Q+|9CQ2J!#hh>?gqhDv=I3l)bE zdep5N_$nyLmXmxaiU5G#MBh;2J4swU1Ch|TF6V^|Pj}!gqDiQpP&_Mqy_~KKJG-wW z`sS{|#mQq*imxoB=6qpC2pQMGGXcGjfr-SowLSaO$RQT&zFlXfoyzib7Y?sz2vL>2 zCzRRG)Ee@oM4$4t6Gr-GzZ_`kOYbU3Lv;}yElGt9$Qr7un!bfeQSld}`iUL^=W&2S zNR9HKtlhP3yt*>#yOXQ^AOO*LrJ(&@qT#H+$>kRubxn_Z`4qmY)_HwsGpIKRxNWC- z=JW{t=?OUg*(Gp-r=Kgg(4RWbX(o8tfGseP@m@m8iKjK=TMT|3aBS zL-o%`D}DhooeEejTA^=^MJJX?Y=FOq8x(1P-WRt+&n}ppdfg}2m=OtQGwaxVX zbZg8w>&112&Bo`5z1sPPi!XZaOpbB>50`p5ryi!Es!_0g;%IGE3ozozz=NSa1OF|^ zWoFLK4C`NH_+K9@`_CRv=T}tI=e0UjvVF?wr>>!a#XI~(RXDnxT-xCPYzCU@z4T8bK$4(-d?ls{vmJI)ode@I8 z1E5D#UNxO7J$(7bF;XKk;67JKKl;Al+(U|pH3`O>LzqzY=EX1)vys!5uFK_ z+3OIl3vESwQ2X*bo84Wza0*QTm9lV%G13(3{ryFYGv^Q*=BERh#C(p9`W*F|;Mr@q zw#cA4f|VIbNQEJVWb9AHjF^20$9Yhf5&>l`Pm=s+4OxbFniaB=_op_o<-N@wOio2* zFpy-R;S{|PuWT|_P|={3)P4Xpz@Db=-)=LIql)=-3PUY$TWw$$xp_r7U$Jdl!?EH_ z$Ha_^oWvuSq`N4f#CU*TO3Cq z>Y8OLZiQ3gp02CTRi2#pNkk(f88*yFFnx+_alP`_MNfa2*L3(js9yMtEkk3NNb_`8 z^MlYg8UwUKPttIS7zR;A^4S`)%d)V+gzC*{4#Nev?<%6pHow}1O<%-nHaf~tYq!ro z8SLJDS56H{>Qb>1m@({Y6NkAGpk5_%K!C8qy9ebh2JJ53{n^Cn2#6FH@O&!V zUD9GcS^QSjyn?#Tp!FS4bJtB~@7O@TVlifQNEGh(uWqSm%K0`M?NE{l zCbTox?;gF&T5EZyUhV023DWW{ZM4_JT4;qyOqaeoi03$3xfwZ3B^r59<@y2E3fC(E zKWlk7JI)!`pM9K!%+>f(Q=17_w(I>Kc-PltT)8{9;TCUx!`mkQS|(Y;WCFPfR%iWq zsP&>#v)88@B(}Xe+x9Jg6#W%AQ9UsocNNQ5bkm|%aj!X+f&wD25{;1p*-x30aq>AE z1Dd%P1)%SLOar0bFXt|y%MM)Y$H6P)GMyZtln@k@RyB@z3u>{)R#Ej#WfIT;DMDua z1ZmM0mCEaUn3C%EGe+9tAw(8^ppD}`XT=34f4hvaX^`EhZyM|4qG30yLEYa|3O#S6 zz7(rJO?>L1YeQ~rV`^nltmiv{tXHmILqC}${*+WE|8;J_1e|L=nDa5Mv zQ<6Ip2-H~)R?UG$!!52@+~wdmFCmwSrc0xve-2-0)O@q=qM!kWzT|5dBpR`nBVNTl zL8@J9MS&aDL>b@7jeQrpcoS#Tx80anXHA8as5LuCQz@xkv@9qY|HgQ9(~U2gbqSFi zq|Hc~cGFWe!=fVE%xq7U_#z?`XS4Rxt+gdA zqq}#>;~)kK<`|(caKKC#UW8kjnwJE41IitPGqU^{&|w~c1fHZpvGFxkACr%(FY3ZN zC`{xO^7-=Vv>1K>mw#{mfk&m92puk&W!G#S?VClZ173@~xAKGIZ_a$yUv*&#e8Z6F zt5ur-ks7;{Z23xMnB1k$8?xzK#d#3iXCk3qzXwtqQvCabr^p+<18)s$hP+I1BH3co zXq}GrY4(SRR4XqPcDU7oW}eHA2NGNrTYH=3EYy^?e6*mYY;v(1+vra=8V&npuwBad zBHwgcxr~E2~xzhEx?LNDPpY4h>BARge#8FD@Ae{ z`H7r%BH2<8vS^zv_s((4!} z)}f>!V>3~VzCyy@QR+O619H5id!6q0%%9))n0c0_Io3LzWXo%ib11#PVzJv+WO1K| zCtQ2!HlH?sd$>WKA=3O>HW3$0NcJk!YJ&6xLQQ>-5M14k-7{3(kITtymJ;~NiLT-L zyHJ*>G~KE^(~||`$SLs4m1zlEbtG#F{gwQoQ4A0ZVNmTi0-uAi!DwgMw%VmivpSZ2 zP%3w%coJt-6XQvVXi+A zE08UfM8X!eG9(t-VtFqb9TiFa%x8RaM5xKyr%I`&n9$&>{PS6(!uET323}zp1@>FX zB$XA{H?kEyZf{KEMO)VZ!eQCCXjufLW(APJ!u;n{{VcBqZc{-s)w@j`-J1?maaL%{ z=h`9(vLk6hI-j5Q$HtFygB8L2PBS!7T)drjf>$mMJPQnaXa7v*X8sPe9l4eSUDH@E zKafeo+@teBOLX4z^G+Vcyt7f+4ZgAZ(j%?$bI`Cama{Km38WoWnl4Gzumbs|d|G>j z*4*xRUQY0$r)@QRy>kJWX^8B%Tzv!*BbTE#AO2W1b!0!bY z7d;bnjn{57-*j4PES5$Tni^!DeLi>CGlH_))0bDU=F?#poq2AS#{XrduZc&a1H9?H zG}ga-=&J(rwWFG<@h`j&N!J3N@*TxyNJh$pt6M;n;JkeR)*Eq!|B5trYdh1w#OAJ> z&5g0)IaS|UA)6Or-Rf8;fATKHK9X%t^SD^t(gLjJ=Z{wE>!=saVE7L?W;?Fpxs zh`(``gwFxN3c75ll9@sDq20AatMow8U_0tYATmWS)E$zgnOF6B;hQBEpi>@&4Fit0 zmnmcnpE;zZC;=^?vxn}ttWPFe2_p2L8NY6HlPq02?3*uf_oBZ~gysm1$O=-J$sO|c zqWfw3q$dVg_F5X?h2=@c7CXd$EXY$ec2AWoP2K#&=XQNMTLp8Z(X4r&RBcJ4v@J%hw#FRN#sXWR?T*Vyrr3b0xjsP|WY;qfr2{V^h$GNv#{xY<3j}y5ODBc_7B6C;NtG)!Xq0 z(xzio@sv3oFoSs-Tk>&q41XheS6)7?|J+sfV*B7%B#n|F_CN{+n91j}J6k0xox;0^ zaRFo3er(+SRjx1JDDL)p$HMv0QG&XgiX2@VA(BB;p7+MKSx4;x@G*$IEwK#IXe3nP zS%1*Z0wiB_22?8yPz0q4{{e7*AbExHSAO}g$DJsTTyxOLkBoow%m00lmhOx5^zQ}f zzrLXpD6V`-cnQU3a4|2gI-cm;$s1n$_6ZeW(6HiT~%5 zpc?-F-_L%Kg!X*#79hQJDyMk_(;l+H1Uxj4io)wN?Ze!|p(L(yvl(&KmR~rJ^QZu% zsv7Nn5=Z_av^9f{1<1!4F805ZOT{aK$lJo(ta{Vb3eNE#?u#w&H-UzObr}zG^jT}qNs zbnPv*b1G)OVqG-@m*FXs({|qIqN8kim|d(ht#Pehlehvk6Sc-Aa}4i%tyKpNaBT3EyLQoRO;%+z-X2RZ ztOF6j-|nokB>vdC4`zJfpLQRTxEu^F{g@#Ek0HJ_OWU+jG|@H3lDcQxRng%azkG?q zg9&GU`27;9R0^in)7C{dAqV{nWgZAS+@I`+3R!dL4MC#Sc0CGu<9MZXcQK{yqBs63 zjW63`^L3X8l$@Qr=~N-Z-OX9_@o-cHC!5LX4P=%ak%l(8&3jQEn-4`YIuM^v3?y|L z6|w#yUHHmuofQtffrY%F#aV$RiAmf1sD&RZ#&$jGp zlRMe!EPthUVSeUk7W+wT(>x;M{{Wr7efzsFN#Rhr!QO6b;f7wd<{oFJ;liTMcB?#7 zI;n4)DzXqnIgNV&6-VSlg#(d}Jf_r31{Qd)wZsZUDtLc=S@O6iFaqDx-#?|ug~oax zN&Z1GJr&9e104&Q%Imqu`|>r*`7m;$%bT4HBVY?Yu(xmsX;>}Tm`O-ZmOdGhT#`0d zI*Uc4+|-+^-V22zZd{`2`Nt!J+#YOb-#f4Z{rn)usU}q>O1fqrmloZw9q(_ibd`=% zm!peDCh67>ntF;B9_jQtn+-dy?<-=%={_35@BEmu z?)azb=HDH{bF!yur=7p@HES(<^FdrKCGB41TMyn@P72ZUU1{H@f4*H zHh)mNxr!X6V*ef(7@nI;Mc4>Vupm?btOv{_jxQQ9gjq_ukI+FsGVz^Fw+gY9Pau{w zXrkz9-gHV`T`XyaQ%Fz(&tFNVRKlbwe6H`cUz+Y32szYUAxx77UetkCHT1K-qk98! zHsNfhh7+PhJ-iH>^@VTWYqLVvN=MN|sdE|j1j5|BMGM>kX~QOTBZ%y4dWnW+fW1HF zqGH!&0ZWf6mCI>NZqT0+#chP)d>-yt%X3%rzk(fq37$G-OC=1^!`C)3cCe_HuR#@5Om{M&oL+m~-QU51 z9BjRC4jG2tD6SYg?W=88o8JSIX(t<+Qv-dCVf_4tWFk%`=XDf+smUHUa?eg_Ae160 zA%V0Wi-ylZhRb8L%T^e#j{7ZaCxOQ@Qh1m>DVf`#emx(YINFENtIM$(3lG}6-wAi* zLf=j&d1n(i&-4liZAe7V&E%Uz*tX`koJ0-a`@jG>b>7=g@wfLkhzf(V5ckyZ#!Fo; zYPs}UAn{ppzexgV-D(H!Z;b=p--&WL%GF4$c85p+qK3lFdCHNi@uw6ur~OQTSjl3E z`i-CQczV~^B=2*eMG6xmmbN1>I{7Wri$pkEo3uKm%@{hO*S*8u7hR{`9pbSh5%$za zqT+WET|u5jb24qk#z4$F6D76=SMC#qHaI9sGEvkj;`6xf*c7*YC$~n09#pSbW{|R_ z%hI-Dx}O+QbZt42z;z*UwKg^b3DvVHwap73&FV*zZ*FXuhSUQrFhq)r!q2m$N#}|3j@M51z`yQqAFx zZPm0^yB$ZbiKAbOfUM~CY;xwSX%WJAG$$J#6put1A}r63ewwvVYm}XHc}B)8FJrwd zyp3-y9DU=xFKi_-BSPp4l>$M;xLO>$TlS3IoM2j&!3pGQGs7x69VQx|`FoRjq_>aw z)aQ~*g4dX=a`^+P0(bOC=yXmk`n|)>O4%(O_hd`16|#67?gK@0Xo>^adJ7{t4l=#P zn0n4ZE;aI2OJylN02z!d-+WQPmjiiBL>-%;F$I(l$;-#wuKmV}O7$?ZZO#iwnF)GM zx0oeLLd_>KvTrS`*a_XdygAO4!&*bh>^(d+o*LTo35+er`7#VkZWrE#s!fpY5Et#^ z-~FpISQme*Bf&<32~ge@OZ>|dFVpy@d^%R$@N$ELXkc;MCbfqV1K@q33Ba(TRVxg4 z+-h}ruHILMlS)JnM7Qe~IA3{WO_@Rnp}!Z)XO-*sM=L=}v|jTuT#2bq^FL*BJZxb3 zJWo|Xr;)0)J<=neJg>dQo2Ob)4`i2;by|DTOYZ8$-t&SOqfMd~aokR>m86{}ZBec7 zTC;K{Z0OgMUHU#bWGEF(_JR~)yH`}y(Ya;g3aj3YtZOKH=Vw`JgC@V)yEqS`Vj2q8 z%OtjKg9#WoC~76owikx+UPIK{-=3`Z(?8r+2shtutGfmBJ(NoxyGM$uq6w@2Y(3iv z@+|2c>(hl}(Or9)r&d+I_5)*28V+VAJga3>uXNPu_Goi`FU9FM^`nuZyl@uW3_&A3 z_-y%%K`E|^IjoruFgFvp?)Pn@i|D6z4|iBu{GPYb@`zUfIE;W6s9lsw;7%)a=<5Z+ z#xRH+RQV)~b_etohSE`hSlD*Ez51OX937sc+l4smMGI#9_E~=lsSl=I8yo(;T@Eh- zm>P$;!5f z^r&%8P5`w-^bZUWRNVf_Z#5!8(71Y)uFV3c^_m|;0=FtlK`@VJ)R#a4J}bdxL&X9^ zHmCcmNlNtw8eHLF(THwmRn}IMl_)I+haA-58X&Srsn`~Z8*g-aVLVfw`XY#||MHKplb;*h~^u08Hga_Fp5Mh^z%ekRqvZyc=|?mtep=J5(7IP=u{O~f3z z=wguxTHI}=^+|n=FFF(6Aw12JKpjWelZ_9*Jbbb@_o2bQ*i=8C2$ZYHuh-m|3}Z3r z@!U|SfoRTOe>hqyTQVsvinR1S9_J}N!RYZF+K4!LVn3*@3LW1Y^v%-)L5P)?Wk_4m zXE3^XC&$5;h|6TZnzK5?}bPWH2;B)f^oy%YgWe4q4Df0tHie@_& z%p?nX!%By;PY?$ba}3QoQREb~Zg;;+a${XnzQy$NHXMWi7#vP%yldY+gKK73KcLUy zgMWf7@I|Nbt&k4SXp6SCL~jP`1Thuw^|_lc`UV_S42yPuAP`cJ_1O3r1cvc&KuB3{ z?#4%)=01!}D~ou8WsJj_-ux_?1ETVk zkd)J&Vt&E91nSq3;bR|Yu-0Z3K($byDjL@BrJP|1;(`24_RL2R^bb=i3DY4$Y5LInlFfyI%lFS>=v|52 z->Myz>-n0pmE6$G@38))*9xYk<{%?euYL~~#C~Jw5+geBG6KyMNpIm}jUP*o-W(n* z72FQ0Hm%XjjQK+KLXm2zcNgjq49i;5N1Ev)f~4-MN_FL}0j4{r!!|hA>PEvo&K;F^ z2H)4o--cMPr_*Pret54z1YgWGzfUqtDAT3kr9KNJ&vsgZ_j-l>Od_?5O+5OsW(re` z=p4OV`#o?rwK@HAD{td#4(mOo2==+^q4VoIYdEo4Gd!;Wg4=s zfCde_P$N1w?q>(_$M#)fBk4Yp)!K8bJsI@DvTrGO$O#zjI}o9a_42+}Y9eC(FkP~f zFsjE0__2?;4k7uLq}QW?O$^`HFj|cmS7HJ-?wK8QI(r@Jv62UM2=_}Bmb2}3`93El zL@SZ%LRGhF$4kphyWK8nMpfhCuStB-r5w>8O3#`7Kd9Wq)=J4fX`4kVFkbIl_upEb z*&m$sT|uQad1a3dJ@t~9+cb)}R+~unD9~g)ce}bhaZCjc*->fYMh=GPK^|^t5g31i zh4ojM<2Ysb7F1XhI1VKCP}yD1x!dj#F4+GSDf5@Ep4{*eK=`$7Ap2Ps7bu=f;@{c) zKCFXgqx(8ptXeYHhLjh!NV8lThSX=!t8xwaoid#$h7WCmFp&xJ#7Vq%+m(weXe%|) zq`<>f=C4=C{GEsCcx$Yfch4@7E_kH8w=ALi|gTJV1q5^G!6NpTeC$}9WY8>t6h)!nv-c6x zLe%=03+E^DY|!Egqw8vH*v|B3wVLo2mGYP6{m>#4OsDQ;CJS1B^^fOUh*%Z{6Y*q2 zg7J0A=c_HR)QX~Yd|M}uns53FlzOe}qS}TwQatiBnZv$~mPjYX17w?LI_s~v&*L$F zI+#!sk`jZ~_X63ZX%lz*Bdpt*G@$dS}!HbboCPek|1 zZEKXmjUXCZq~6|Xg#65Uy1Tq1*<(tx_{*Ry>dQ7H>n{KG!Ow+rbgCNSq%Trul(Ujd zqA1Bg@y6WiI5XQ5C(%@^N0D9oL0>;oP0Ova*raCL$@FK#>~l(kzJ!|(Yd?}TOZ&DRYy!LQ>$$?8QhbY`{XtM5#jhsLuNY@68ogfH~D8J+2sDZg4E?c|MWEc^0be5mX&sTB3Nm zpn94lfr`H6wAXV)SGoB_$m-HDFR{ zBl*A{X%p%&QNA|Ji*JtmHCEgZ!9{zI^a12EGZUg-tH5BP&;5w68u{#IxYW#J^Sh8& zH1YL(6bYEfJF?pGQYty`8Pe;{jjf+)Nxk7ka~e;pY}WfeuM>0I{t%ygp^~<@^m5<9 zWhQo1dJ1Ri#cH?PZ`?1q5YfQ|jwo0ZqO$mf<^`LcD?EQ#SVS%wxW6f@!9FH#93;*9$qhMQ`oraf_54SJ;|4pqB@ zjQ3{T`Yuni;M?#-our57=RJrVaJdW@)Ds!q)pHVKQ(H7{@kkSBRBpN$&u99)F`gBq z@3|{e=K{W$pXHZdymC~4>!y0pX0rS!B4?mfw_}=i7VD|Uebub{5BesDVP0Hz*GkF6 zH(fdH14UjP(&kuvKYqA$huvTsVWK+Sc3!mZ@Dn#}XdRSdNtZk(>O^>SY9%!)Mx{ z=9^|teiLem&|4FY(peUkPlpYh?k~=N-H+!uEGJrOUNSTx=F##qTW6~F(c8ooe3hX)<+^H!hf{6b5T zi%8m4_Wy^yw+^a$-Qz};5CjROrA4{~q+6ts?iNI(yICM0qI4tDARyh{NG=ehySqCV z@jh$sbM`sMz3<%n@11#P_Uv&+5!Y|Ap6C1hM=pY-C8_Ve#PcsBEjCk`yU8j zoV&_Qu)Fjy7FDxOB~gmG6LH#qx8K@Gk3lL%Ywc8-|HyTD{aS_y?-DO}TL$eoNM!Wf zapTGtt$tb#Yvpu`P^8eM-0COKD|^4peDlkmV6RPjiK6_) z>){+dm7;AV@xJmzF=d2*X8~+*WU6$o{d`^EDb`Dn@$yAJu_r?wb(186HMXYU&P-K* zuETz687xYT;^Rvot$WIT_^C8CJ}ri&xVWWLg(13011yDkF(EPE-A^_tZ5laWj=@&O z=8(y0-Aua@^h+}DVS0`zT zuVKn#-Nwa+>^`AM>OXuvhO0|5RGDJ4>v6G9F&&n>SU_S$#N`QDrKyhDa{rBEt!mEU z`uhzPYukg^w0DX0XMaA%Src$f{CdZc@=YrRp;hJEMnY zWk`N;wAvR5Zb3J$JlKoi?=Z%hHKzxGEF?YebUq5X*~d);&#-ssntD*(=o%pLr#$nD zZZ7RovF&>`V(wgAdFW{K++>GEi>BXD-+kwyr+Jlt>(Ywbr|!{gGc6Vl}>BrJY=xZA}*YP@eT?)}Hl@ zd$;xqCbD=BNWe_kF1G1U7R=MN4qdm1rd1&0MvoomYjQn^BvEqePv&Rbu#sh*C^bD% zrfVCOc-CpB&;mg{BhGjAtbxL%VAu5C&FG=)rKaOBI+_fIC)&> z!*y%i#Tl!%?E;NpK;Ey!x^M0I z^J~?p7VFt|7OoTvfv^W})^B*0H8VnlQufLHwVz2FS;;)D8uKWQ^){{V?wHm4Jqan0 zP7s~MKo6G$)H%(2W_^JWCWFQ&65?oV@uMZikOU&M#kx0|j=WAtTg<-IYPmCD!Mus8dViTyVXYb3niVbPYe%%mr) zfCQ5?FnkaH_B*ozgL=IJbQ+R9)x0}a~i1n)wXjA*un0SHxx;oRN$45AosUv^a&L(Xz}P5kK~O> zpOH1pn7eqEG?bf4iHiFyUTh#OLb&=wznw`2fe)KDes!(ERVK{x^;A2`;Byyc44K`||YX@Jm4n z&kmr}#|!=Z`-S!A7c7dv@jDS?ynp`dr9Odwp7bXTf8!_r{*M1cW&7~J72%k_H2?M^ z_vc^#=n4E*TvS{5^8fJf`~S4gr>V31}?~T(QHoQglf8-4Q_s@%uPSShW z)OmsY5>4_g$iC0qr3%{s2^0EiA2A_}-!gcex2qF*4pP1yDmH(}jr#QIM=bl|K_@Xt z6%w5JYCW1C^RnIWVI=Pn=_PoTC=A_HPFqrqS8g+#0mf^S6&8aTk_K9J)(tvG#k~ir z$3SDHufI9lQ!ouej0>*8E^bbZ#r|s+l#^w?8+bB$VEBlTwvcqWDT>z5j6vSX`_e5Q z5UPg&*XTOh(UCn|_8~&lZ}gntL*cLDFzsLWOeDvRI06M(G~UnEcWbxgt4dxdePTZs ziLX3|Q4f|$Y5=~Tb_Qzqx0Ary2_QI6!};$({o9yM52l80lB3){clO*2n~_@tg~WL{ z$AId?%c+;mD_-ho4>Fd0(sN9L-U8-Ph?Tkn9GlBCXA;^wW02}l_&7`NP$f&Gl&fmD z#WTsENYPZ0GLCaLt24^yZ$3hHyl6}sESCQITJoEr4??Nu1)A3Bl>JQW{zLMv;Ekwh zZN|)e*Lw{k23$6?3=XS(W1X|)g7W5Lg_K~Ew`1)2?O?t8s5%B9oDPfz*9nfhdUcNF zC!TsYu)S|2e7KZ=_2`8fRNIz-@Q7DJgdDo5Jof|gb(&htox!Z7oSIr@4>FfC+&$@Z zn`VEo8#d>u3r2N)Vas4TcTQG+8O^9cvs~zsm6A~6A2ghuA{W!uMFmGRC`n%c^a?2j z#LPXcCSbWW&a?!1OhA1^RW+B~vWb4yEQU2Qm;$)uuKzc-e0md{T{R$;qQ8Py|K9wS664R}$ZM1`2DUiWd(umqmR5IHUsPQ+Y<2f;h!Y zKJBu~(V$xFzR*H6$agRo$GZF!9PFraJPz|W>ZEg@XP5f>(QW$>09Wl#2EJj7@GSmX zAZD{Elm*zL+MTIs{k!4D{Ar9Dg+Pb)(M4e*FaJ9+&A@tP0GD7q_95VS_`%U&%K*3`43(`UgXysU1Fqk`6!x@wbvHp zD~2tO$Ln^|aV#N#In%RW>oX%gkJrXCEL#6lQ%;vOG0rsA zO6A@Z=)yH?A7AOwC@0pfwA5-?B%SD)8iLnUID;Y~8}KMlyDX9Jrqlm^3yL7HfS>Sc z&8O$MzK4DB90o$ymW6oZqYoYzG6SZHzCc@$ieCBKvx!h0I;zGS_XxSf;|RPrW4M;5 zjyP@y?ar@5LX0mDD?rp$z9Se8zId$#)KY9xbbjK+Nk}C{66Y#0mvfs&+?lLY zXY>(AIpMl!u`YSz8I{HC$WfqEF?<$anxGlK zNc_%Nj|Oos?SG$S;MYFiWSVcer@zTiJWL|MC12|BUe8=^p%-a+<_2{n;-KMkmX^!r zw4M%Nw;v`1tiopR^%eRK3trrNDBB6qdI^|fEY*At@2r^;B%0=pvPunQr$qUqH&v0> zZkW*x3Lxi6ob2#7D}f3dP3hd*_oBXc2H3i(D}p`1>E947@EuX2x)2b*bXhcuw!L_C zSGCoHS(I&>o3j~JagivU3tnRtQ$2^BJ>`?zS7OSx~JS6&&p?k_a2iSrU~Yx>voC6;d^aC zw8OF75gE~9MMm$&+t)d`jOwcGgKm>$?)5vy70mh8Crgnjjw!&vT!L`d7>_B@@1f?^ zb92@H?aKzh2Yjs?o=B{CbN|U}Gy+LC>39r4bdVzihQ$GH>vEpfP zopCgudY|39kCLFEBol1?fzrGq9c)@9iwYOk2Vu2+0Jq6;sgR-iO^wr`y8UB1vV>e!sY%8o|$M-=HdsG3yrP zK?x8dPvbJPqCH8>NI8pT! z+bqNs04lp^6zQg!;8Kgsbzp-1!1jZlv1zf#Q7#S`7g{X+1IuO0_{g?_Z6?q(MXK*&%pgvO zYG>XwjTu$e|C)ZrNb+XV{(#X^^y%V#Ow6vRc74gZYLz6{>ThVjG8|4M4R_~SeAc{fQ+WO0q9;W z@NO3Cd>b$D@FnUYF!0J?ig8}2*{v-rFSnV~23oD{k>wji6qD87en+8b!dG7fDEps# zL-TYkiB2iNC+5OAQ;gpqy>8q%-k!E4P(N9Q)uRU7ty*=rH+8ag`&vYl^39R=M88p% zBqiV&XD@&xzFg06=?GSq zI7s__0c|dj1L+o@=Qqju@vjHN&RKGlFDms#ufBkU#0`#8M#AmvPvVNxe-5nCCvW}fk5%$D9pz#TVV=1gQHO0fVViH~$%ZSY7~SvN_B*5F zchT-lRScGxG()aFguQ9|#FpG{G}9!QAuH;*KA%R|BRQF03?hG)HJ$fnuR||rj`kPm zqJjr5dl+zEK9R(XL!P*S44fqutv-3;sqg$kNfhdze)A( z584eLSyaNCX@`EEj-Fv}j)P*U4o)aOpj5P{h@@AM$U7gknkpz)F&bkbfLTtY$9N>K z%=dk`Gth|C^NW}sXg#Dqngpq}$SE$9-rnTpTNc8$Q5ZYhX%t_804sF@kMlHbcDu!Q zP8iy}_mVpzcd+T63&(Sc^Er9Wng&zog?sj7lePqEZ)sQtpew+BRdbnXisos{$|pIs>nZf>KvotqELK z{az~(S2ObO>Qs-PpX(*vPHrKBAP4gv2p$wHp534p^W<3Czx z;4x=DYmBw#O}=)8IzEfGWyJsxBYUa zegY_8TUFQQ6RRTTzF#exz9gNKT3^sbl!mJm6nMS1tJP0MCy?_PXo+}@zDG0Q?O?7r z){9=(+P`FO2-W0D@#50(7U;on&uen6y>*MdKjIaL+if!6JofGSD9MS!HgO7^+znp? z7WW%oXVf3Y|4qUE6k8kNidUdq08;spU=ajDORH3DMnxH}qz>$1MIBbU3M8*+SGY^) zdsM_#e5k$V*=#zrciGtYB=)F`=@{G!k2ZI6rt?dE@Qlq|j*=v)v@p7=d?KOm&W4O57oA!c(O+O*1^Pb*tDM-6g5e zV0!G*BEBxke4xNbk(sLO*XkLY>WOQrN^HD|VU$Gisw6MdDflKIB{A#a629crA-Lbs zJQh=-!R&F8I&`}A`Dj1-0ny-A$&a6XDz^F@SqhQ%E4nQD-BBJ@KJLzOO#RN=FnPAZ z?ap|b-#00pcoFuK#m#Ef&})Buv-%HzvJuAs@j~86q{j`0-pK_Q~ibs~?Qt_v^&;un;E4hnkMVgI5=KC_9rVYxR( zOhDu6aLvOA9Kz>~n=xgx(hF$(5uS&*9LbjmL_8x3b* z@IeUMj3n96sJbn0f`D7122qrc>FSr#T~WCwbV}HaUEP5_(PtIn`~f4SE2jNPYGcA> zZ+?EImY3l&*y*Gu-_;4glI+BS#&ZOK1cTYVxA@GL`XB2#bmIc-yW-Ra;rN=?(P-^- zk6<1B$_9?tS&Q%Q%k_Ma$eUp^2%XLDekcD4=e=b~35iV?hpS(qE)w+B<$o(3OoRs7SJ{ z5?BS;;RG@3DRA>@!9Y2Ot?Ka=Y){@|!^uE4BSB)nc(p4+aGg-<9j#5HK$cd~j&cRk zMKky8DHQ9eF-U}GtBrK4L`KhhB97+q7FKba5V&RxoL$p28x`VmR8+-A>Aw$SckWlC z0>zwB%P_OV9cys|?*@?+r)(C%X!LOoXf16KD6F0cc?mj_k$p+4w3ul6Jj!6AptNf4Nnj?97CfTwA^pX z$K5uz{X|A%^TC#@_6VE3JYU?4Llq)5uq;~KnWh=as`Hiok&fJwkK>EWtSzNo`3`?r z5#YiPRjFWBHH`rJa0bxz6QHYpkd^rDf9VY$-^?EwOa0#|L}jqYFMVK)@bt`jUtRtR z0&+La2F5JB`bFahB_|Q0?fZA@t}YJgWs{8#dpQ(jcc*6$8VA2q4ATX;O8BGUhlAaJ zNVcbQR4D8>tw(rsfyDckFE$m7eMcDdcX1M@%{X(zo!dHoA9UU$waS1$_#-ebBuZqF zVxDM5z%KY}#_xLJyN{%Dks@hFaF*QQNx^^r5B~iElXZ3ik34@Lj{jgZBAEc85V@VX z^7$XXe%kyG%&(BAKklFS4vKF0d)=+ColN+DzxjVZ{Qr9F%m^SaG!~49nlQOfx}cvP zKov5$pFiE%TYBkO!ZXZ+Q*FE8tJ~xy*mn|TB;Gr6MIMV=#MJ+9g2|UQoc)|;{Dm$m z*d*0~*OV1VF11d{dFWi`oU-dLR<}Z573jf)wTkEYC(2w>-0urN$&kQ|0X`s}*pM4` zrbtkZBpuP3+4&=`1)NJ#U?`$~^>sMZ$+(9^$S4@@JFOjWMLwbi0jDKu(zcx~kih^I zlOG>LN&%!R*r=hu%K}!<*!hl43lV1R%BUz>1(|jj!+E`_=ST>>_g*q&uu;sE*JZaJ zpa=Q&ztXJa+DXG8x7W%*x|%NZwn@R`#9a><^Jv-w@nSrnF%fTne(kcVnpqv`sC6QK z)7&fywh0<~yxERaT7T4Sa90EOf#{89iSp@pQ($i5ShOI+c++?0#Io>}*7JlQ6nOj$ zDB=E&o2msHLDm^(9X_qvH?MhW(PR@ESAQrvp5Ck;V+ONo{N0<$nb_04u%Kz{TI$lDReNr`$9vW`8#0LR||RJ^E&S0(0$-OfmU2pZ2&%$L2ID zfV|L(GVl`*NcpQQKTUeXTWT5w5CB==+st&{c^t{Mv?JgC;;YNfBlg1E933~oIOpz48p*Bx+Z@M)a4C<^uGG$|;CCRNQ+JguV%-4RUp>k9A zm-TbSJ!%hDlz47|%k zbd<&ETWa=i^I5xWkLMh{ZVzYFkk5PI^=h`&7AG%{)WqWlkb3Qso(4R7~8J(z5vXhIl|hhrqkEpnLy zQeBT!kLRh+!Tz?P00On%^rGM#^Vsf?dG6m3b|Q(dlg95BLlXsE)RSbR^K~!cGBy2S z;|>LrJzoZXEVHbz5ScGPNX*miY>HlfUQ|9?;8RB|b`@~n{<5vYQtUnR#m~-m7-nn& z!pH}U3{t`wxC=tnTI<89Z2lnG4835l8A;a@ZPbE0k@BT`bs@@HZXd-AD=Iy44Gd8Y0(<5v<;`;YqR-4#iDJ?p^mfKmK(_T)~yYbKhU6V6%z$ z#w~Ah=3b~Crd9#yNjphs|9uZe5A;~da-vpmExKv!n)-UaMJ!fU{`2Dfq4drr)z;EF z0M_Kqz;^4#WH=U{C|282w$SRrMSv|uN4DLQjWe094x3nkq#5fzvhtRP%Q#`Y+MDyp z-YZZw_T!m{+6#z`DPGsJkqScbG(_y*!U`skToTVpyyK8!?=x!)tZ4EuY<_Ak9jv}y z@M6-djRr|FAB-;yIns1}o85oGnGeLIBPCjTnU^1wE`yqD!xvvsy!!gQgrTXy0y%bR zkA$SFo@^Xd^RKimib60fd}gfS8TCu=l!^sqVS{!a2ZQ2_x1s#(T4n~Hy}7Ih1NK{u z;;g1_Dx8nn7qdQ8GX!9gDvPFs0VSY|6o~Fbe!J*{%Jj)l+HSs?azuiLUNQB(f{yQe4{sSg~kdWb}KuYfr%x?1h1TH$G%uyG^aR-h2egZhRRq z2vq3>@BFv=B%z$kD{*xfmR#W;OgXci@BIiVTn~gCRujQGqlK-|JIA~5cBWH^bu$E! z*ZvJ$zLuF!dD!#eGx>(D<|$m)prl2p<55D&eK+0VO+T6O0QjgY?zfjAuTX*w0bg-! zd_w$@!*gH56u~Wb{D=MRy+m+tF46kjoKW#5xlvY<5Ts)5$yVjYV+6L{`PuogTOr-70s`YK%?GFG$+vQ?w_ z_n53OrBfj!s?e94%B6t05L- zb@N2(mk`yB18FCkdG7I0+YyVI%Lzhb?fFcz);9XX5skU8=I-b&^>MXMp(Bv~0_FGo zKzPCsZguQrJ(HUWxA7L?8xnCiPd}}4RiuNv>)Kb4iw@<9x+7^Fpr_EXqQ`4a&oGH5 z@PH=dQU(NBJMLTu-bbnV-OY94-%sJ&Ct<$pbHb?Yu!4_c1!0vLV=btoixD zE!Fm;K#!;4yyJ6jGeQ#ok?OTdf;)D<1jC~TxPYOrJ{#}-Dx5xuNnuIa3Zewjvs#uO zZr|Wgqve%H)SKRA52vpH41&tez%)w~1*YCV+H4PiQIl`S8i8hhuga#wUZUcbTMEI;e)wK%(H+q)LlX4IQadQC3( zU-K^Q8|{yqc$#3dQYPX!8(PdCV1?sSW9>0qq?Xg?VfBMsoMl^;*FZXZ(gUKUXV+HL z_i6S(tfvHndVZ?LO{=1Mfp)g87=unmV3-G!&?V}r&_{87xEt6r`Jk0N6Z+us@El{| z&!0_U1wBL-2&33LlY*x7?r<_TAsuCHFw06()(o8*F&cwg z2I$Oo5~YpQCdY@@yzN{E>8sR>pfm!j2mGeEjtnN$`L2Qkpj8- z%r4ZPT9u4$KtI8XrQT=|2lOTlmm{`mmgoHL;!JuI3;lq%s?tZ>D{@SMm(&zJ{n(r& z8+T(~kov(S!u$M2XNEhLGntE%Z+ti0MM9HRB}(|QMGen{24Rpad5!&A`6Z#CyYE#& zG#>U4>)_iFX1bV68;?po&V(-Px)roV-xJCfHS#HT1QQkN#_9T$ zmz1i6B}0CFiv>od0U2b?;Hy1v;y0Dk7MSJ2%p`Q}Xi<58-{p4aG6@D~|Q3v~_a#F`kB9{dYs zt%MddB$@tt@|6gA(iE3E?E4o&&cy2>N&I{DD$lrIwD_13jz*XbejAkp0aVsMWxLJ+ z8N2jty=L=N*(<|1;FYC=P%csrLEhVm7=a`{XGNcGSqqIBP%VP0Ue|h}2dV~{^y}LK zb&dfrAU#uC5mT|w#TgJXkkNS?*ukwn0}U7QbCR&z$$-GPYMYYy&$_~0K5m>QdmgfJ zVvP-%)lI3xe{38e)vIEVNXm1ZZNbIoAO0BBoU*N>j~R0X0IO?dAf7_s zEy?K2FO|QaFaHK?P6R!YKm1oO)^rjqGgHTn>D`6CK350!a${041cNU9y)}2Ay}=9Q zDuePgxm<(W*A>bGNBS&!S(Kg|dQ~^x?gX-pAUTkN+l8=Nego=5LEAapNX%iURMQxYjyq4Xw@|+wZS*Oqg&lQ6oFlZPuMaN) zvVj$@HRaI;;Y~em?kbS;BHa#y%91B+C5^ZWJO%3X^piy_>iOc=HacjJi`|X~?m;nf zvnxCya$+*cOf^n~tYeKvzE8_y_}#aw)o90&qE#e9NnI+4$2=bNm(-*@fL-cBcoWZ~rJ0itRv0LBMI0h5^7*RxYrwvM*rQ8GKFY=c$}YVvGpBku$g@t- z?%)q#Q3F%i&Lc!*=5M++)y&2a09EM;*OXEON*g5{iM0?Mksv{rx$m7is5lzU`VFpi zcl58bgBaE-TtQn%8;YG<@N#$3RP7nCL)q<6RVnu_8Q;!*(P-!5=#Sf=8o%KBQbT5p zy5jk+k6v|3ahmEa)1S*|mX-Bia89di^GOHZtk?J@2Ew`cc-M?e9m*@Ni}5GU&82g( zTTFBcy+N@J!GaA35)`Y-nbT9xeZCxI3MtG^pEB@;q}Hxd-($lEKCQ{T2l)!K?n8J} zon?0#uc#!N1BJaJLBCNA5Ft#Ukb5>i9bs+NDJXCHe7?REKpw04gXnbEP`2BcJ?;5p zfp_mGVn9;W_+Uc8$nfLgqOGiDHh%_nCe}|bNOzC?P;~QhHk~)sqzvT=Rpz8rnho3v*4;p@yZX~(P9$Qej%dk@qT?wICar z^6rHliB>K;&zF27^R`1SOi#X6(D0{PRl~;Xd8@%YV~Rxaf|vXr(r8#~TD7Dc_dmTe z@GJCiq`=rI*lg&nbc)cmn(YuhAC^ICqW1eGVWm>`#&vys0g#1Rk zLc#^nL;^@SN$Wl&gxb+grRvWI=Ns(VTh)yjyLkXbT3w9zhX`*Q%~yurotR_LcWSxQ zHi52IN&3C@X#PBi>fGldAYdO6k=6$x6@&*SVb-in7{p2jlVlh@X~;>z(iKz4f!UHK z`JYRGw|TV1nA_UX?u?0i-HYPxOTUKBD0N6WL1LTOo$2Pqp0mtdW%4UFz2*qD%1M4B+ zpG!^!aU?c;GzB&9odc+)YrwcO-{=$b>sZT5AdQjJ+g?N_vVIoMtW&wce6!rMmj9-e zjmd;;4&h9gs-v}I+rv62j_z$c&!NEE(}!j2WeVBd+;dEWyPl}a!3qgn%ni69aC38A zP;bInroNi>>9$*>bbF?~a^%-j#pNi^i}5QNE|4Q@V&8rhVIrPxkX!hXX%J*g419+W<*u1up%fJ@Iqqd+cig zfvmWJ#9UX*z#~|&RMcX1u-qLL`(iAZ`HK|~4E1dz#wpSL6Hmei#@V%LwUTmEH+?NW zy<7itB>g!%o=mVak#FB@H=-?450iNg>Vx%9#iRf&`oY8-2g=O?-|vrWQm3B<8@=5u z4@1s#6h*^lNY!Oc_q#_fux?Kd1-{gr&FY^~v~eTJrgxb*_zmY%!x@|w%fdKhUKa>3uH6p0lAC4 zwpsQ|GL!51ux2!jy9UNMNOuIiu3k!M5Koo-$eixYN@lB@K0GV2C#nb%K0EM1EZq7M zzGn<`n;80Z=Bsu$WLys>r(dj7EjEEz1v6*O;w5)_(L&HtIR|lxz5Iz0)W+v*^K1H$rW&bj+dKO9OtJ^c@Lhl&M;z}6ol;Lyxb=?P*j*C=7C+^FUU zdg3@60w(1a*Csm$vlrBD-9Aj(#|>T=t`@)eeOZ5o5K9)tW@6*b5dRjz^mp&kBPfBk1jdMmAG(ygcD< zGqXbfA(0l~G85P8+KNDefYAb-^?C*2B*rCL&u`2IT$bmO&^XpL^;7*5dw+$zHSI1O z1+Jl$V#kW?^lZ1XEQGjcUDXHV+E$J`hv_jl$7MC6=)r=wg>|}j6Lt#qLEzKL<1n)a z&;S6%e$}hmN%5BGtQ36<$foijUmf5!UhUQD&wqdVV+KXvKA1uROT7l|W$D{)BfNk^ z+FzH{F-GqIgjXAah;qu<9#Ds#Z05N)3!x#h(W$HZJoD<8*`3pKZ28WJDtD)H>c@i+ zSUppi@GQ2pq*glaS8Po_2)jj~0y=jm6f=Gc&zOlAqeZq*u<&K|9@{a|u8?m-SlJsp zTzy6L;1Bi|OV z12L_tCL_PY8*>WtJ*gY~KYF^Ns%5w%>o! zK+dbKXx7-96!3`$;a3iVoRhrfkQQmLTZq3(WgiD?-?-bMv`Y__ zN;eW87mUs7;aP{uixVQ9(I#9=R zl+K^fEAxyRe1_*bX|1e-;A;wBSeWolFLPG(=lB&>u0l7>NHVCgl$=kSal(<3FbGzz zv=ryi{>tUn3Z%|!Q*HR{CJ-xy93IQ$N1H+dQ=MLa?iJtH#IldjX;{ZS+Gbt;fGukq zAb(r?oh&axi4D%}eyO-EX|*&i7p<|C*6@JHPSAqMeLKd6{cz2t#v2Iq;w>%e+)w!8K`MvE9KD^+rK9@kM-1YEa_JB0SVOTz zRzNSK6X2eQH<&p6&3Sj4K7`DQ0Z_qI7t4;+^d5v4&hfo{R7^$)0C7En zlUKb@Nt%XMUwm+tFfO{$+NzGLAk%VWmCO! zF;nUY4}#PWVP(qU}fAGWUAm zpd&%7zL4`vOGUX|6#k`tGRTEPH-6C+_EQ9lj(eDU&Qp>`A>O)lxkAHZqj^rfBLpc~ zeEJ9kmRS`@tZ30xLU3-kzXr6`=9d(4=Z46u2nUG24DC<`$%%bOBupR0?4CyF1p z$4gY^)jzThpJ(#pLNrOwrJgIY;BVbli55>{8x7R&8*#gxr%gf;a)U*r5qok0Dxp&` zRp~Y=v!t>3kZLwUy};m?8>sUgW;1}JGK&VwYWM<5o8kwIjO&braWh{tMOv-~D`X{C zK5|Qs?E(G0y_xy9f?(_iI5KJNbF3woL3=O9Kz{blc*Z_dh{e3nmy3P?gIOq4pABUUhZvAjRG5#31G?<{+PMKEm#*Ae9DfyKDDiH@I#+HU*CASd+=~H8x$7m(;v=hP z4uY3hW1gQX{bCIF9vWQ3I8YL5OJ+K=Nd9N~6<0VP?~v_5-G5LcNl&$13RY)}D$SB7 z>XY<7nYjlRr@vez*MIcDliVbxdRkW=$S^SZ?a*Thxi*QU-5n#S7JQY&rQ*H4f?bDl z(fCcjfiLz2s+GtBQvB6`=Jg?VppfT@)nVH4=2UhzD9e+ZH~$!y7G~;Yj#+1=Q(;bd zYT$2I3_>E<-yYj;wPWW^@)TGiivL(w+LMMx?}p8B^gmaVsZ5G}v6f(ipG)0EPgO?L9L5PKVbk+>)b$gIY_L;i^^r1J%a*PklOHjdv8=1ubfy%F%WkzRPf%UZIJqbEh<4U|$#JnoX6RsY z#Hb8{_RETT&&P$fuPP;x7m?Ak{? z5Hf7NNisDL#Pz$HZ|X`teWF=*(29ev0YLmZ!=SP3eBtzPJ>w*EZmWU$j7smKBc(4% zU(p)#T2&xw_|%dyB()O6Og}{W)InfahpU!HA41GO_|qM^sly%37fT^=ztPKQU*O}l z;_=j#@#~0~mKA`-PHN zE{2Qdl6~XCKxT#1mO698r`h5=`aE2}rmv+@k@?gKkD^UetO{fG z$(gD$kCI{ACLSl7Y0TJcti002knti5xv}S8F1W^PgjA2xaD=@Nw}o+CwWp9g!oJ*G z$NRm~@?LXNN!Izdn%ZaokyMthG6^TDts0x}^XNzCUyfrKH5?z8@BuqNM>f5eN+n!A z=jE_keVui@XOnHH*3dy!>#9<5wF_itm-&3g9odHi%VwA7zBX$$4KHE&xXX8S=Q5z_ zD?qq8B)hX%JK5w28}bCnK8ZpQ?GWy~xW&$tOU;EXI6prJnt}M$?^j}Vg3G|p#;+Zm z?$lOa%Oh3VIb@L_!4y7|44z@|wLc~xD8!h7w*2QPA%hcG3b6<_96GrHp+GC#g`jzy85Nk=99GzdvY zek(y19{cl@((z9+mw}R>nWv^-bP{u(|L!WUtt73%nuRR`H9Ucqd1aot!Q#Zl12oQQ4792cGMse=t@4|A`<+rCR zWo$8+Rl62uyxT~XgOJ}g1JMP9{i)Kql}u~W23=RVHBfHJCXJV2;uPE@po8ou zH-E1sh(yRPCXr#DR>%&IPQ^&&JJM8uu<;KuJUL-opB7pcUxS3Ce6Pew?OdaP2dXVb zei3FWwCjXSH2-YATXC5_m@g0Cqg+DI~1D5IFA9Ku&hU3OR zct_AHv9h_64&g9I(iUPoR$6@r1Y09Vt?cUqqQk7Y5x*MAt0X_f__1p;9`wHwRFT91 zwv@WxUcm0#=yiR9KssKjl1;{d`VNBUTK#F-&D5;w-cFwB12@wz+rqXYGPtZ+OABpk zKJ%k@T_Tb0YK9^)o{w-R9-hq+t6ilD9{#Kfa-1eOR+Db;Il#SQZ+!dH9q-mOPrJr4 z*{S-C^jPVfIt|X#a4FR07#s(oKY|Tn_K8UAs{#FKZQ9H`hanj`4LB(|#WlR$bIU)$ zFwvxEjDH=KZSY2q#ShHYGM%BM8Z7h$)9MJh-}T)e3b(0%B;XaiY5WonXzWR7)S^}# ztm>W*aL9Bk2s5K^zFEM>3&vBoOsX=QQmV8G)2GBVdB|@%nxCVOuW*ih5u;yJ!#O5a zfN*j=6f$#IEr#&<^Br9y1NWRb!*;$prd!Bo4PDB<3+ETotb$@SZ&Cz3xcX?-Q$@T) z%<9a##vJ z2`C8efVS2{!(*ZdAr_X%%%Xj|OUs8~`)Oa;{O{(`A0>s~cib50c6iBk+Cx7A9|MTbm z@3;T|^zem6A63J13>w^-MMHPqVr>2^FCk$>5cX;r>0FNb5WQ=G_A!Fz=%b&Riy~2k zKA_^f>tt&zYFz8U5j=8hH3WK_5uB!pH3+~L;`v<~ua5fPPI%9!PD93$4p$S3_%SVZ zCU3iak>|RT8(_mhmk;4lN+1QeHE$cJxqG&*X9q8?_4laYQ=sRFZ z|B6r07(hyB@;D=lXIsLVD1M#saz^o8?-wCynP|FX!_IBqVo+uWVs>dW7a@3_vslSa zR}*t}rYYrnzqCMf0xR5l!0PyJOM$bBea^c{wIej)`_cB9VUd0j5bR8S1M{W5LJj+1 zz7|f+PUY0}rjhAt?*#kV(M2S!i#0un@p`VYa=83+Nv)6m$K2WqNYyHRH-L5685mP4 znREoq*H5!xyqB^`QovP6Eshp93-1oFzc|E$i$Or?@1&Vj$?8FTAf8e^F6tNu9;=^1 zYEq)Gx9C@)5oO@Fqir8?FaQxlaC62K$D5D4dsl`~v2MGauSY@ZxvKQkCeUve71SlS zx06k`?;W>?^$a?kt#qWLXu=X?p;iKqP?x<^o938}AA2c(G4KYE1Sr0KaglO~8L}r; zH4`fz%c4a%WJdel*z>g2fp@fEiURQ-W~5v^yYz-K^*5!AOZ&Zsm3lt(<=Zq3legg_ zU+zEsX(RXgBfjNPXIM)3nzkS-t^AcKXUte9`rf}(hz0giy7jKzR&(2i%IsDEq3C}D zs}OWLj8-FPdi#`19INrEi)&im4Xu^%g2mXQI5h=T)o7sroZFgR4A?~*=k-v34-=Q- zM?nA*s~aH=&i+ehi1lAOLxL==vMji%#Ma`bcY>pqWOo3l$Gfgr8)JcyET%3xl7r^p z%&nzj_upzjUV}4^M(nY|A;maSU=2|%)a}mYwz_00c%Bd9HHi3~Kg5tr`Sz<)l(%Yd z>GrFhOC;{=bd>q65vGU*Jb5!=(_?aR7{l)P?idT>lw;q;>zUJ6P74$I%~#2g1R&tl z*1Bp&j9CV4QBOhI@ad1w>`Ayc-k``uYYwaW4mg2dMUPRIVDW|D`uKQO1%H+`N-^+O zp9{IfB3yP3R6Ko3vrtBmoaawY+Iw5`yJm9hS@>;$H6ADcIQYY6^L~4x9WF#P_yoFX z=);62MFI&}7qvjt9w6zHA(b`2aIA}l$FQ!3Ph>GB(KNppWc%Xc5dAC%d>|OObMq!A zGrVsbLyfVxj`E5Kv;!YJv-*{qj7zWli#E>?CTn@J*HD$uxePKX9%yzOt2s*@Zx3kQ zT)TvMUjIaxsBwt%l1@13w%nM`@zdG(2uwx3$Y|dQTJbL1u~7oT&P5EJ@S@IP_4DWn zN{91A?yBT?PnMen38~hu<>hN5EWfVxK=U~PR_`J#qt_bV#kb?-MkCAL4;tg`DmOp$0Yty{u!n3(xM;^J?0O}0t70+YcXP{ioTHO zCa01Hd6I+UOfaayqxR!0etSBF4|f`;O>_Zpm-b(fGjnE}I=fz*sx}d-J*p4c^#YAn z{#U^M@gnN>Of;o1TP_5x)rp0|6RijJYxL&8VWtvw4|>6mk52fQ^a& zf51kg2LL`fD%_{jDGA(|uHK}WDpn5o@G>a(kBR`wfPr5RgTkT#**Gq*`KNv7`TyD= zHG>cql>;Ov+k_MWisnt-$t@Z5~6~ z#{#UwY%fSj-T|o^noPsjP9&g58n6dT1M^RA#KI^AkT^=4pPzoQGXwy&zMmGqxbKZe z9fLme)b&^~)d`BPhIs9t?kYc|k$kZndBtz)Im}W%+&-+001z zubFxDNaru=_EQE?y0cL({rr68OD)Qih}V9u?n5Gn6*AssHUQNil0w$Q*sA z48{cG3;)d?ug2sm$I}51AoPt%|FyWbwEfjj|N4Q8P%Kc9B%EU#6B9S*uiEE;3*hXc zPMw+t&6e=Q^(;M_(i@gfNl&j=O98efOaaRc0Ts`_HGM5x92U7PRM~X%i{d7Dh5Bj8 z`^{VdFHeW}=M6J`RWpueA<#wfz@tac1$&p&=n`&A3hXn=3}j$jykW_2ztqE>D!j$d zVyQh&csO90xebnB5t3rgT{kE{7R|CI*TWTcoS>rxmd+uzG z#M1B@2Mm}qc3MJw`m}$D_4zFW^YIrE9RU5vUdPy4lCEAy^IhwzA^dwAU<@{|Nef!g z;Ca-~St*oHnaS9HFbuq(@(qwpW1oFpZbiRKk7N=4^3KlODDuZ*BPtm=<>JV<`r}VG zv`JKC7TR+%GT$WNlMl18AHV;^{waBr`Er?gD{#5H>ap%~+YZX{PLe%e;j`6d7pvCw z=GsiYJ%uhv^o-pg3>g6W&nt+4pAtx1Exi;avE?Cg)vMy&-aHcz>jzWmSpiWwD3M~! zlLnkJ!bX?nFJhK?D%=?vv0+KXMtDKi5jY}S8SvpJ3P`$x_0Q=%yIw+xA3q*!R$MKm{( zpc%NV|F#Lnlr^8cQ8qa@1W-rS2J0ad?PFn{PdBD2Le2^rK+%pIn@QJHN|Q#=^>jwK zw}HJ#zf^s02r56vSbe?MsegS+OL ztq~`n>%aKi5e6w0JIO2y{f*dk9`%Nj-_7<n^cw*k4Xqab6B1hA5{8+c zr3NqA>jO9hE@lBKl(n-=kAb_A29>3_6lS_*FP?a*n@x%MXrs;D_=y3Qj1^wXV~O+W z@lJGw!-pY?#pIx=$FtkmTH&{ZYlMkWMlu!^Y!60}y6q3sdGdQVx)&H_g0p4wMS~Bz z^}0+aS?pdJo>!A?55!S3aUxkP!WsARMtab_Wx0f}pJvN)d!-X#$y?JX0QgIqGUcW! zBTK3j;gb^-J|t83j9s-87xymXWsLS05|h(kR%RHuCxB5ht?+jtX|_H(MSZq_(}~=ATHP7#p9_c> zCk3 zns^v5ET75Heb2W{s5!ow8w}ufM{JOz*1sjLTH3uoIV736)hzz)(lCw@xi{_{WZD=m zdY^gMyFrWw!Uw(4C7qvn*;m=-(_x&5`=r&T%W9iNx%EUzGcHJo=k^9oIdDw|7eLlO zP&R#_AmS|kd3RbFCqkrfWNU&9QX&$QnA*I_tEobqetfK@Rrs(1&VsbCp#a+O=?lj=R;oVq4p~FCo?nnIb zX2(Ox{prq*CV}7T?xm?1sdF4-9LSy~2#`Y<3*BFc(t&zjB$fQPcZ@QhCm+w0=SR_S zy(@L=Bl)QiO&@@S{M_9BI^or~yN~zi{rAkebzf7t-9D?ScOi4YwNgGm+^E=o)K|9%OR)yM#@Rh@stZLG=qB zhoucQNEwi9i2&kV^5=;_9NVfPnX-M*fO z1$QlaPAkzR5J-T_Y5t`+#OzNl??_Il>_A7wcZH3GSEEP%LsKI5>@CNRozTRJj|i`W zdn!#7gf>;7N5fS)y&z(~FT+H|b01vuGyA4QCA~PYCksQ?@%aS%RV#=c%m9Hrv#z(6yK>IQZqAP)IVs+Xb~y?UdNUpj+@2m-|?;e~O@Yt-rh|r~6m%nvD*dddB7Aci&R~Zohpfg4L69_lhRwP*sK@gNaoi zmGPrB>X4pwiWt+YQ;jBNykd}-fAmB_r8a4QyqfPnoo`Ix=Zh4qE}_+_4qQ6_Vg!_} zIeQUqeQrx&V1E(lGSW#mf!Cb*G*N?}w zvxU~@3=o5OVR?NKf}^Nk#J}&NC0Xi!kM2deL5EP~J=p;U!Src;Q{0I}n^#bIi9d;$ z4WF9T+6>mlMU@skX;%{wo$>>Po(1%~ zZS9lCwj}Z{9vI6}FfgOae?$|D8jr>kq+uZsBIF}(IH|2xM|%GN`EZ*!@Ls;$=ZTDn z)$ilHP016`yDGsG+T7kOLUuDj9E_*vE<^_lK`J3OLnjp@fAE;Y|HZ39(ZeDVzRWah_M~dRq9nec(b~a5$(AC*Af7P6Ma!C{4XJgcTV|reV7BkK?5ri zws2`VgRkR|JCOB6dUObdPQ}@ zIXO)kC=%~;2SnvKW>(n2!5`m%P@=MBwZ!%-l`Oat!5dbeVhpkY?VMw8qk2*DS`i#i z^T8LQSkrA?fT1_bD|#RfAv_k0LZ8ewnmstA_|}x0LZNP%IGJK1dASHDO=4i>CL4pM z9p!V=`l_RE1vP!hL5Wr?H*gr$0vkFb;S(G0z1R0F=(V<`v*r<}i zb{u+PP$*y!J_8BOgJRY)`Bw9ZPYp}{qM|a{ZEO>t!X%_)os7E->YYmig)*I-Kf}Uf zGYGwS(w)@%Ju02JABMXVr?MJ{A%0nHTWdEw@5Qk2tT1vg*v=GaKg0#K_ykR%W|dZd z*swSn=xEX#pTR+mqR0kkOB6X7EwpsMpO6E@Lao+lt=Fa|8YyDx?&rtOLymi~mxXIW ze%GA*yzTKbE~uAPkF2wKX46W2zPelbW&*`2vmC26Qy&81QQT+N1 zu0h{^)P*9rqxdjjuK#{f98TzWyJ8}v%y1L!-NZJd45?{UK07u2oAZ)qqaG(DL=p8> zsn{$hv6(6+c!+a~Prhy$dIbAKaAl5RW-X#x%$f(e&iXvhUZC%90q>-}Pm*#DL!%-K zdn$(Tg^c^o`Ll-zl#plFf#<$rra`pm6{@k6@>TULU2o4TV4g~_*o>P@J*p>(@FTTC zzdl^2Q)&m~Sud0{TTXRtSdxD3Lu8tX!eZFsfM24Zk_`uzB+(=;!C68#I&99Nej%K3 z&B55mP=(bPaSkIlW%2) z5=9>k(%7knm(M;nrd}do{vtMxMGb};j7}^|=OSS3iNsFZ)G!3`yS{-O!vbQ+5C>e zF=UCpdnGg6Hg(ud)z#N|Tx5|(P=Ym+af4!+;kfF4!mWb8P{RYSki0bM9nD0s{{4c0^)QHTQGIg-o_!=Qa+raH*T#=9d6S#mFn@VQOnXS^UnBZLL5iv>={` z(xf6^s>ty<;wnz+Aw%;&;%EtT?H#NwmTkYZkjq37(7oewSX+p5x_CJ9+aAVYC=go= zkz+zEGeMmltZ7nO{srL`iOuxoLYAvG#qcBb=rDZpueMVz+#Eia2RYrZ1#w=wQFtlK zNw7%v>hEvHSUytREksb6?Ha%7wnSM9c(^(k9jCG7!ER0GaZq)aFqTJ(3Y;!h{WVMYKAx$35OCdhIKP_CieL*tDh|luRjMp{D5hH#cbXjU zre1}id-m2TOUXO{?;D`tzzsJ+ziViuOs)rK&@z6$kg1; zo8eK7aO8@JXl0L=?()sL9swf?KsXx=6=yTKPx#X%Z=BLyq(|ARYp`+b#*0z8c|gF3 z1#Je*AlZKG*~OXBJWFio`;mU5G$_74b03Qqj{L7+K&}*1my|YHqKCuebvv99i+hj~ z+)u>7V%)DEhwKy&YdaJNEU#SS24KE54Qqa7T>NnT;xKSnu%H`mz57H%ocoZP^zo=Ldfn%% z%L|J5Fwp5hua-wga@Gr*W6u=w(K=3)b_^pgeikor9o_3Y4lx3JhAWDag|V#X2nO+P z{+c(j|8KjGHY#!zgW;2glRB6Z!aa7HumbZ!O*`|O!EEaRp;X7aoAX{61Z2_ug_@WX zm=61JjMWZZlV{( z8oaxcsMG9?hNaj`lbv^W5SpbeJue^G{D1G`uK)INSI28vE}%YgG4wm7PigpV=xS#3 zr7)Jfun{%VjsGr>O?S_mIt6?bs6pJp9@8G9VkO0F&I6cMbqN4CAT4gO{GIV(orKR> zVX=6U7<=%o4d?GzcNsc_rP2YHBouCyF3hIAtfY@ACsm!ua@T2S?vq6Kk^(5|MDRDU z_&oNg)%x0nhtR2PMND}Gz(`P~nh`OIyvHO+iyB1neLq>z8O=Pc5Oh|eU{bwgNNqox* zD3opua*5c`z0D|_s`S%lyAO7HJ4=4xQ)>u|h)eEx#RE5m#FgzQ1&HO6q9<$?a#!uc zk|Z{i#Lse+|FQrT zHh?nfUV>+0FYf;CoDf#{i2|hrmvi53g`YaQ8OX%U5@@n&y%Y$3K!EWNV(JJ6MlMmi zOiTPq)A$oe19Npf?H>Fvp&b7mH;(`LDy-+x5QHpu4(C3VWiQkZgxfS$<$o}<($UYSy&;?tL2@YIaj%U#f zDBp=H@&kqTqSz0dpGVY1-<29oQ^!bDEM05z5<1uo?ssqey<{OSg+$zvO}Nu4F8%h? zVLjX5OSK~vF5Z*-e)4)Rdp=&hZRTgGTFfLdTP%?E*K6--iE}3|Ra>q4m1xM0oCXeEv)e!9^OnE=08&MaYZe>pzUx{1l%P+8Q}UOHu-(ox#za7zfR z+r0}cH%?c}(>+g{tv0SGSUbXpAuT5e9PaR;JeCZzymoCY2p`i9H6_aDGBtz&B08TW zA6EM2eF!>~EJ7Zzj{mAo6sFi}Ulh>cYZyGXQY7f;ls=Um<1Bg@-66^96L4n|!!HJ{ zfGdMxyHNc>qhwKx)h&g|97MtL(D5)_A3KC?G~c(kNXhzMy~dk2C!NrUu?9u&cCuB* zP14^s;QxRcc%x{191SIM5k5b(*|=V*B|WG+-6e!g6%>yB$J4O2p%4c-!><@JGp8{t{Ph!e_@P-J8IaIK0MzVJWM7k zYfadf_uBXb#j9lC!z{JTuJanUd)LpDy3~K1o%pbkx*-)~IMMkIYoyeQw(-EIL`94! z4D1+@%k7HYs26xP#fTy$q~D<;L>KmKX3Gk1*6v&9l~Qh^oq%f8??Wp8N4w&iVAp7( zL|D67jSo|3wuJ6rpBCpxA4_j20ZxSMu;v$}fzvPD3IeGF?j8Wyo*OK9o6+dkTg^V8 zggRm|xZPcATnxFz7r%Okwd|#0xI1en)J)U~5QY_z!ObwZ-`u~9WI+}gfN~v*vL^iH&WwlexzcLyd19RC+XL_)G*}MNOoy5_c4^Q_{T;&2|VRJrTmIO#5 zU;ZVL?L$DsNUYrdTZ&7+r~+}HBR{dXx)lxusu0wZbld(;Kb<_(Y21fs2td~QjG4b$YkRc z$CsxNEeAgbr7}`3^Wv;+Zj{UqZ(#Y&38RpEkRJns#GkOOJYn3L`P~R2TCVYj|LBW+ zNg>D8qOgSHeE>}@xwtE=Da~H#w(>1<;YAOzKxs4T5y##e1lC&gf`)$+4FG=g`5tXd zE@IqeEMm*LaC-oR>pzS-;L}qF4Ix^c>ge@`O&bm&&ms{N(H4HfS06Y{lI6|9zOr6x zD{72O2N4kqBErN(r(W0Y`Us#cjMCrAQP=V|cH0hcl$fH~eTDg4kz}}(PR@?xoj|g* zJrtjRFn5Db55^GIDW#>m*0(FRx7`o0txZKF*#0E`cQyMfg<&_94_E4W3TbK&#D|SD z4!!%#)Q4ICa8bGD)F|BRS8~q4eNp{u{>lX;hl4DabvbK(hL<%&?{|D1t~}e}$VH0h z(`tA7>I!wI)@G#}CY5C2Rmc+;kvq5P+U(%V616@*i{<4@KkuX7NKA8K>cht&9+?iJ^lNl7!ttV+a)h;f}S9zwf2Bqpk8-A#LSRVxu(UVEo zAZkKI6LjPwFr2@LZuLbFhKQ58B-r2#*NrDKXft<`qSg@K%n{@%E;Ao40t&^f`HyH! zhcG`Pg~$%wnh}<)gY#5DG0TIWEXmp)%{iqMh2D`^42-bF2=p(e(bROwpMn&Ajz}68}VEaJZm|N^~sd$+E1@8j;)S8mLTjbc`>^cJa zFA3M%xv-z% z;AD_X&XuM2PpK3tlv77?)8~tB4Whf+xkx@-9J@POr~$&B)v9dWU8uH9nmIW>hPhQRU6QOF=of0FW1$B# zjQ1pP)DXCXU#*YV1HRU=sL5_8&`fv3QOpF5N`3;W-)&v8+C>V6X#NYt&5X0}qJFKe z>+`vUkA7f}h`!+KP+drVSdSBl0P%vOtVPR0rOrOW-K@9lcqu#Mj6shA<)I$-?Rds+ z%Q$8colN8Pjjdv+z+EHFj?ITKDrzk{wT!0u`cKmG$nTTr9-B;a#HniAk1xCbhSBeK z2cyrwR>0frFAd-AaHDj-U-oP&1!b-_;8Q2fV04Jv!T{$iF+qd+pF`O!+2#*~DSr;) z{I5+e-@R=w)AEvQ#FDWxNb+-v*y`VI%#tM(^41v5Y!xkM>lq-Pp;R3CP8HSBZ~3dz zzz0ghwk9ZEO-^cB3I;+98qw|@jn}H%e5UE+P3Dy4mam<rQDAK6-zUTHkZ9E5F zgyVqtI^i8Ci-e$3Coz0<25jUun5zu-*v$2QSjPv#_#W!ZSh4L%zr;a!~F_Aqc|F!|dkbzrbN%f)@6 z0uOx;X?E1^v92YBcTUWUj3*+1&*mH6^~O#O*`~|o%7+3fm6MbosSgp7hq7di;JP8Z zy&x3nXjsfI&T`-PW>L)x>Rv5-pV!p=BI!VvXnfBE(?P{DF&>&?<1X!sq}|weF5g%o zo@^S-+uy0T^!g=%%JH|StHo!-xOq3#yiJN;Z<;fgF|pqOon|b+M9W)Zcu;D6!H_{B zVxN%y4C_yw4|mH-9cvVJQ&CuR_wZLuTFJ}_62fJv%rvUMhe$TTT2Grk=X~$qNY>gM z2g|D3DTKYxMgerde+o@jzg)WY#Ubge&7P7HvOO<_=UX4E3_8 zM};kWJ9GH2I!T)j5BX}gT{-(uJwq{C$ z#OlVC>Yu61jtUu4nMhG zn+yWyDZ=oVudVGlZC>p#-<&!fvaJ$gxD!T#beJ3Ia;W=GR*FWxF)=B7rkCOF%yGx~ zuuklar&G~uc=kYkJ4dPi0wlC$A1THkuVFC})q-?>>?W&)X?dUb!(8lNi;>^28rEZd z!lm&dja}xKz@INXEW>bK5`mIOKzK<|o5EB}$Y#o-DNI;eX7~CEgG~?ozED}GYzt{D z+S^2&qWOoTv(la^??doAO({?w{Er_j>Hl9?iLvjtdxRmB41R$Bvk4ri3Le1FS|(#5 zfY><3YOV81je|`LUpCbITQaNgTw*bUQa1PS_>geVmSb=P#+s(W+fVu}4)Zq_^_v!D z5r6P@IgM37cA4=xQi5A;Ug<&V35aH+WwMtkImgK`y#r|kj_5}n?(-3LOZE~^&+2Pt>Ua)sJ}$jVR6YX z!hJd48LDON)GpESEyo-k9}!As(cmWTTOK&dga%1pJ~+10?p-*v6&>?UX?xuKF9e~z z8%khI0o_SAFCL~`?mJ!BzE`w;#;IW$efX=k+) zh7HFIbjbYrLcO1JtyKedAq&N|XK-R^gxn63Fm04d4@U|2Y4xiaTvu|x2bxh-L}weH z@8ca)>bJTZ$dZZ;F2%0wnxquXqJ%!hk~~W5H+hPc)yM|-CBS1MW207tF7Rtq5qpv* zQ|!J;-UQI%trEdgVlPPP#Gv;k_Pf_JrPEWim+(|;nLx_aYsE`#upf`Ie_R$RhO*(9 zyq6d~g2OD{7@v^qUjoPPc1Skouuwv+e!1&!O^w0D15VSrMxXTe>)vYk87Pg4W{4qj zhy&U(_Qqfx7)KerPb-VLXMjhT1^^Qz^ihs{E{R zh7Of3Sj~`6VJY)|N8^VZ0>h2_3$ni#UXQkuKU_5Su^ao&on@O6r7sG~jBr12m&E?D z$eEuAGI)%Mj_LBy{r6UbL5>4d);>WuT&-Dn%UG}Py78&ZVe(Y3kLV#t4V2}oBrYD6 zaQ+Gu^6qji*^Q6>`t2vssF;zZ)hWgSE#IikFS}bi-xiF;Q2ZyCi+FvI1uB+Up!{7D zow%`2zLTnH2`f$f)bjo1Uz=s#hzEHuS7=A6OK6--4k6bYCV0(oicP@7u#g{En^hs zFzjzq72Feel(xtbvCZ!!->3HRjrn}xY&sl};h>f**Lo-m525FXE|4;%#d5bFvNk0h zK-zhJMiEI+U;6ZOy+S{w4}emrUP59t+v)7@NyI4c>YOO;G}scC+|7q~2%uKmX7lM{dpIkzLSJ zr7k$+#p&C&|jg2v`rC=M(HJnbX{GpKcXka&*HFh zAFe*f(&JVZ4!rsu6bzeO^BaI4;0ZC1KbOUL@dp|BAE!8?H8G~q*ux2kP8Ia`{>24p4}>DpC5yC(!gz*4jDlL@jdXx1l4B6dJav zxNqN+lGV}CfAVcNwZb{M0}Z`MJd)p2{fzo0Iqr$QHm{^sHU&>MveCQbsw75vl;}9tNQ{cQ>hjTmn^elFHZTYX~f z^eH|m1S5sW$>79ZQsS8DNzvCw6N`ndd;bvQI6au4FB3&u0t+}ShM0baCuiyk3z(lx z{22I7NPe7y^V%4yN4yJ6U99cl2|}R}yzQ7mQUt|~A+1Hx?{t=sNzwj5jv;k|WAqDirxymNy@@FSGC|;o$z^zLg669}io&L3;hTwa<9NPKeZ2gCL-y-%xb@Zx0*90DRFj z_*MP?_$2W0q=x?hYo4k|`f&ftSN+%buYkoxe2eS+?*ICtF1S8~ei=~M9cxjo=POUfM)>D0Eu@rxh>yUt{~v#>=zFoiRKa#1H$_@WWZmDCV%Zns zQlvrTj`tT9@szS^PJ=|FzZ7ybhx#r5X9STR!l5P>1KQ74JRx*`mXW?W`2U+k7pdbw z?>Nc}j5d}VPEcWyiP7&bh3H|Bk3V6kgM2w5PzCV7wWN~`Bb7DKpY?L@%X@2Ul zUu%=RI=KzKI0M)j&!_VQe-j9=Xk0$sABNBE-fdTx%942!5fIQZjW3TkCpa9IzLftQ zNn?p#rITF-u4Ku9zxR2==-g+nk1J~bSjT{l!nkp39MgN-jRt6C2_?6_W==ruJ1T>Z z7!6<;ut7aiV*zoqKdJrHaOn{*9?Af!trDZoN*OQ^|BGmvMJ(tj<16UVu>j~>w$reZ zzpxc0S6jyK`@vE2db=L51^8+7F|B7Ezi29e=6pB-k9{~GSjiMod+Z2r{6(TL7Bk)* zG>I-34h&|a<{vxhPFv4}<9U>sRr95|R10LtIvuy2 zo*$5$c1Ez=(s`Z8o%S+4rYHvIfe{J(;tUo*|BW^{IA`F(nnR(pLMZLEDh)0}AwuJ1 zqJ?uo_2A8~j0T%kbn%dpB@U~D*|5|3mZsC;Re!ol!opaBHf&8)asG@_){{1;&BM>T z+jVt*U+Q#t8e*mFm-tjcixy;9SZq)CFQE!HmSYKv%rc^#9L3}NO*;D2oKh&Mf)#gI zhjwq?P`ArUh-v70VB#>}amz7&!9XVxa93#W?JRCvmwHbh;LCUf#%!~VF%9Rp>tX#K z>#xc{AX6lwaqWUta*X}eWvk#FuzB&3;r0tS}f zsS}3Mvh>}$|1zDS{VbYN%~m_}k(pKoIa$_}r~?vE1Pif_o=o+<&VHKtyE2We|u5xLqY~0t_hM~AyHOX*sX@m38*WDYL-8Mq>)bbJP%j! z?xe(`)B0R(Z2N7|YO5RCpTU@S;hHn=oDrzotS9%%UR1&W^IlqGjrF^iXoA)LS00IF zZk8ix+*Bcv=d1Gd*0!no_6K)jFUnSLnBnRVBxC0YR^A%~BsVt#OXYGqa$DE)58;u@ z4=i&T4SsUAd%q1v(zwThg%z^OwX5imZ*oWo|y%S66 z9&ys|W;nTMIH1AKb9Ct1LGattr*ZVJ7+0{QR-r|Jg0G43=>XP}G?;SrT-QBv;RXj?3fi z(XzVWrGloPgIl0ptH~eB9fOOSt+_#-^A`~yy)Bj;5PEfxuN8Zu1|4hN7fwd5Sq8iR zt%@0qm#MIw6&_NopA;W+OmKn+%#=Q?Br|?csIy*FYVR3POqV>Eio0)90>b;y{s5VR zHa_>GI5n*ErNyw$VSa=r!|M|4bJo6Pnm{Twpgrz>Veq8xM7Vue{5GIWDX>*71(9<7?Kl`3po8%Q`hBR34u@ z8nRP7?2cP)CD<=EqZ@gS40zY?B1HA;@|%Vz&|Q1+=E>7wrPYe=PzPg&A%oEVqKig2 zymd;|+rSdcmit-YK|45Ger^RjsSZf1$9>az&HDE=5bR#4M}c+sVj5*hXP=Xv4kBe~ zxC}4guPp_b?isP4Hb;YdhebNR zAoXpF-G`@qRaoj$*iZVqPFe(WnxrNO7(7_oW=)Rg^`_Me6mr+1j-`pev<0Pf&|ZLj zb-s1OJP?Gmd4+)XwL_kOT)4S^JnJkJ+rvn}BcE+$w?YS=ub(!Odth36cE^AYZeMqB zz%R~IjDOT23oWs>o2kMm;k-vrkDq!8yyrwyyp?`yzb~6kX-pt`o0DLV5uw=&5FQay zSu+GY#0r$20|R3yi*<}~ZieE`wiasg6OMeu3ltx7fE#u&XU>5MH%w3LlS%aLM6USM z{>6uQGVzw`Ej4`ms*>JL%6!Omv2TnY3NSM|HSh{NjMrhNOeOZl0l^ERO7sj-Pd4EB zIwc_-u-JZ}|ItYLGCr$F96x9Bozo`i(46+50EqNGCUH%S70Mf?e?SpLZOROtzKO@_ zFw=zs)-1g=kANtMn=ToJd2>F9)G>~L!%Q218%iZU22{|ojZwuCP|GssS|Ew!;aE))M%M7vQbmnW>XaF)5D3;5OVI`<>ZB3H0gHI!sIyRZ~mI+cN^5%5qk7eVlbp${&9_0Q6VeZ@Kuf^I!Wrsfl8%kyOKi@vnw|WKI6$u}B%-?FPo&XJ!M+ zb4RU}2)rNLqJMrht019F9@m7&`!h*x8eA2f7GdM)b{v+6CAWOmrXEs`_}LeN`(=Y% zIotYYGaUPKAh)z8_S|!Fd)#4B*O}?;0=3TcV08qrW;J9XPRzmPlvX1ds`CaDJ;PzR zdd8nlxB7p5v>hXjF@mlA@Wl&$Nfo3sZoJ#CgCt)Y_*qc0hDv9&O2=6rIa_}kx3##R zeD_Ww51B7YGguJyhj;RoXbo2VYkW!E#^MOofs0e(P}{o+3}8%wDxS0bR|P+ccvG8{ zhRp2g`^v=gxtH`7GT_HqvCF|4;CKcQ%$SgV^CGVqD$jJbn-=3)O#8SdQoGy-nO3Py zUj?|AC<`sxrCKX`D#>?=tXUR+3K2Rgik*xM`=<4nB3qvLjNRtfE0hSctTf>NZrXJv zw*4kR))dU-uHL;D&7PP+%a;NMiFh}0z&j#1ZSaBx z=N@ri#VhUVMdQ2|<&*E$W|3I%UUEf&F%iq!CopD%Xlh8TIY%F8?>b$43$ku5p_8UwCi>2b5D8j~tuVJyc4J-=2TWJ|#aA|KNI9q3dzs zGfph@c(_L-P=^+S-=1@}85sWujIBzC6Yk%raVFCX(fy-8)mBIShyD~M{$W(&0xHvy zPD7yZ3L+$(&n0#Noua>Xx7dib30K7br@pHyH`&U+QcT3z^AvVaQKl4^xUdIyB1A4*1;V-^18VoMX^8ze ztB&{D2f~hKE2vuBj<#`7h(QjGE*^@kJw6aaMAiA{z3^sFcNa}w8(C=L?7FwI0)2Tv zj?dMOF`P-p@k~vHa(ewh&{#GWgOC|%e{Cq+8O6!%sDoEmV29~Mj>%SWw%!R9g_EeB;h6 zv8#GGVvk3r-8V8TIY8=W&wIqc#cI`-pqmH)9+VEPh>WQA`h zI9Yon5QR8-BXJ7r{(*gG<*zQ{+xb44SV+wGd`DaOlK3hyQ=ndb9hUO%4>QV(&zbG> zhDTVAY`^W;TkNKE*Ih|Y8s16m50Z@Ikof530;4#4@{5HFhe_|@wmrUK)^VU-X1Dta`TQC`a(KD{D4oU6w`yyj zZ$_7%Uvm+r{HmSZ<@J0*N~M%FZ5D`^Ft)ilvl+K{e`2yb{)Q;P*V(u5Os9Fl)=6Z* z>c#3*yBz*?}l)8r%B7NwGv30O>WZ!-<-QMiAy`uP10d zbQpneNFEM;Q-XtgnaMxa(K{c}C9KhIe=F{>a(C;8{R5Na3`P7_=M5BvjQG`wzyv_s zZ)W@cB;@PoS@$nq)fxmMm_ORfgrAe#r4JW=TBKXgG=;45ygvvUo+ucWhd@^&zOXCC zWS-zk=veqMEqdVH>-o#2cPcE^seRM0)8-322_(nPelr2j#~chJdsc?J;!UupVm(x< z_C74x?S;qqpMm0a%71}Wgc{|7Kx7lL{}>os1o4X@p7c^it0oqYqD~i$v4fkv&imjm zK#c;y^x>z%7{~E0sija{VmcIcVi-HEW*HRU&OvZFMC_yv;pClugM!VVLYAJ1ZyOmE zOfMORM_(`|W@=#SpiY{gCvxYhkRg_O(cQTEL>#x>obHTIZiG&YE8pjJ4fb`rrK=B- zWDd_a6UxZAtU9*I1YdPLm;GAz=Z*Z_0+W(1WbD7Q^#3R9`<7g#TPxl#)zt&nbfxdM z0@|TaCx6k-%8<-11rR)#qdeYW59+U{-jimfH$>hwa)g*(Y}_X^%u||GG}n#u7|K)9 zn2*}yMG$NBJm0P{#S)4zRLZ-3uiayg@=;sEJ3AaO7yLq>u{aSNvRePC(O6G93^&u`J5dN8z%Am?Rs`4j{L&rKPFz_TOAn)V7RM0nt5CLE|$Ww zQ@2Bc3-P!^DV<-;%^X)kEKV@NKo0E+Q1O%$D7^&F`~J|QP%c~)-q0HyU)&xqN;aQM zw|W^f2M&v3=qwnOs^n&982ut`uzOTwOj6_C?_#UAdLYfWo}Z&Y6N5LoSh!9$bY(c~ zRMAeNRaJ-(c1V!&+moS2qeefA#;2IAnR+}q9ZzMG=SZy<&;?Y@ts3C}-s#RZeq&UCt)*IYF#Z>I|yqFD)Xb*`Uw<*SBS{I3q+Ih4P&m zlD#&T_ZMEr{RN8YS~xhxPVuggf@HD@|39MK`3vkcW(V)jr(Z1Ek>1xIOPc^ar~%M} z%m}-&)BH^eUBj=_3t8vHQlBccY3w?`+v*j#v)Z+2_Y_$mjQ>WS>O1hJH27=)P)bv! zKLQ(*u~}a9y-cLxLQ8Vr1v$jHUkMCREQx(xvmv`2O6N(@XuXpTe2WM#IN{2t>VI5g zI-(e0zYJM;2~YzgmgpnE=d8K#aKVJ`n)?G2{e%{QTqsP5b^QCXG4pRz0zS7drBujW z(LJ9RTb2P$pZpp*T>hj&;@q?hc^;_KeDm!X?e*2K))Blj{z$ht4JY;-&Kg+qs;)+k z^pR4#%20iZJ>(shN>eUkxpn}H_su^2W3aiujjx#us3$eL&%Bz5F$y$@nRFf5F$>^w zL#x4N*Dn*AgfqfwPq_FgRu0?5xC;OAV~(uKi=+oF z?SNa6`tG3UZd|I5%ez#nKXgn=cbz2}Q=+jBUsVUBVzO@I9RoQf#tolo%en~jHHOj2 z4O%!M=y!})ays9b0Yfk`(}ijt$g)0TL@^(~(`J0~?4%dD@0@8vi8RG(DWXPU{d^rK z|6gzC71q?Zrg0IJ5*29zh9-tU@<&WS5GjI)H0ec9TBHd`7ip0i5KyEg2uQC|1ZmPc zD7{LFgd!wDK%(?s&gRUyni=MDa-HYdd##nVch>v;-uFupy`d8NnEoS-8IaUMlCOQP zYTe;j6Sz-^{1xR2!qQ0&_a58|1d~t^&14Mn##{tfcGfKGiy!qT@0K2zbbk_-{ z#F*`)JC$Z~fL;o62s0%0S4Q`yT19@XpqzQ**yMkFXeh{8(2^Xz*Q#mT^PVDQl%cR& z0e)BJ+(fmJU{l&HHIpY(M8-uDRy}Ouo=DH4TiDde8uwHOjNgfc>TW3S!OQ9#ujPh?MRI`ehXag;- z9)8JFFthEbKzR~)^0=z!(42BkxT7(YF!)fnZ^3M^CEWr6B%U@@EAEy(^H-@HuY^a8 zuH2XaN47z)9!|UL9?Gg!*;DP4uv+9Dqo41Ebo51Q*2sul>WI}A%>M6#zGvdk$FxE2 z8@@Zm8rMVVXyc$31#$1sZ&K&$CD@&Rao;pNK?~&$bePnnRhA3#`7M;_X{3LjJNIGy zCD&ngV%JGJit{OX;k(lX)GPp0y-*H&3 zI~AEAWNY4Tb+746_`OU`ZbCRfAPp7SdjjH+X{tN^(Ja+s8_-yEKX@oGhZ-*8=FYrXcM6PWSFc3ZI_*z<) zJI0lv2Id!S826;|7Gpk0OQ7XKnqrSso%=|k&gFQi(3&5B0AiWxpaY;>?_ zDPX4(l@#`k32!6DDLV<3B`cL{__(bcM|l{V);iR5v##?$HxKqkF4HznjF%Tr)V>V0 z*$5Vk-}EnBc?E1)+(Sv?hwt8q9B%W^*AiuPO@DWf{vM`L*855doSH!q-Cp@>)wy{; zLwabjq?@nRVb$jQO$$gsb&;t>f6sKr2gZcV)Da7;R3e!#h@MBW?K5deLJD!HuJ zTKowt;l+XE=mc_uaxu+kVCStbU*UX&hOcx4Y=4tCHYtYxhtYzi8j>AYAsb^Q`bE>HUcUCGgA>^Tv z3%^Js8@B8I;?D$&<$}Pm;&Q`tnI#^E0M(VN$|8aIlAg;PwzjY>O52D9fuzq?7Ft|k z_ezyTd(@%22kZ#SD12e2R8L$_)m@K+c{|Go4D1XE?6S>}ajUgvIn`K5W-`w9dh4IY zmru`G;((sE8GNs=Mn}h9|1*Kn)R$Ts@zWIZ?&OUSMi_<-IzOh^%r+pHsAICvhL|@@ z7#kXSeNg9N!0a^mG%8fJegacCiNISR$^3{l*UkDSb)Tj$2*jW10=Xa*xoIy7!a`N; zeLr28)QXE0w|(35UPOC1&Mz>?OR?_erPwb-<leyd3!JTx-!|e3EHb-l(Ivh z*}F1QsdAY6X8HT_JRJAlyMgVS^13hoOCsKle}XO(Z31L3d|C)&NL&fEq>@_NEr$^kF*~@ynh5mMY-yGf^KGMqMCo`m zsUJMVqJ>+vbQ%=!E477w*>-@* zSb8B9nY5A89wIdiXzK>r7I&q>m7h+IUCY5duW^g|O@L|`KlJ^*O`{4~q#dT}V&`AB z+WLK4`-|nPC?kD&W*RsKvUg&lY{HUS?e=l(#dV(OCf;O(^>C%5^oZkhedDl$4Tl>> z)j=5`h(@P9q={Z(-|=BUHzbh|bQL9VxP3HPG2sZ+$(%@uJ#guBLVVHi$8+<;4Z5Xa z*X3$Iut=y7@k}H3%?EUfUYZxw^PKOEeuV&02PVQvPg`A|#S6Av4lKRp)#=iuvXxAu zbTm|7OL=RV1u(Q!5V?FUn#iJTsu|$q03E#&wa-=x6k)m<;r9W1DApc>#hEfT1t58% z0|NG#$%Ik4PL`p1knPT)FWK4AoSs<%6-<)}yabxtuNl~g$t5eO&@6IG2X#Z3Hi^}@ zbHaO6l(f<>!bUhp0_D8~rP#E+IsCOlFDNDuMv8yk=?xsI>MfZ$Udd{_*l}g72yu6b z-E&qbdPbU;0$HHbF1-sRL1nR)BDKqHUJ7tJdImj9S4{5rOo^=W*u}rY;+N%TF18wB zj+)ixIjkngvl5FB;7=TflQ5tExm#gS9)-EslyuK&0vc##- zT{qhLGb=RB;A&w;*Vf5gtFDe;Fk{sRqFv;gIH9hh4bl0!Nnb9x)cho8FO)T$hAs6hmP*WU_{j{Vtl0@E``_a8nP(v~csQOen zCg_!!Lu%a@YaJ6n?6MJefS?N4-_?zKPK34}<-UcudaHwJv!TuPRs*eX=CO$t_Z*g$XGm_g4RN||B6Ys(bgYT0$ z2-ZD~J5RFx2Fs0r8l>zYIEl1Xgg*gh^awxFReh(9o?2p*%MZsHoH#Rfjmx;%C zv8nwf^iivKQ;jeO&4#Hq{rx^`rau$n1GRMaD`PvjemTL0b;)eI*KgldUbQ#La{GDJ zmdC+V@Pq3d7X1@^UElQ*?;T6yPgS8!f&%hb73cuBTVrgegu233y)~z79;-_duX>z< z$kFG2&Ns7l(Nx312_u!wOFwf*jqPi^5HBIrY$Ph+Ve+tGf+h=4w3a%&*GZ#Jl961i zqC&bK{RS5jH=#wzTufOVdk$@To5Po9=;U%E#gmivZRcGzMXJ@yEM*b~X736MN-Lu5 z6G@OGx!1lqO~+e#u7R%c?MhQY!;K56=uXbxsDb5(f)VlRxBeCvU~D5Xz&?GhwD8MfJpk8D|3EVT zJj)CUckqWa9ANp=)LYD{q*4x*e#?)$E&PZWC8u8jv~E_(N@#FrCNV}Kvu5Hz+*)-a z=8ZvlW(~EM*fl~4TbKLFu2KB8yd9t+_Ra~}R=v?Wqo1)7+<8DipPa}$YAx-k@q-E_ zF`qG@NqrU$&}!&fsD$UrPVTNdwPAXQjp=nUS~@@u(xDqG)&+uN39l3mgt zd`qzhDZ$|vFBJD&uRD$zmJqxEHumtgZ1LP1fx!}qHz}6vJbp%8#47Z*Q&-r&!bp*x z;pDX(I=+kL>Z0%NaeOY9Jt%Rb-Fr*%yQzIQW-m#-PT$RpC1N;NwUF0xh?O457(L`}-p=b) z<7$@-nR@=W6;x2p?3x$97fOG*h|WAP1FGDm1`=Z2PFUzw1wR(x&sAfM*Wb*kN5~oQ z(H4Pf9B2hGDP!nR0SMsYg6lt%LX~LlJapvk6otfXEyfCHWG2XXLhHU{d#UR)tEMO$ zN7A}q3;+2|Igqy|;|7#*OL(gR_hR?VrYXow*+U^8{ZDDijq#oPMWU zz@<8P*tbrOFM=15OCR z(vA?{)5rb)`@;$TN>;@Q#rVSik5vco4RM;}H7hQp&oXoQ`r8SDOY7;=Flj@FUo~4k zP&Wd;-%AICqny4;!lY>wkecWES!h%zm|E6;__l9izkh-Mcn} zPPx6Tf1;bhs&j7|=Hy9h)3(``qErNW70smoMd$VeO2DO$W~%D9$MDdsP44@6ghzwx zSuKS%OH8e_!YY}&q$2V}vd@FeV<$E7Bp)p!oYCWCbCR%=@kvNWxmh0JlSyUt@3`yp z$#?C;Y(5w65KWaFWS~oB`Qu-X)`rJ2Hm{=F!sqbBq$8rCCq$`*l{^V`Z+3pyAT+Mi ztzMkMUGc+AFh%7CSogwNJBF?l0=OfnO0i}t$U8hn(j$Lf!d7Ccw9jAiJ!Trlo_NaL zgD#)jLR-dCK>>^w^cfZmJkkaX67&fi^uh(bz`!8BLHzX%Wcjxbe|?5_`tw7JGV^0F zFkvtmaS?Sd@Dl^54s{7EqC`|E$O19pLJ8S|_5v~9!5(o`w7~~RC@AnjQDsrlg7$W` z#II+_;Aj%cH(BremKmLn`y1`;p7*D|+5P9v=ZaJBin%-o;{@-{rs82>B*L($5dZ%- z;g7^$sNqM2C5Rzm!9{~m|HB(3#6U%U#D87}j0BYf7PX$%RuBp^>_08?=K<*&bI||S zgYu!lO*JJZiob>Yryc*}H1#nn|L3XZh=s??N*+I|qyFbJ!GfNog7+T|{Et0G;X+2~ zU~=Szz>EIJ3j}RroYwZk|NnOXYmX|SU@YbN`)bM}|JTW*bR+=(@%}-7NNkHrY^gEl z2Tu$8f63D{-$(K7h6*K$*l8G@^%0FT9G_t;}klro7QQB|Fv;(7zodoA)2aK zm#aA%PQVJ{kZFU%d{UvJf3+Dmypu1<#dT_>{ppJPimVp3Q359IP0a$+cW&f2A8Iyt zvm-XRJ-W@Zq2LcNI^nYO4m{M36Pbd&g0BVL+oILNzxEUjmKqM(8jULq8^7+2W~d!w z!3|YM`i;H4-b+?goV6YhE)==TrE)v>@vDWH|B6cdVLFe8Orut0e!9}M@RVqV2vL79 zZXuO`Ki@Z<@2%!RWCiq5c)lkxM)<@yLzyKMD5zQJLQzS?gZ?-BLPdm7sfEvFwZJ`I zbqq3loGp|a%H*=!#!i4(GlZ2t$ogs0FJ()I6de;bS#fl2A_Q39(k}L3FulN?j?N>z z-e+R|qZ|Hd}eXa$^<&zuU0Jx>l9$#AL3(5UaWLIW0dFDI{!giv-wFG!h;M$0yU-hwxu-|C3l#f??9DNXc@&hGTkucX#>PPC&hPF|W2%nS#XZT3I7^8vx7-c6E%LJTe zV!}0@sk8j+oKbaI)d~TwT#dl%s1&c4R`9}gH#h7*6BthKt*OC>z( z1VcRH#Nnp-RY1yRuoiL>a3%P*IICet3wd9Ct8 z63byb7xc#&WC5$Y-)O7+HrqXk_&iRy+kI|n$C9a~$zYL1pXc89*JlDPtAfCDd<8)52r{tuVW!Ljtmr+~d z%obDY%tyv?xooLJBW^mp4$;NDFXK@z5nzp#c1^0<)JuN$Z&wQkoI6g3f*(o5rq;Z| zAV=MK-~48|JMVt4lh0tSJ6W!;jU@Dmny*mF`QsD;>!^i(zJ7r(yeGmJ71 z#$uTj%B6f2idTvwqSLDS^osimclvv#a8nx?hZ8nO@dSuJnqPWdvvw+J@VzH&vc1Kf zHk`HuxqKQ#53B7X&ta6;3ctrXdhe@UU`5BkK-DJJ9%uTREXvkum0ou+V7X3X4#^Dt ziT}yC$zEr$MZG}UK@wU}Nrz-skjQcyo-ztyF# z&E59-mh9^Y!*>E>e#v&xXP{SH(-(3X(+%Xh=%yf^I%Y?NEo{%Vuh&1f`u+H`C!B>y zM?H6d-4(_n<71E7B7#QWa-iiBapkscH}tv#$eu5Y)A4jJ4HVpR?_Zg^pH`;YRR`fs zH!fY>9?i%c)nY#}VVbe6VD87iJYG&*)tE}e)@Ad!w#Y|^p#A07o4FtY-ydPx-He&C z=9_5B`D`KD`TQe-z#ezTZ8(d?Svp%(Z`k=XK#QS{X*C|V z?A6aXj%DO`WXr}dJ7np7w;Cg`+?BILp5E8%zuEPA@}uju zQDoz2+Q2>^i&1~v@O*wuL$gwuR#RXL_Bwg1%E;E}z#eDWZDE$avK5 z`S))TG^Kzn&67$(#~St0HI|E--Y1`7u59Oh#r(5i{Z{Q}hw7_%Z3d5n9M4hv(0~w= z$J;NC@s}p&Fb}KKL$5Hb4R8c|TjN0G42>4umz}q~8Z%b4V%eUl1RW^JJY}fe zi`O3}Z|Fx1y|#G%L|P8Kp?l$H26XX2uOH8L^dNUrJ9CeY(H?-(f9z_h^f{&bD9Dlj z?wM#H-7PH8m#UA!J1v+5XG-|5#ehlYKjc)CM1`I~6+0F?(z zdsgLn|9B0KAJPs7wx(nEZG;^(1JE1a z)bDX`QPLheR>sIuztBZuI?pK`6|T?g0hr}z=L{37fPV4zD>Rp0;b?AVr&@)aOr(|GwT9iYT#;CVN2l`t#Gba8`73Mhqgqy$l|2J5t z-gsTHZnOFocFggjW&P2zjU^fSNB_f@ew=_{6^?cTw!0x6M)G7DI^UP$rDtEUxe_N2 zPkq9OeAy8MgBfF0{#SM1)%fXbFh{hxJ+gJD%%#JWs#C1yE6P3&4lgJ>3mYEiN#{l?Syzj+81raZ5+J! z=r0l7o-LX(Pbr%-#-LP0rmj~0Al8k20S#XWHOc^EY!w<+yRH2XXi`say87JGOC!!h0 zTwbDM%q)WvL?YSM&?sBz%6Bv2MeM%^8X<$pg z&Ts96t@G1dAFVoq+5N1E-gy#RyXD};PDq_uIdSMA*K64i%OZM zwvu@D8N+fOlC}zwVc+6)(p!d3wO_Uq{-G&DpRm1qFwOmE}+MZ$TmQAKO&m zLLuC}cP`TX_913qPR!(R9CR8bgckvzsghE&Oqx7)n)FL*USIlz1mnmO-m|2eu$uiBP+flCEVW8gB07swF{+*&EmGE zGyb?7@2GevybTwpRP13T3xggOJWgUAnC|IuvScHx%8ZZ4VY{EXRW4^eI%h)?FVMzb z=|FVz7FGV3d2E7&B6F&k)5ctKy{hs@CYkpqH5W7IpsZl~b#E_dAA`B^$!}?D%v4$T zukubfM7(FT27N}A6R|w3r=%}34kUt=#$x~$drj;IpO;JA?b2rys{Za1m+sodLhxqr z4?-N?%Wd7)4oW-u*i@6%ZfCeRMamP{=rT1tXw>m94sPF6xYZ8*wa_&d!iJR62aNMX zrG5rW`FlmvVPUNJ3fD-SX1l6Gf!#V=CwOw!;I%YG4uz+XV#YKU` zW@CsxXypdf0tgZo9C|a=g%6jP3>OLzg-&se!Kox)E{p;=$cZHrX#mWEBqo@MA|b5U zM|V$Ih>@baPZ#~42?_!3mlY1iV4*gc$RX@T%w~AoNO%sP96!%#qbpn+a(8`ZoRCQ_ zhNqFw$;-j(sxOnY!%BVmtH=yq#Q`6wkJcPtXxwX#1^syY-RX`m|eb)UN*>YLEU(}yl2+62^!4MwCn|oO*Y0oPo@6-F; zs>LIl{gg6io$CQZlm??ThTrAjFYPl;m?ccxg&R2NTP40eODxF-F%rH*DtosJ19wQW z4ob5-SfUts;uTqDJT?QK^%jvrbiiozmFo^D+36|G5qw^Y4LFLzVS}{1nJUGv(fLpc z#}_hyj~HwAQuBkU3*X-SEdiHMWa%6ne!wJE>Q!jiD6tpNxD9xz=@0{ReY!80J^ya@fz*j@^3l*uecy%?fBeb zdBk$Cur;R?_-?oEU83Nt=1~wY1VhPH`j+gd)AL62N-)~iI=P#B08+@k-UC! zc>gS9ib3Valgi^Hi?qmf>sqhl) z|0EOGWPb>;a67Lh-y8m%E8uJYn!~>bh$wO@*Q}iO+0t8OxwaKBRKuH!-$D6Aw$sL4 z?S9&4Ih6?vSZPRzC*sn@R$B;n-HB*2zc(V+gr!M+&9JOzKX~>eThNtdJDHZ~HuU>UAET1f0{c>kTMJ9+mZ=S*hQ*P^Ck% z^6)upq3&>&a+y`oJAqUJR??a38So?yl2!Zjm6F^j_vWa9ybmyHHoUt$7 z;yon`%7ya*8^=$!R-4#-{$)C!D7Eq^Rw>9>$92T02Xga(x3XU7Hv3*B32rg z>tgGUjT!0$TPB%O0ojWsQsOmcFII=H>(@XC(bQfCNUy#rV~(a_aK&)!a?yXcH#X!R zT&<_#thb%o@CSDE)QEY5O2e&CZs+ZR7rYKrJFTPNwL4uGyJi=an$?k`M6HlNQZv@`946vSs^#041o5^Vb^K?)Z=y zp>*qzRbGXirr#&lOI${=T~XzGA`f>AnOqjuo*rQc_z)CKLo3499(ZpLml#c0rot;u zC+UgwjvM^+D*58uDC5H{KctpeE51eeY!ARS4_<_SNd6d!WbFt-U@Sne2__et;!Ye4 zA0^nnR5=pA{8ccAXP47r#*6b?ppi41Lp<)h zc5#}KIo%`gfQ-u%2pv+CNf!DmuMvh_69qp+fOQKaO8t!s5rG4%i!fsqeIoJIzrF%<4-*7_S-sBL=pkvX@ zG639;KU;6Wdw?PG3vxeusnezJ_O$2!mWd8~d$t}sf>aV0S>EY9V*>{i(MNN>x?91OqZyL**PtCFIMR};(d}Ap!%lRqif5)2on@FU@yhq>zaxp2>&yG5hLu<1axB_*N9^mXrp99_qTMMHtj z`KMmz-Vlj^0FNfmpN_s=_X&3~x6iFlCTWv7FEgrvCmWqwuQ->U4HoUIbGBD3RjFIT zV4xbCvM(I*K{EJDni+uj5gJ`H1$}PNw|nvZCH;gIaRu(>#CpG?hiMJ_XLnvIjYf@d zfb)e(8c~6tx@;18^waD)K{KqwqBH{8&!JaA)7Vdw7RN1+I|%@x+J2aO*h87Pb7PnA zP$$@n7uP9ZCewKv9nR3Bz$!foA^-0KqLEs1-2(w3;l7)lO2T!WQq|9cvILRzojw^u zX5!o~nb~QZ|k}9uOk939qcxP$LY8=6M;;706O1VuPy3^$k}sY(M^0u2pU;J39T^ z1OBd%!T~`JCl4v9FqANr-qCn=+~O7){d0k2-BQY9d?ys<_O9z>rv7^Mag0fFUQSv= zWON`wHjUBZM$_5zsCi20J}#Aq1fyD|v&M0$Ua}fKuHjpP6<0tXiiv!K$Ej{|+5!Q(gNJHX3k#N6Q}o^W zJ&{oz&rQaV-J%t!yrkT>DXEd2rbvb?z?v5wM%?S$!;Vr#K`@J17Rmj1GSz3mmy_kX<)^Vheh7mWr)f@m_pZ`L zGU?D??B0(t8Y{+DQ*Btuk?`MSj>rKk>?=20UV(QflHE6cp{TUI!cT%#f~|p@0nW#` zifW$;1qkH~DCYdKx49hFehz=sYjxqEGped!ik`Pyc-*LDQceUXW7Osi+_2}yx5a3E z^lTt20d~A5bbbxSeILhpSI9nsA^T~BL|Dc<=+kW0m3_bDF`<(+fndM4JEBFc`MAbg zLQAFi+LYfu-Zcy_|07dSsVR;#J7cSB7m6Sz!>sEpdEqZ0CuZGY3dVKLXZ4Ps_#`AuzHW&W!ql0@t5o}yd(q{o$ zWE@%Nl#3x*eq1z3%&UloT7tdNl-bGEDJE63XrrRh&!9^2E2;Mz{D@aMd2=QlGHEU2 z^4LKD>0eR^LIt7s-He9D^?VKH-9$p8XCvu49UVIO=SZ?;7Kiju0?yvo&x=$$#QNJ$ndSzShEhF-^82qdin4ylpXrKIuuW4a>$K~_ApM-XH1wVa^DEqwPmnb z)$tBK^>nHC_}O>hc;JOYx8b=ioC)*N-|6$AY+>I zQ_Sn4D10bK>PLNSIc)x(N~M2!$~-=hq3kf6fMc50#Q|U%(zRPLeC_hNEd!`J6v)jM zDp|>YkVh-#pBpZ1ZK%XpZd4+0MU3;(&qqW2{vs5YH+(c_H?ZN|T%3pN$9%XvcfT!_MQ3eN~|k$ysw%ujoepY$S<-&AWAD4w@7b z)liS^9p@N5ZYal7vZ#})iG?@b<^8c(-~(BDn*^=m_JC#R%3_s)s|I4b^BAMQRXqVT18$C)&G(nex4EuWmTuKTc^o=qUoj>9HJq2=|r)W@$A^V=ZpRtc4S_3aM$pkJCI z(g-OTjqr({I2TmbGOtiy*UikAWdfoY^tzH7e~p)5(%s(mD0osR1^+V}1=X%By5(6w(!u}3f2WSI-OT}Ne;OrTPPNlXTJ=J5EUDLk2L zjTNYH1Ej)qpxSBZv?`J9{GHdm&QW}pH=3{(AT9<@=UBdQIS{+D;v&cpV(>7uC|qv$ z5pc;YufIeo=*vXocFOjWLW|WcM>?mcCshTqr8x#BAWw+K|h2p8pDWpLJ)tY&( z>Q_J_A>TXW%ioMut)GA(9{P;<0Yb!i7*P)oi4DIWc>VDy)1n@CEw&3Kz^+%z8Y&5e zw8@I5a71Jx!9FI_VZiutb$3-Q5+0}R>-FB)v30o&-Za@z9H;G|@vwf2Q*fa|)6}H2)s&$oa9>kyV7Up<&RFKe z#!_w$IZmV0cs8!u4k%{2nG-{HB$#!D}lAK)n?qJgidW)I`L-R{Sn zt645i058AqVw^MB%qJagBYt<2s{-04j4Fk!Wy;+cR1obfbRXq$*_q{}7z{h+tgXs5 z#`ft^-3irjPC1jUXI=M!mIb9Y@q}!OuZIu06tOzID0p+$`Qw}sWzB%qUSp+k1c=$0 zs?7vk0U&1<2q3Qo^-k>GpHLnKI2~F9>R0?6cLgkt+*V1sAFal0InMxDP9J^u$20o1 zd6_Lw)WNr`xF!#Vox528IYUusKbAlZ=<;iH2V+LSQp?EiJQ6YJA9?d1W;8)81$Q1N1!sP4Tbdb1se?B+j{Jc45nGdqxTHN`GGJ_&7 z!#1JbQ{s5S>rPG??DWV>hh*I$&mSjTgBs;4c$tfqxKas20Bl82N@htJO}gZ((`jii zAZyU7Dq&{mc;_|rTkG>?)E-odUqu*zH54YWe}3K8h5~vzwMenvic=e-nGIIRdFJC? z-!%cCFp5$>dz?}!w{L#?Y;W;v@h7d6V%Zeg7e|Zyvzzpp z=hQp-{tAYP3!p^dlb3gvKYJiFMkd-yO5nxiartPsSryMb=Xfh+^pdbj!HNcC;frQd z>#+8$9-Nm`Q2lO9JeAIq#o@Rhu7~tM5*Pi&gE&kO+4nQ3w>~HNYzwvWz=u^ zjYWkUC~iw1&2*i6hL>~y5Q1nxxGVWD_6|7d#b%(r;m}>k@>f}pnx^JgaWlvLF* z|I2NP+u}`MBN#p{kHaU5-7A;^xJ(Xz(b!I#%Y<{~3Tw19o+X%2U24wPrLRZUi)ZT< z{c2NnCYu4Li&a}2`4V~tAN88`d0b=a9N)ZSIoIsgH;nruk><-hD-D`+-Tf=uISKJ2 zksnz<)sG(fR_baj=pk- z5?1s-J`me|<+#&fZ0n~&ZwZE_DB?CeY zlq97<_m@o$6D?&I>(+`RW66zhPhTPO)!R<4c{jONs`1q%TS_-9l?u=sG=;2+lmW7= zxCMy(8(J8y!L#~#-SO&D_2cQas&y>X&Ag~k8U!%v;m`JH)wgIf9b**aRvK)u?X3VyHPP|7 z0t@Nv_FpurH5HEY^6(5{Tot4`pZx|bugB1c8{M`CWES3cKEK%W2S=NmYKnvuYUV(n z?4=o^$_G^}*VUQfXT@g)n1iq_7Lfl=;H=bp!W=5y3^~-ux?e5Qr$sGOwT^UsTx#-I zhwgTpBkC!T&zLDoJ{R~X?-$+TC-Th6QvL;!TrkgiCVReAP0kopsd-;2G&wbz^NF^> zC7yLXJ1jIQ(5=>%NR)dZB2I~h6Mh1i98Bie4@lR4#B~aR#7p1lMa5qD(noLDf&Kh? zzqyRd=x=N4GeAiD@Z-p;H6#f z$4`8@mE)ET5I3cdzVn_^9K_5kHGBnD$nM3^(`lU#GL!`4lS__R6`B5%NeG7!%y(uS zPdN_Ts%WgukyX#W>qeR*yp*AlOvRV|qDhUVob!GEezIL4-q~_I9S6kc$^I1pq!_IE z&4$L=W#q@Rv`a8X0ByXJ1ddfM%!uzIWNxWR;!~{E7NbtA;Wj1UiHC&SK1{nyY%%&3 zT~*HGh|iXidGw6J{rArVB%G(p$|uy7xa5!FtE?hP_N<#Vndx0U--qklIr0qNjuL5&aCw%_e2tWg)4?J5p85eV>J9A$s0{V^Qm^zf2QOAC zM&hst4Sk>hSxV7d(7s$Dcc9qhzX4c*jeHB!Lve?+Nz!U}oAHJ^iD2oOWOgq}ht&O6 zoad92!XfPqmHA11?2Dl&Wtsh+Rocuo;p2jVfJFT zFYbKTvY0KzIIATW9QWNDOPd$@;lA2s7P*jv{}*Qd2Re^}gfOwtoXFkz27z4=2Co=- zYJHj1g5~U6I2$V}FjAGR7{DCI@A_n(%4on_{7vQXeefU;dJr{uSFWHmK;W5^Vr;g- z2IKMQupV5?)Auf;7(&t2_`Pepfd6MyO~&2p(s@Ws`92iz@_`8WG{>VM^oVRWS9Xej zSmo=m@#0Ev#f)JhHHLJ}rfEJ1j`wb(Mz!KlRhz-Fe5#UE=O3<6pGelV#rf@wH39(> z!>kV)MLGeA;3-A!yB-6q(DJ z2Jv4oIx56>QdCY$OvJ_&oS@2EFC;x1#P*n8X==oeS+H6Y{Z6e|Zd_?VW;Ak+Y;wdJ zmE}g)@|MD%RpYEE_Qd<+nLsZ5%?N;+wN|lusp?4nfDdIiozqT_2CbGU-eR?7j$r_c zrcc=ia!24hvt)@0k4YKu`iQ`rmnEQQ4X!yS_aM(VmBFY@8QY6<9%(O~#|+;0aPS}; z%^X{VOC{#x{#`8h;cT%P2Jo}RZ0mrET!39RUP#Pz5mNbbao){6+b?@8EW82>Ijq_) zqT415_@Z_uhCdV$fGVgLWMTZfQnyunsn%soUpz3A{W3BLyBn$HW}k_#sRoqx1&Q3w zMjN-_a`zFlq>DNXBgn7q{)hliMr?}Xtk4_ONa0%zKp6M{RJM_PSZsH<`0^7TYdK!d z0l4Cnf?(@Fr$vtzCy349Ng4*OhU$E^Ke6+1ZGmXU`}1`xx;ztKv|7oXLJq&-jf^x3 z4_e%?Sf}6h=i|ZjW65>YYb_8-(?pNc(Y)HtC;B_XpceeSHu~u`A^B?)V0o8X!e@xU zM2{4>SR}RwqL}{GMuR1Fg6Wn+}IZu0pH)fFd11B)-@L5xkCvAR#ubQCdE_Pd*e%dVZ3^& z^g3RA6ZGkNhRhgDhHV3i_(HuuiJaDbBW?~Q@T9XiE06O-Ns#O(uUv8k{UcsocZd=- zIUN=m*_i0asXO|Pc1zVrQI;L6ubZ+H*q)Tn)>?_K3O0R$qwOvnpdllM9zA>Rfh}%3 zn|LRa;+`%qte0IGDt^fi<6pmcffAp@ElN#-cpQO{aw*LjZ=3a9Pu>ZZcCX7M*grT| zYV=Mx9?d!{U1KyKx9HKu@br%p?(-iqN#uJlg7+7WUm12ok?21#8g>jnldbg9On>?X zEy?|4UZu~2GpC0^8}9liH9!Fzoh-YW(sz1ok#R~VQ>+{^8vkwmoPS_?_lccTWc=vrvy-stLxv-lKJ z6j9<}GO*Ib+Rb-ky)h?Q(Pc~d{}DU?k-Hd+6Tji+Fl$Eh!-88uKHVKp0pkd$=J1nT z+nEm>S*zq(jC?M6lCfGbKxE^uAXoVy-DEz(+T^$y@ZHu{i+@R&97L)9Zyo$g6@UL3 z#+bepC}Bqv2AlbGz0ZKk^V$or+L}sm3ippj|GWZ4BJ?MOkwV^`3-xGL>7HtnuHB`P zO27{}ef-~(3Z$`MrQnjE97h@8QI|kC#SiindP>CcyQABx7nwgz18HH{2Ag$K@cD?c zzp>i>5e&%jL4y`4PF)o90~8Q)X$+n?Nww#a0b@=Ud}yG1C#Q#eyFb$daa3L4%2n0z z{#Bd*K{-6WnoAY3o}2#kO1Y8R zli55vjb$3zpdRNEi2f^*4q}N4^NSNZZ|kY)<*uRB;`6xF{GslGILCFMxTd<95V`p^ z<{xD6{~8Gaoff0|9r&X`RSkuoytEGI=bD6DSy-Yf7gmtRFD;{M-b(d{m0=@nI(?IxkNX^X7js8fJO$$H&XN9b1zUIKs@QCuGh$=R@ZLIj?r*svOWsP z3u;=DUnCIkFV`qWL}!+PZ{G9$>NGT>#BPx2k-6f(X!IXV;XamX~6RDtdc|5;PShD1o6X}G>HQVHJ>kOn`I_S zGY!d@Emrg=@wrD;i@VYUX1^CMjznf}Z1}@0k1r!g@FE=xO%nPrTMgS#&O!G4KAb;L zQ|WB5QLXRl^c^TKn>1?4R>)yd`r0Zx$XX#$51pGDxV4>Pzj|k+Z7PW6?)`|PG+Q)4 z8rf5aX}64mVHJfqmKyN2(Q%gDB|}XyEC@WfI>K+L>2!&~k56zuMnJcthQ$2E71_c+ z_ltA1qmj$v*{l6z#~?W7bLky}_DVzPhZu~>9>ejStbnyX%?e$nn8&qxfX?X0`>^_9 zmBhRFz;}!4&P(w2r$wWD{@16D!I=y9zyg~kP~}`G*If%DXV*~SF2K`1TW>Z)YLu?> zCkaGS%m1=pdSF|vzyHQ`JE7N9pF|P!6mV?FjPq7!mvjm`$Y306B zqm5-9fEy(4j3a3S*1N_cHL@|oSQGAH?GK9Szo<%Qb1O!`OTB==oPoQmS=*n3vE4io zD3m>C0?#<-s`OAsHRe;PJn7`5=tHI1t4)J(aMa`Bv1tQvfloL05(V~#=2x@(fG+cZ zC-3%y@v_QQk2-%Ch@p6WISR;@+N zG@;rBm4;g$1ikpZdBzJ<_k2bHi4?B=fCs0@Yg0lYzugv;$j!iR7d_^3#cb|9GxaSz zz(L$C{GRNS;9{Oubsd?MV$>!MTLA)I=#^C0IP{0+t zfTHzR(EN}3Zcdimw?QgNOn8INBL*AYqzWWC3|Z7@&2rTZEtJNI2mHfu)C+8IuH#QJl|msDa63`uW$ZCY4_(-D!uq$z_tnt)NQO!AZUd=zU7%kfTJSdNlB? zHI*Q|iw)c!u-VM@BunEbBZVZ579JR+P|V{Qx+tCr4wYJRObfKSFOn_go&6ISK~XA5 z3Rd{m@n?`BB2{?lxx$`GS)$y2ExLsEchSd|ud9J4XS*Y0X)Jpmt5K;(v6k~t{EV_E zH~hI8lf7;)(Va}P1XR-It8_B5I;6Mx*rj{s-3>VU207Hxke1rK$u!H%YoqpMG6o4C zJlC9Ie9up{mL;k}B5z3oFwh{TLxcw?$~CK(f0sXhXg#~{tk!!^KF=}yZ|Bw_^-D3J zjED$F1062=0quuuzK9YAgy(un9NLf3Fyz)DObBI)w3$k0jR@=UG^c1_wWviK0a{QF zt*QEYf{|d`F}}(OZFW?3+ek$S^$h~SYVD%$%_mq9%!^Ch6j4cpr&&79DywG{eA9!# z&fyyCi9`W9x*Ml}J5VoliWQf;sNdrxvCmLPq(Hrh1~}I?(Mye z1^XM`^Q-wCO1I4M%`)~O^X~=Z_cIuyHs&ioqZRh9&7!mswF4)cy0Mcg^AYXiR1lfW z9F%eU=M&1Muu4=o$)#2ALD!f$2-wbj`xJT;mXP4;ziZHbUz^i@S0_~UGQ#SVv*YJY zWV$!{)d@enqvsgh(57sF&w+A+RopPb)*rD44Eb(6dy=XU=fMl-kKilexqC6O=bVy|aqNF5Yk{(aJE$`pxuO z4D#8mreZ7VEG7n!>JCgTTF@++oz+8bisyg+7BO^OUNq?+GjGA!GpP3<;~Sh6)*Daa zAtMT{tG(Kjd+Pd;nRZLCvrxBZw^WNkwT!u;b-n|XQVd!e93u&mnco|UR}ad{O{!xr_Hz!@qChhL^i zQ%o>s{#HcgFrCR`_5FfTYoA4h&|t~w#Lt!5dC%HP!x@WVcqu|z@DFr`wcWl7*lYx zP&H}n+9#{^ppb|`Id#L+Y`3ni@0Z_%H~#sz=R+=Nsa|A!etMuw14^96MSmfU;z#Sp z#N6!TnF7TLT-W~cmQxhY(k7Q551@{zRZgpl-wj7LqEe2a*2mte97wk6QX8#GeZz4R z-+Kad*A+&GRpd>Mq1R>YpDtQ$g{frF=#;g2F!6CY%z>WjAsxYi4a!h7R z7v3KhUJN~YSSnC4@Hp@EQGj4#9$KMY@__63V4$SqjJVs3r|-lQWi8Y(mB;NZxrEw> z+vGA>m+*>sXoe^>%oGzGFNghMDwhKxw5{e0Z`c_+l9VBfYff_y)cdnv3$qdRsb@A? zOC7!AKO;1m{4E0U^pB-5RB_iG5TBM0d|@qS*Cg;G))Do zO_jj(JSE_<;u;;*YMb>o3HSn%81y?t&0oFZM+N7FxPPy_EdaV}h`1bY>*3m6ZhbaT zl%mk)WYSV&D0G7SJ7NNoXhv?W8-=K0Ro%apu8WP(i^O z9vZ#sw4**QG2RE|3%@;gQKj))E3otUQJxT6M2J4w20mU14tt#Q5F@_jt61)29T>wQ zAT1?|lBpjOZ_Y98jMwn{sa3bPLge@j9wCv!Lx+LfM-5Vz!{Kg7Ep*%E6jGBU8F*1d zrD|z5o#njXqF6un( z+ykx{Z6&&jQ)I7_O?(7QW<)295f}j@t3ZQJN(a||a$>mg7=mn^8m~jzJH^kFE316U zsU~DV7C%?I?~Y)RGk(d--wj;LoA0`6qzy23M>QBbqaJczLh<2GB z-ma#Pf1FCh7>V0BE&Z2`11@OYXNGw$+IGF1U{r7aBqh$!7^pxzfI4Td#b9=`VgR5ZXFKaa?Xlt=43{41S+Yvnk zD4Y@Z55r=85Gv$}Txl8mwd>EMG@CUwG`k&;4)=Ub`Fpm|K^ZU!P zae=9eaHM8mUFKxHyja?`tM~hu1Q7sHLYO7D2n#~)Nx~?Ct(<`;reYK}73z2LK?N?xWP(@ssJkIPh z4jPaF>lHd+keJ3eA4$mJ)?J<+yLWJ~lm)h%gd$X^LoUeyTX?3x*>m0sZg0g@Kh1$c z+M^UQ8kG*@+hm!`ejyhbZ4cBcHF&zp5R8;SxT0F9146-@r_Ek*bXbY1nO_q(*C=o_ zmaM^;+@0Yq_bPZ|&6}$7ik0%rjF1TSnrkK7dPFP9I61Ii6h6v6Izg1OmVK$7_u}{@ z_r<=3tbq@-sN~E~lIfuI zDia?Nst?()h-1itztbzt*OCm08cA_OB{r9f;8Fz&9;mrxAX9DxeE5HKy>(Pv-?A-C zaCavJ2=0wGE+IGsZCn};?hrh9aCZ;x?(QzZ-6goYef_)dopZi-@3{Z;=-%DE$6kBa zT2-rR&RGb95HgxA{gS`5?MS#Ad+1#VoaMVh=ba_xgG5nl-}&bx_T%?MOyT`6Bk}Z} zUb-}>t0wxaI&_^*cY?m1E%xHPml?)x)fa@L)I19tI=fGw*FC$Phb^>YAB%&gy8m|5 zs4D~Z;_b^70PWr2SP1S5CWL>M$mD~;r9uBlD!MnuCWJphJ=3MZgv8_ddP-<7L$lWQ z!+nlGDbY8}YsW}6r+oB|Wt8_WxL6LTzcrBV>h68AkT^B}$&Dw=b_`}h%33u2ZF!Y9E24U$z`Ibq|4N!luqe-oST`J0&^XO6z>zBJYT9&!#!|z-V~s zfPEOXO@Az7FS)Spn(7?P_hP8ruRnLwPkbc}mY-*HJA28m9$?jsV%sTr*zQ5w8(c*- zSI2x>0`_(i1Y`DyKC&*d$xHWKI?pajd+&+80?dlkoBW%RLg@K>+E|G|-)RXu_nN|f zL=*m9yLY6hw~-$#bzd$&j5e!sq&lKFfT{mB%^ZD8KC*?k-`yZvIj>R_rq1a#56ulq z+4&QMj5kJWm*h5<+4$9}YtQ!aWb%MLK`GD~gyk*rd!kT7_{--3OJwJ8r2KWg1?t5c zKV5Z?C3OFc!HlDmegV@Fm}iO;6Ugw5P+*xMK;TmnetU9upKb>g?93Lkz3CCc8C758 zF&Kw*#-aS5AU|ocG9!=#-rb)fe;vkb8!rS;YAaBw{oMV|YIZ9`ivdPTzVun5EVoJ$ zN{k)z%0dr`N&%UK4;m}3>n$LR zP;-fFsV`w$#fQTanW`jUsbPhW!u_aeC1*CF*GJ6tLnn=a4~yG$Qef$GVjp6%sGy#Y zX5VoyfySmoE{-(N+7EuvP#n2BM8yri2uH9^-m9%~!UOPy4(IKIGP=S?1V zNY4Bv{CCu#!!|N-D^8~d^$0Fjl|C$?bb%;^x;qhr!J}7lZTl(LyQB{B+c1JBpQ@ER z#ZnT>npgEVa%0UsS7ikfJLErw8w7^aU$n>py!>^RG|P0em<^ZmwA^pxMH<^wjCf{s zQhr5z%L0fnUWEkwzvN?dbk28BbpT`{qiSU)XvfFYAHG8Eb;{t$JmbeCr#oGCdB&(_ zmRc8EtGA?OjyB2UmvM|NUlyxki8GcpzWGsm|_F#(o4vSZ-IBPw;pli~N!1 z`T2W?QlJdGsyMZ1`pZg;@K9D8GZ616WctG!3_Kd%Dd`50HB7jl$SD0xtq_}w_e6eA z9w#O~el&c%$=BxqE%rRei^xC;)(T?gIhFE5-z_+$^M>-wi(7N1O@u4HO;zH@@tw5C zg4MrO1iL2sU){Q#x(fg~%X0lf_)<`9EMvVG(Z;A7 z8g_+-NassW+!s<3w=QcsGD#{NOMy<&x&wY)le=xC5G1L9p8y)M8mW?d!!fNEE3`C# zN#aEopOH}M*xa}LR0levRH>w>?Y;U~FP*mb?~!d>9aHa$jD zHT>WCJ>H7XX){So`(Hm+{OtI6MM`qjpCX{1EI9xm zy^ZI_HUO(_TY0o8&evU5+HMovJ+b3*dvEcHW3M_?7E{)uA0q-R^d<6LW25uFTcld( zgIE#bhOpQOPdY3I#e>Q{@?z!D*qz9-462k(Xm}MLVT*;SuuX)nR=Ci3-xX@bpIRAOM&xX&Pw{me$%zK^EPF7&( zx$afD4=G&-QU}&tU8w3Jj4cuP-d1IcO*g)eih(MVt<`2UTdBQf3&9gA+d6stp(O>_ zZn0N`+4p81^9zmGO5_auorBlg1j-STbX?@}g3;^!ZKXt&e}8zBjC?A!m)Z z!eAENlQ2=GP#neAOhj2v+um>|Es!VB9DrchFO{z?P^!fYr?2?c)50QeGp=e->RF6lT zg!8U~{Si7yaES)sEBfC0yJjm;D_I6_G-UOHKz*iTJ?>Jfb$>gZ!-F zq{o{wM2Cy~NVpqGvn2saQu?XL@iLT);L%aAikk_h)-;K`nlN zU|W4-r(83A3d(K6;jh0@OW7C2einTEtj%76iJsh3Ktgy-9*&m=v?VxyU|lsLo>!V? z`FPcv9MVyyj5HB2z$c5Hhk9B>YPKi?xi2|nU_n0^L}|tH{uasDh1)vqnLx|PcxQWe z_Xsz@DYnUtgW#U38St$~VsG5p%U#g>;a7xjRQQoibx%$BQC&U(=Z25n z#XLxY`uQVGEUp@6j^uA^-_q2d5Lp$NUEHA z7>>WHw&HgflJfjmysz-+)+(mdKPbHx2w>oAzln$J1qbR$uH5zP<7fjf;eEfZQ|+XW z)FiOov14LOw1fPxRHf(pJDnOmY?|o1ebExW^}yIy|0>h14P_-FOqs{xrus`BLk_}I zT%+=*r%q8He=uf38n~nSM`tE$7YQCdE&%4pdfBt+4p0{UnU$?S`x!v6Hjm|0K&?r{ zJgs}*_xn?Ls`^8A;IZ1Ad^NG&x+i=wVo32()IE44cm_-#2z{xsMh`O`1e;?Fztspb zM7JT2*JqbE)G>UUOrbP23fF&p+{Z`pV%!^xSv$BnmKSt5{276X>U)1s7|B2>_q3D3 zJzkFTVU-;d@IjOrHekjv-KEIsuyM5G4#~4D{3}=4Eh&i^efNifpNG3}sEqW~Ch{`3RR=m{o{=jM)pjc6f!Lr2BqA|*#Id{z_Xl5-wDP3AYt7Q6A77QW-vwgG`; zm%}}G8TCQ%YE^YYo%J9K1roE*D6Q3=z$mbm`V;dBaD!^kg^Rj;rKr>VBLZmgVyA0D z_WuT_^DxTn!k6_CYgrj~q5w$#XvLuFx(P|L@_9f)a3YHjaNlfS|tYjDKgs@?-&k~v&Z)MYPRL_s# zu5zrkR!*b*fWYv0E8q6yXWLA<(VtZ&{u1cJ42yly+qu&lXCdej!kFCsYKN5XO^x5q zzFm=sJEh%^C8~UVe^`I(8UGHrLWITgkfubHK&PhB zG2m`Gkxr4A<|O4hgHPZ+n)bV|buhW4WrE%IG5)UWf@FIp2Xl72>jFDX-6q`ayaPr^ z$u931D4d_DENvmw9j{<=)D;9xqlvRcBV!(ZjhV#rQ#CDt{4qDTFT|F4QI%iELIdP3 zCc|3tHt$;0FB6V_N#X#WQpfIdTEqU9m(Mw&#i9#0Od<`zTFc;#NSUcm;^?UBV%v0a z#AqyDXn)Ax`fkZ{I}7h}lwFdSO9Hgfh0s;^(P&%jXc}Kf4l%hU3Oq4|HK+5w_?^ao zc6LZ6DNcedR>8^Xpc$y8kJP^Jm9$Bq@ab3`_|B6fLE8NS7l(}s*3GIRaRo16oMHg^ z-9;fL9bZhC^obn{&_(kx_E%A`41g}8U+_CJko%2N|La_lS_~djvg9a~>dta|FvPN# z2^llDr&AO+dNeL=D&MAMnch^>z6xRN!M(YJn>p_kvKzNE2#5OJ1w|-k@7`*OHhM>! zD4%Y>46|}{Z4(EiWl$y593F!$j%j|aC{H-ASKEyK1!=hycVF#gCz3a zbsTC{K@#xZU+7RB&~E&ES(DjacG#9mS$;eBdPjT>Ue2%3>8>brGKbrNk3hU}ywN^E zYH3@K18pBkHMJ8s*J(TMDb1ttAP$u8aFwd#B8V3B=7C4PN9^?fvyzwZA?*R5ni$3_ zWJe=4vBz4pk|gO)0gG-r>gY{wyiqAvbQ z^FOf!h)rAz5>Z8f_=7Q909gBOA8k_UT+g)SOr6obBV)L-9zaxMh9*5=KlhX=E1Xgs z{>C8@88Dk`1i@ zwT9`N!Xk}Ey*p(if8CNRf1t|8QXjxxoJ)2z{_N`s;)YUeXt`4_$7RO4lQ<~hOF zv4wyA{2Gy<3STm(fM?nG1b3~S2xs5^H%tnF1fG8+mO6K`h&5Ratmj)5qcu*{2rxwV z(+#whjKn$bI#lwmQp{ZWu9!m9GqcsHxRRVI@Wf1|l^4qNLN035&IXF0+?azwKP(Ue zC9vJL{?PR1>J(X@v+ob9@6iQzHdmk)a93#LcpfALo9 z$;qv2&o?X$lFgdvh%Ct%ll7E~heY&iWrsank|R%xg6;pTpY)>#{q8aad;K^TGXQS1 zp6;P-lhS@N?wV*lre0gOpnyzgW|U%3k(O*)5s!xNxYUb5q<;$si(5?zevp=FJy`%d zB$_{09IYG(UX%Nnh#(7z=}q+X>U+Tl_L}I&DyOC0Ww&LAhu%h{Xsvk~>ObwIf40#< zrNZ;<(&?6HNCoS+c(yzB>UsBE*D$KYdF|*jgER4VJE1NG(jd4&}0bBGrR! zTcY5PS0YCF3Se5dr!FLA)~{dZsvU{suuK}9D`kW6)lIg0?w1O}#f@c7ma|U{wzS(j zg$MMYGmM-n`D()X87LPCTd#b|Wkfe8X~jXsqVIa&Mk#38I_I##)f)8dCJ%xe)ns`j ze?14A9u`7TY|Hg((?Zj$tQ(Q&Ca87QpT|(iJvE_Rw8q{;4@h>!gLs*Ha@k1G!+O zJ8@aH%gzMv#x;?0Xaea?jFLn?Bw1ZOJCyne&6)AnO&IU75&?{66|Y7cGnZ<;NZrGbxs!ik{*Za-Lmk&L;imO;}^omx&Tb6qF>GpVf zsi@{0CEE=tFVo`2ZX(7)sKB+74Y;?n`zh&|US%oU`SPuGjqt?FW`n49UtHq`Mh7)O zxX8YTCdE19Ls}8IXd=0N$}ASjzFN&UkN+(@6lMQsMm+v@6Sa!4U1(*)hXAzv^>1$C zt({5#EK{oiF}M6FJ(_EKT9Zc=&k=%alIjRqy> zL-A!a0<)aSLqs{#kBT2#ishr}V%!u=`7UA(e^1JoCbY~)zqJ&G=1=G!;GeYpmJM6= zlO#WZC_};+uwf!npc4YW>DcGd_a9%P8?DF_FLaku4r^C@gbCt>`nH@;JU?&U>ghrr zU9S2#oN(=(^Yk;aA)yq-bn_@O_85}Hz>%rmfhpiYN}#nVVub&(s(3%;-yDz{s+!U( zk5%$#N6}7cXXIuJBBZ66E6?&!de=4k;{pZf^)`n{mWvF-k8h?}-dpUZRS^l-$hC^u z;_fd=Zc;MTf7M+EEt-lv#!!5P*eIg|0w^k@v2Qy~yYv1`U9Th%k#lYN9CY zk6=MFCqVOV;vH@IKHu+~wz^nq4SiR-?MI~sKI6mc2+31z+vw*iesG&3u=lelWch6v z-P2Px@EN)B&o+h^84zzf+JLcCQf2H>V}qzXffk2hU(Y`)uCyj}?7yE!mC3JC7UTiGI?1 z<*|FbVm7C@^n6j9Q5=l5TTo$^Z90(RVqI%pNIt%@a|k=Wjn{q5b_v%GR~qno#t4;F z^xi)P7yF^nM`8#)h2Ng8QL3>=;f_q&T8*<>l^A?nZ+8`b4$;>jUzh|?KwV=t)pKF8 z>wewY`^Wvk6w7i3-nNhiu}2--DLq>NECqE_`G6mT>i1K*A>tjzl1(8&f*$A&11IIO zc3pO_o4KN8`dN*yci}B{Wj5U(<+Nu2uR&-W2}*F+G}N-VII%0yR0B=u?)4t5gD?&= zA6myq2F|Fjr8D^RI?srP-NciGo8hAVp)Dv1#s*k2IlB<%3D3giYdjutr3GLq9GbZo zs8!?!B36_HfkScR&!PolR=1c}TmZa;JwZu2bcWX|KeGVcnFMeD2(Fg;Sx~JV5N^wQ zcHxHMOS?67i>{93@%Mv(3hZqJlh5%a+2G3FNY&i6`+Rvzf>{+|xtA*m7Sw678_%_< zW=0Mb@wEK363T7}LQmcND!rSE?yTRX%us3G*7L7g>w5SI-D#nxR8bVvb7-->}DU!*d-zPUq*a z>&p}H|J@E8mG7S{?f4N+!p+52n%zN<0iGH-{RWk@9w`i%u5L;Nm={E)ggs6$*D}m< z7Z?gV-25P$LA5Bn8sG!m*~jIRZb2iYW6MgEa+O)wq661uu4-P`$3?RK04j@t2t5-L z*<&SkQRWtpc4AdJ{(q~~p0!dr8$0TW@1hM+6r7XJ6=jq18j>JZwNDhntGsUl*%dd_ zlkwj-=_xEF$Uhm#hljae1dAOCdSD^qx$=I2Km&#Tcb7Sl z$4Vsy@v(pmZfT=c@A_0Wo(mu5jXy3B{`19;d|SCkD0J{Ri2#qRV;v zYA34;k)}gsUGn0s&ySxXJ9r@FQ6s#m0cyP6U^IrDNoLIkiA8eNwadtv>BN^%XX)oV zvc$^BNk}|324$4c=7!JEb>2P$D*Dq!b_B=RjNCzL(4m5nJzE9dhSZc74{i`UHNM2E z?l+CgB$ExCUp8tsiz=QL>V=!nhtFM&XLj~AHSl^prStHJ zd6tREHNt;663+;peW^-RS_nSsN8hR_WD3^eX5AkG&-?2cu^i(Maa1g*wA2`8cAJRJ zn2r&kI#?2}F6;WPyvNnKZj{5wEmn0?vxX&A#IASH&QtMV#)d;M)m%UGP!DZM=}MBa zqxmOL>nvr``h6CDlL&s3Fw_H;YP1 zD~IDqeD1B+3`dbOd0iEl>#uH^injONi?)><&B_$6rlVJS!HQwIdb&D359qeCl-Qw> zyJxeV>Yz57BWC&7Ij8L_7u1_{5dq{bhU{yDsk$2i@{o1J0far$zuTsD!2DF)4bf+W z-Js%=BC8Z^p>s%u#++#BuPuxaX%CG;TCe-v+gCuGXN*k;m;=5C8mbXl3u%mox1l=- z{+qS)&02DZ)$-V5negO!++@ckoaj^3WoaWd+75g(s8!bvff5SXB#I1AJ5)7jN?qun z)mz#*n&>urk28jN8zASNomG7F8^o9pr)U3!DybhJAIN3t6Aoe>qqHDlb&XRB`w7nc zjV7NgpyJ6mw^$)3l^`>kz}V{K;u*9&5dDD)RWc%Dcl0;~y&RT?(aI6nao6kJidEHyyVf*08+O<2@C=L~6jXxj zB%SA`n$?X`$3~y(5^YE7n&-(4QC|#)q_RACg(hT7TPKy=d`C)k7U(9d=*T8_pDA9U*V;#GW?*iV7!H-Ka$vF95>JP=vf0qt$ z)`Mvf{f~}Lm)X&ztcEU$uJ^q{alb?4kBtFAyxJwGJy>pEK95pq1_E*-*>Yw{5DXpn znei-%6NV!k`Vo{na|n)~!29<6={zvbsx4%=OyGK^ZHbkg*HR!fn+s{iO!IypqLcZ;u z+&8ti;a@-#x6rd!!AHg+gf;-d^jSm2GF+qqS21n=MyL6LbXsIc+vG0oVP-yfQ*&}_ zYCyU8Jd4@mAX*7_Uo5jibmO_B|8|iV9fIv~k+(QeNmQn?S%az}hzCN`dJ8qS5~<`X zF)bq=9Qk}dxa$(({x9fb=(7sbx&5}*cKK=QlTBTm(KWr?2~_EsFYa5=j5vtG-NvWorD_5>hfL>#ipAs#yqn^Uh^5 zO5BM_%A;*1DPg}cdw2L1>N#P>+0Q9QRiAy&H>7n(rF;=cIS|r;LHa&|H(KTQGUEIT zUn~OrI*{ebpu1>yXhXr=_x#z$sQcl=3Lyjn6|gbwMo_9;96m&hx^q_9KIDG;sA|>G zHMLad#>l8$0^56PF3I!O)b6;D`P%V&-g{MFphfqXW}83f8_(m>k_Wj*n>%%4;7%wc z9qhgGTPHa;ow}JbCn0^r9If&#h+*V?8Tah&Jwchutg2b zsw*xmz)?Tn`Wu$pk=0jOBGeaZH_Yp~BQI=iApF|at1f=Sj0aPMHu4tKFx~jBj>^4u zwdW;Ow^Y93vt@2|-Ot)@&D%uHy1a;yZqJeePyMj^A%qi?3e{q+h>1Ip?*2ka7;&e( zpa#OefhUxL%RCzr-<+mM2-jGs=89{r3#S;jx2xIkzvMA>?W*M_tHC zV!ox5O+W@xpKn|IZVW%#zN4l8;OFkaRgePs`lxpRsp8=6`5TYG*#wtCDlHK4@7BKC zokgi(XT*_(Hfcc>haS?j(t9DoA&Z5wG4|_v0xIy7Ue~X<*#;2KBSR*{*^G_-Ka(`* zHom?rxfE!RB!)S!yzsck3H~WIht3T}LCvu6k%vi{C@P_O`PRzjFHrz5S_2>1{xuI3 z@6@23NA*XPFaoc|^C^Pk@aAhHD$73-8=lXU_KctAouY&Kb71$u%~_Z!bnk(D5GQng z>v@vQ#xG&$$eOf?K2Q)-HiF#ou1rKZz4r9g3v^Qc=KF#!$&c#vsgcnZ;-dh%Ro-7$ zdbI2|vvUl`Gc%%UgPUcz<}pjSMeS{_HK>zGq7g!?NKt$-R%v`<8a4aG(;6*XEwtuc zlR2Wfvj7V|{R_WRI81Pt1(j=%CgFhfC=i#EzEZ|a{ltV?oO{lsT|HnXwvgI!2HYoK zbr18xTZZ5NToyQ2&-t=zP|boi$EBK!youM1Kj%WC3$7-yEr`qd&`N>=khTd_z&$Wgo;^^VqD!0{g z4?;C_b!-KUSJMi@zJ0+UYuzfw|bdAr3)N<%e6l2CPgT(8j8(n|n zCOh%s$dW&`;D}lU%|?g;9@Tf#QtWXGpMR?{ko68-^1?nq!!Op7YO%Ws0y-%Ayg`5T zsWcnaxa>JX>D>TM_I{y^rJ@=)+X|Ab!tYcwOW>YuB|Xw`A->mwr*HjN)Luq7fD=eD zciy#2{qv!|%jq@$qtbP0FA+%DL9S)Ti2$NC`Y7svT^MRKs1OH#6X?S_LHzK{4p59jXdq>&R#0NXw9ku7nS0}b-1D+CG!HDBM+TPOTrbs)0^<MOfkA>Bj9N~#{#7{T@ zyn}BMpkcJU2Cq>i@~OITFqI^jkFSTX1>H;y?h7Q=gQmv`be15Gag5&lQuMn~QGLd` z!im06CZ&f~K5(EzJ4RwEFv(`~N9Ti%2iq|Css@{e&}lmJyknpW5jDZiNb@{%&C4X= zuo z$Ne-G(YXG%FP|0p`;O@JZ;yoof3^4CT~b7YnF?&Gn01e5kae1*@ZnTL3izSO-p|eF zr24b>pAk9>6YozSUg)~1lkIk7xV;^761tdvlzPw#r&-gKm4N6cm+cE0&Hn@q-}=}( zW6@U5dtQ*lve{m(z?NT9J8BMyW_@+L9CwBdx{>t^w@|DMn$S*1Z|`4W8jxi)c=__w z-V{Nxcr+EW;&8Mr(|80L4I9rr%R@fRH?0pn(d~R@ zbq==1=z=Yby5NXF_};iUBrr<-CU-iAz5-rZl&Zj{EXJ!$OP~*{DLVRj<)l6kGnpqH z7s`u!H&PY=llTvUGP2DJd^V+2wYQLuSEkK5X2~;kG{Oq^$NCD8CF*|N-uP$WY)*}1 zmQ6mbXdBTP9k3y9aSWEa+uW%Mp1p9i3d%qr*CL|6Qgx7Nb{& z(B7ldFC2aUT|+^0G_3b1(X(xOTrwGl4c+UeJFOc1*Pq~Z2o^BB zI${T~xt=M@uf326<`ny)2O6(0h}eL{aNYAHE`Gb#ec+FMK|O#*^`|*f#GJME@q5Z>s=5Y`8@$y}uLa%V^b)h1O8rJY z%YKZOS??UZSYH8qN804}`IngQ-W`c&!w2;qO{V@h-3=e-9|F1VimLGcfu-^^)$=p4 z?O}zu^~VF75rVr&b!fR8l&$Q{#pRh66T0zEqU)gZ_i0ghny1;S+vqrxEegM78M$5} zVOt0n1hx>k^@E6G1~{kOzqrFU66A0}lz7Ekp^!MoMS6E&&&E=&QI!LeyEyU`20{GF zu&Hv3J-MF27-R=`(G|7fo;|&$-K7o4FRh((lJTTrdB7&E^rQe1lu5hlzYtQ-FNe~n zYu{a%ko;tl@i+Vp_%MY+w4_v_o43iVPP&tcO;ao!(P;=*_gY8hs6;VKI_pfxlwds- z{fgJ)PW?~fR8SWz(yn$hb0L@hBgst9<26CRSJ)@qk)L-bw>_%+2m46;S`bVwg`Xe& zXHccHefv=u;0H2gYZB`_%dEl$=_<<|0KIQaEJ@%*{IzN{jw0Ztcw>^tYH-fvhp*1UTHRV1|*}st~PDYFX~H59Mqag zS4rTQM{wLN!X~-Yo(HkK;@fV|09h-5t616x1dXH97z>SK{u3TiVqE*`>ULodd@`{? zVzqmcomuLRH>h3^yPwmRoJ~$RTlXx9eTChj6{-KE>cDsB`;ve1ef$PE6K<&7nNu#+ zZ(T2;)#`(=>&@`r?{8W`zbMGZKM#+UX~($*{1X^*-BWlFaEtXf&+S>Fn75?s@VrvZhFnc-Kak zd`~&;TsY+a0$(>Fyh{V!jlcb|4)p@RL}h!>47QXT6Wpe*@4i5(^T@*77xB)LI1rxr zt?3OZ{&#w-Z$KD?_KAJgn4gLTZ6F4vE-4oE(I*y+OBjR_g7|?Ii>KfElayb3kQrYc z@(_1yxa_|*NB>PQ{U!Vlnf1++k%9i7Mpa!>EEN%fms7L%1>4*up+vSbVX0qKM7OD- zoBkle9~r0;G?J#+NQEUOgdqe58Th#MnnXU|0(A%cEfldR1G?5;3TT(~+92i1FZ~5p z{~^VaBV&=n94kF26-oW;X8ccf?P>*LwD0Z8?Wjuo|4!un^##I*#S0`04E_2aHLCx1 z&aTsc_^>t$tO~dP(G2)E$@l+`{hv!R1>t(*GGPDj#M(wiNZYm+-A;kIwEtMae}?s6 zr}Cxyhcx`ZjqN3?{TV9m_sA2#dx-boBNfUoDMd^;wu8F;lhod$mScEVUFUHF-Ci@7 zabAS6&=9I$yXlS-fG#NSd;Q+i;L#czX||NoEES5PneD?+K0%FEI5IUMa_axOZWAC|`xQ5u_L z;IfBn-9v z_Eb?W54}o0Z0>Qn%*1Ak6M=U7_2!p6T_*wNmXBOuBjwb-ZMh zHS1ad%k((5xpa432I|gqvQg`;;A@jdT4Z$j@SkVm8%wMBJNQF|X6;15crxh=on3wD zN_;Vg)m1zZw@>%BZVe-3e2vfV&&I0RGQa>lhWN&It@9Kj(VYSL$n-E!>B8dG9wTMe zt*MHdn%#ucx>~sw4%eg);qA4?`@F*{`!9Q%7~wfAe# zMcHRBeaOgevQd`Y7#6@3z?kZ}gK>SrmkzlMZpLz9GxFy2y4T;~F{8Wr4fxXn?P#H1mQ`plmCM>#)8;{|SUEq`%SWaM-s66K zLFjQiZ4Qs|V!bm~50x>k= zNeA?#7jQ+bVk7ndig>QXc<_9ouLQJP8RKS`{I7u!VQtzH5g9OLelu+$26dpF_<%8s zrVj+0S;B)LT@v54<(I^{ELHCX;M~_5waq*pd{_KLzz~gMzS?D~F6(Hj-zGbzg7&d^d!z)nJA1)>+s| zzhe_UgQn`%8j8pG(Ac(5`4c20GR6%TV~$9!cm+$-x;axSJW>6~Si@u~yAB4G27yk` zI9erKUi|#E#o0^etwtKmI#bQ@bh#nTThgpw+Uj9TKP$J1{-2e|R}KYNyGllEuH)Ie zX*>hCk;tNLUNV^LE>ML^;WM{ z8}&*}%kz$RX@KqKPj9siG_q%tG1%wFq{|ALx!+(xyk)%_r)IRLo6;g7)A1BC+{d}T z8k5lh=ArVQjW`;GTFu&nu$B89xlQx10^xal!YHQ_JZcocy zg+g&v&!;LPZck@sCPUjw2%n=3D6KDJO&aWX3O3)|Nq zFzT^OsJ+aqwf%)bsdDSss=l;PZCKaLdNq2AyFzbq{fs9Tt$+4n9837sP^DlTv7%T$ zt)|R${Ak{#a(@22YyHfXqP?uDh}4N}-P<)9B5cUANTAy}oEiCWWqIDR7027l&%$%= zq%y_YnK*U?(Is0x;dNZAo~?^h5E#*9MVjd4x!)bBIirst#-1-fUXRI1E$QZ-bCYv& zZdQ$x&17D^b$c*C7(^2aYAoMaN@Pxj@2u@UPkCOWI}PgLw$hk<`ZP7eS7#fykpr9J zBpmh0tTR00Z%3Tkq6-0Pg?gil-P`l+X~qRV{hVbj`2zTd*{ic!^!bPe2^vNww^xIF zI_De>|BA!j2z*0L$m`qES;ctLnrpbpGu%V1t*md|4b zvqsYpgL;XvS*Ew)s>z%W-`yo&8o)kOKAC6EbRyM~fAw9RxaE9ly}hM17~3hl@yBeP zoVS9cvkoemcS`Gk(4$GrxL(U2-BD8yt`b8*t%d4Wt+~1u7Jiz}f?HwI+C?~K?xicP zv~ttAtNAh;6aEShqvLgkx8}D_Rh3#w-$-(}wHuzWlq}02A&(la2J%!W7+I zKN5>A6TFM>dWAI+DYeT%-}KEX*A)7%Ov=S(j%?^OjVx!fEvmkWzWqAY*A(j2?G%>N zHN`oos6X?VYw#vL(qz5YDsY+rJm%YM1UH)mebY9ORWs47bqD#GM6MPZbfkUSf#&d+ zU`1_HD_b+XwFK48-u8)xJW=B?&}2GnG*=Y&~RMXo8Y>^D($N zep1JSR+Im%KRT?C#pRKS_U5gjsDJePdU6N{Q?urs#numWN+=UL`DU zYH|RIKOJ=BJgacFyZ6s1lXaig9N($cbr0cZNywZw-|o7^6vT<>e&iQqAGiufBCgx6 zCgUNg$)~al)_4PEUUNhWyk}Ld)(}d223Kpuuba9{zAmJMrk57$&4ZHk!Iwc;ky(KL zOrJw}?M?%usk&OZah@?u>KS7A~7v z@_dnmCppu6aQ3Hw>@MxrhH`&P#p4H>(ncM?={Zo(NIs3T=J}K7DqN!jOqJHRgF2`f;?ynNdjP~dKjze{{G7=7kyvRE*>PI~$pB{X{dOy)_;w~`wr&{3|C*OVvg?TZXsQ|&?S`cohEy9NflkpT7jq|Wrcw=E+}z-iar z?MZ5(WHW_9vsfSQ!xeMeNz8N8#(h-5Vw5oJU9JDZmQm8;wSU2qbKxl|l7l#N^sZRb zdv6P8olhJ0#DK!MSJyz^Vd=@wacLN>@sci)2wYW)Zq;#j49%NlLf73mhjq1}G=62X4a@9&m#h zBP~q+|B9pX7^8g{#9IWaO-U*E5sg5o{1WYOscwDD(cM6({7IP&aG#iaxV0jDD zjM?i6&UNMexzCLoB-VU&UTPJal|LWM@+LL@081jZ73E^B4R>4$uD0d-)`>y7EN%wf zesQI(1T0|~r~u1Abe>+0?gm!K;5F%EknGo~97AeYGe;&Do zCDZ*5@A)W=Y`dZ|1ViXbZujZW6LJt8BfX~OwvFmuuF<{xu{(#!m;7bJN)EV1Tq`65 zbv)zd!$c2%!}~4oBqmLp_xoKGB~h$7!s97#)>0+GMwTH*mgM~KjGl%!ggxWL;w&;< z8tvcQvnH#L^PT!Ve{m)ebpJl7C+3cMdTi#GgBMk$!8w4sP--x%eGtR#r2b`9weTf~0`a+4-1XtpqvTws`N_ z&cUXT&CoXHvyuLNL`SA^Z7Y)NV-fQG^-}bs%cNx7>2z>#Fs)>}`sR5*+5T7g83ecY z$As!fcSiEtECt*SnNJv#J7Z2C);Am4z471c`vx3 znmA#UdIk3S-|=WTBCfe%BDNq^hf3F4&-3ZNp` zb>lC87laqt!edkeSM4O~U4f5y=$_|H#N$}Q+N@yn7oqx_#vZJ++& z;Nm$c&cwZK3!(u;3A%UDSV$N=Ge~zUCOIZ~=xef&9oC<_c0{XG#F&FoNr?n>6&a5S z-=DwRWXl6z3DO<40IK7*Dk+@E`z2oQJAmWGoQSN1`O+Yv!vS5>owX^t*T@7`GB2Z? zS7IUdd;R;{ul;gBI1iSNP1NHxjzZhlj<-RRK}b}^^TQ$6&jUH{*m)e zqH052P)`IY@Q-^7MbBj_hL!$F4UNbB$Wl-c-ZvK_XJmAXXZOz;o>7AC;5-z0pNIF% z2-ut(k+W$JxIeb-j|ysBxO99Vq%O-_$ z5Pj)IU3rhIlq5>gvW1POE}O?eGJiLd(ZmqX!L==D&o}d*Zyv0kZGmoC<{q!YVy9y> z-|WQFo?*}N($n{A@NVDn#uuD_Z>^g(ne>#?brkl#63jg#2xpjPVPudUzBT!4!S>$q zaTnXv>Z;AGoqyvr!7ZjY`WxL&ve-4z3|DpjX6Hb~Au~#MET!OdJpWrQ8t!J>)lqKg zo5fB+S)DY>f@YHGB}{@)@6k=KR%?!nbJ>@rX|+KU^nOdn{Yu9TF_V)g75-*}(0XgH z#8NNLABkwM)MnaR2OamcON=&BI(+g=y3Q8rHJxu2wR7cK6GCq$_R$pk&+dD*>72VA zYb0=Dmkm}c2L7vabnLxdZE2SQa@P z_8B}JhXhnGW#m8f1aUDeYIJxM$oZMea`B)zG)PMx_=f<}K_o9YrFQ@8`oaDFj;KDob8_9P5(|{eOLZ{h zH>m{_j6RPu3EdH`LOxIZU{|ANUE6EUwN=LNxvxxe*Y!EGgWk_BA;_xcsnF`3{Er&P zhe2%_&dVW6noz0wiv{4}V4J3VTS$mw0P#(b&X%mK8>)?TN{r!Q7VLq+U)HZCNk02B zSS$Dpll~a;oi8?80bjQqk_ozduIeiNJPwte41>kpW>Ni$BmA^mI^;Y*30E?~dmIm0 zbJJ8Q4jEbbg==nEJKuz^^lZPmF0>sv(4!nbS+e|dNpDJ$=)a6L%#+m&(kxVO&r)zU zmF&RbrGvODs-r_2e;JP}=`v~D{7CkXU_i?qeu7ClPHM9i-xrOW)RHEyn8f*0Ju6KuJTd~|K45jOKT0;%^M5-xd~PhqF>Y~k{D2GR!B;O!Pl@8g}N?#i3<$U04y-OSX$Aj>jq(V+8Bj@H*Y7MCR?($8UK&dx>LW(TH* z6Pne3^qvK1hw=k=pHA@F{fM>8(|63Dfwf5)uI=v4M3b(aeCyoS5%2e?w|_=YHxPUd z&CEVdtv)rBT1q-w{;gP2P7HA@edAb~H?giQ?8r1HYZ|LF(#<})5?g_geCaP1^4-Se zY+^h9?ZD`KKclIc|A<7^+j#l^*n7{oCetNsT+yI{fU6)1A~r<2f`EX4E`p5?p#(w? zNDDpG1OY__0g)yhq$l(eN`O$LSAoz1gdRFb3oY1XWrSS-?4r)efr27cFPnKK=mU?*pmTGz% zz??KCaPg#ckIR|Ww(D@iy2l^Z8d2W0fMLcK4r^vVG>|&z_7j1#*h&3JXCOtVr5}48 zpx9*1tAm{4%sU-C=LDMGJmzDaSJ8av(z|tr9XUj6cf@~*jYG?SgzI6tB^{;LpKehSZixEG+ zLensDEmN2c$u9YjyH_PKVmIx^c!3gG=`laa|0p~xYfko^p~%0~vk-@yt&>xv9xl{g zKsD#nL@)N+trZ2#i1u#p#cy5)7o19>*_WmexxZwa#y_xxn1w>A(i}sfG}V`<1CsZ( z>S1dm_KKcI_tw!hO+nnwAH5ETSk}kNxWwHYcWbJaW~>?W4X;WxuE=sXaTn%$rqnGd zv4MjWW6F%6)5xXR6egR5{Bxd_uqhTbS-kgR^H)9SCkujqPG}d%-KnQmARDz6poWQ? z^mUlnmeM{LrCL2LITY039l40}FsNo=i(kyKq$HN+dA|2cjNO0~=v(5B>Q>Y)fNf;d za9hQlkL`I8sN0>kHi>U{0R|ZE>KHg@8Gm&O#m*$^>iKxJwCX%3(LCnOl(afy79&&)R2j@9eM}E)V$VfJs~mv=R4v{@3yQ9l93jA(+RDt zxgygUO|i3NmTB;ie#M!A>iGwG`x^rNBnJ7#DLA?$=&N#cqBOU-$kWRrA7bF^k}fu^ zm%oa4v81i2-&|dP>+oIU9FmBqpbl*lv>EW~kGOT|Pf2_hh?;k2t4N>Fu=1EREUbaL zm01*0yYF(bEDxM3nOSi)8s3bq&F8}_W803Y9mC#LlGA3%?mm~vqhUeYlr$b3gO#_jr{Y9w(3UE6k#k0i>dLM#-5#GE>lSD>JYW9;H>nKLG%kYA>`Q$Uz@GzP} zTUXj&$?Y8+zY+@MoM+|Bn545bk86R>&17CPsoYQxA26>VMYN#oL`Q~7l~#OC5}-=e zlUC8#)#=W}+=(YjKINgZAq?i2xrzjZEG!0};TQjGD#T%h$FZ7_rTe`)@rtaJAYV8~ z>P%D)(Wdnzo%oUSL)iHa2)6cO1^1MIPDu;Ou>@TQ>J_&j(^k|iE>`*1r42*MGiDpC z1%B0i9)s^^V9H)jc%jpc)sD+JBj%(+qAyi&2>397Ru`;%rWurbW@A8Mwgj$LA5&rM z|6{mXz=~Gx3X6?KY_WY1drY+WOT8gOrO73~{+8)-u=iWbMRD9mUhz#K?i94HY6@@v zb84wwxSmtNU7{)UXKm$L=%b01fD2n}BG8go)%(U@f-Z~moHq;5O?|{UkljHA23)GG zb$W6>2j+SNM(L=u?dP?my?_bUF7*U8q$GKf-{7L^*DaF_ zOfG>*_Ha5uU0ibge%&BUn7V7kdW+i45CPubB|C&d?8Ef^KXi>PO!>Z-ci=G3R$8)RZkw`)rJa_g!kUIm1NF3`LH2BYBmb5YK3j=pTV z>{bY<)uOqq#fijR#@=`JWSsZi0MxIRJ;5gCT8P>xO+D(#l5F0{msKHVMD?od1IYYf zG1^N@2P-*%lk=^BRtJg+ewnd;E1=79#5u~loUJr7a0YL_FygTVx~ zY%F~_F?kBj+zy$xIjxm;vt9D{td}YtJ}0Uz^nwMBPCWP|A# zR*4m&uCGFw`erEm$<_7vn|j!9_~S}gCQ!UKGbsWz`tGZ~Oz-wCh|^0fIoR^$99uUT zx+|TtoY5;*e(kZOty89d&lByt)PwzZW!D#eh@^*8QzBoXB#9*Gz!^{NF7mvqsN9f| zt{y!qp^nJ=UVCTcqFL3GHf7!7&M}Q7D{pP?9_cM10|i3E!!R_@L(%6+oE z0HyZ7!b~4u;KZ13^_9N!%?U7DLLbh8k`(#5(DtC?+m`qkB2hFqMP-+;>YW#3QZuInz+WfK1O7 zmR}exqP_`MHN#SOwTcvGdw1*i3tOdGgz@~R@j_hlB?`^3-GDeJ?N8L+ySb-0-6abH z{AaTiG-InEptt=Tvs%e(0W7?OI+SlpqOdbu z?1GUWkat>T`g7VkA_Gd}*S4Ue<@@QYR~VKxU*E;2XDgBFkUN&qUIOUVR>c)lyVZP^g}`$Il5PLj5$giD zFMpvK;;nRP%bs9!C7PveC>qFIMI#~K7&NnlSVlR&dKj>>rdmYG|134r396>nqR(8& zP}rW^F=Y(#cvG2}%qBU(e??Y$$XZETrBR&0xYu;}rHO`gL}xA{9L1~}D`~6s36^s9 z1OtQKf)#7ltX4W?XWo!rKPb|ymY0yVo)bxLV^;B&0$ZsB;Vklbo&DHzOT*9vXV%Qk zoOn!hDtB>1+i6I-Rf!>g3NXL5YOR&{I8ip}iYZ<7?%CYKQ(G-_*Z^G7Q1Y3d1Mi%mBSWU;^s7-v>I%$N)TE#WLeu%QW4>A)zTzI$d`A4mZf^P>Au~z1{e_o z+LgCCv`0w2q4B)qv5hXg#0PC>3L~BB$2`ip7g$m|oJ+FA;O!m4UgPTtwHgN|`#RNP zt~+{5mD2M?l}=ZuOvFSrh4}l80x|n6SCxm#v}*BuKw{mL!ZX_Cp`L>JTw)k@wo4g% z^*?D5U%F|-x#T2i|461&Pc}7c+-&=^%R|}c%s>(Ki|GE33oV&hZtDl2a`E(unN0&w zy7!fJ4b@u`2!o2mqVvqY+P6C%M3t|Pol4Y2AjXo9EfY(yetX*iHi+TP5hDdNlj~#7 zM2pqtVNUOD7ga?m;p>~`EuUw^;Jxpquj%GMR$s;po9K-}rJ7k}%GeTTW`=2ZLZ zTS!Wa!0gfY&KQGGker=A!;wVu$G%j?WlHN{{V(UCe)3z;%aW-yRbW+(qfeh&A}u{( zX#d2`OJmv$6V!gf^Ua)>Iw$i?Ip`gTNkp|fA!A8!H?e!2R z+v#?2=+}u}RQQ^x!kHCXDpn&^2gbHxrX=OK^;KVWj|KK@Wu1#&CsbX9r zi5DJR=&CehA7QYv>FrqZ=0r9ccx`d)Z;;?j{-g#$jZa;NmPTw>Q;*5pcz%Z!Jly-X zu|3b~x4cZDZRixE2B!L2SHmQqrE>S|Cc`R1J;_xn2kAHRhF6~^xCyQUB|_<&$Ag-` zovr*Z8U-Y#cvbQwkB&TlbX==f)HUB_dE3?dnC?QIoUBEATznnd_rSAP?2XLo=!Nhs zrH?9n!+?$^#$dU(I3#@{i3NVg%tqtUI8bu=7#~;A)p}_t<7HElEVSrw-mP{0MVh%r zBO>*^q{sH|z|G@nYqt};_R^mH;tpc3ulL{Ip8rL6^AzhhMvMEqmnF4jZYuTCZg*AD z9teZnm1fxZTUT3sm#oW_2P#ulbOoN}Nwi&jOLY>evh=XBu<=}J5WgYUCVEohz`3=~ zONFoO;asns@3^ztsR7fFEIj+3cG?R#6~A~SK5Q-&a(FZb)Ws1EQ?f&+NS!5~hU|9Q zWjB_HSn|mlDX|Y%=>WS}IU!M;!_S&oaga4~&2DL%QrtzA>USE-WV!JUIlwn%qu8u? zM!`@;$6Ux;>}KIde=wq*TZE_3eoo1_%Qlm5?8eVf9e>&91_$jRVf2poA;I-{Kd^}X zdpO@tsHCfNZK%2!vhfQ0!RV)?NM*?w-fPlM+xoo>88jl|W6l{I-tETyXqZzJA>1}B zU9t4KAg+$dvq$xDB3D@FcCm6)6_F@p%1w(z&uRi?amBZ#O!J8%sa61D=0?a1&Vx}^ z;zEG%qMN9VLm`oqz_spT&&=QK+;L4hda291vJPHOm6cGraWH#IZ^r)%2T+{S%2Jot zD=>-RtDwrKA|kEWfa;dRyyGpX%D!U?G0J;+gq3AK;huHy)+qiR*tLGKQRRzGy~4qj zGrX@1g(YtFtm>!uZ}|bb$ zb~sY7)ShZoP=B3Pu0Bhg21_SpIp62hZs}uN-lGWs`Cbr#(JCKT+dO2<2IyZ|NgSsD;h=>o{-b2Mh1z){!12C?XG?Cz5j1MpeG*)R$bFPmUJUaQ)V7c!N|l zoI&5Y3kAju&3by2?RV~tPsJ&!fihRir*31wB9f<>OA2mrDk$k#LG$(=w4A?qGQ)q= z(=A_-OZ(u!dw|1;JVr59b+sDvbwSh#_~qD96n?7^$1cbm30t~rdPi{#jPZu^aVT1Y z^In|uQj7dFTuwPKs;Usn7Zk7>{w%61<~8X^;fP>qpJJjPNvPUBCe$^cNr)({N zvB8}-*?jj`1ZKb)2rkc7h8)EVmTe%SiuJfNDaLa5Ws&75jf~E{4uJvyyz(!k&C)!L zJX`iK#)V$vxbx^irgE>gpZ8f*-CXOF@7Pr@E!SXeV-D4qIIQ#zQbESG3oJ$9X z;6fB})L_5DXei{u@Tj|%ErI2+B!&mzNtIlpJ0Ywls`TeGTN(8xE~IbhqxiK`1$B!*`-e?v7LS6 z$LDspN@d>T=6}n(b)i@RMQZ7pGwPT9R=nfJc4=brqV1bLuaZH%5H?=fmYUW%b#` zvS@Fo0sK)6nU*(#%A<|d=M4nu%6eC<6h4FI4Muo4uUdKIe#A8QUNR+HtQW!UkLKtC zo7!$@oRc52JaQa(l%x9nKoM?;`V=xt1L(O^loa4-65a3}3w~GlEqv z>O3yD&Z}%@l)W#GX{DrDwoZR9XcPlw9d45`qb1$K2(3mLYtnw1jTnh>t|X7=Jr)Cb zh<~!L-cLCDvM)x=(nkD(#UH`Jn(G`NZ zm75Qx*4GXlDBPo~mIQ-MIV6h_|Eptpm3$u!2K#$iw7ggRWw3_6!_Nd_!KG4yV_hqf zDVPWc-S!E@0V-%YT6X(PpXW z51;U(+jcr?(y-}S0;kLT3CTep?&uQU({)OyX3SD#PkWwg<&5558wwZzo$Z*m>8tre z34v(ow_9oDMiDzCAoF~@B5zRPM zAZ48f?805MyyF6K_Xor@0siVk)_hQ_Txe9AlL54V>Gyk*TQ=V zt|~7#1oRI^Ri;>c6wlmI-C8b{yV)?W(E)Gm+nzsl{G|GYOmJEp0sYzaw2PH>5%V9O z=pleT4rq$bxu$AE!-+b0iOpe*?U=5pu#je%bPpGWYqFUejeO3qUi#ficFV${fQG6-xMTDANBxUax)1 zJqGIUjWCt|DZ!toY2YP%KGKJGx`jBu{|W5)({-;O#Q@^zd1EU2PZze+0~pUr9_c^J z;^$3Lc^>8SDm~DW`_q$to?V(N0ibw$Y|qXAddGi!5NXBPiY zn}w=a$JRR3c5+jGSFrvcyyh_)E^VV#Y)~MQ+?>sea1{F|Xa4C~QeR#!S4@lZKREN_ zCj&HIx1%Tf%=P}fas13hJO}!IxMqxR|HHE%IRH?9US(VC+n>JwPulxQ^R*FJ{i^vt zZ4t~Fz&7x|2mVi_f6|wK>D&L5=6`1KKTq>FH~fEL7J_xQ)IAKqs-!(6z}bmC6DW3Z zB65!HXW;#xKE^weKI-@iEzMEydzK~A+cO7A`@fCb)zfypM=5+cydf7gxPLPWM1=R| ze;)PrAesz?SR2NhZ$#QLVqLsKJP>Lm1^L3!kjC76J;Ug$^ZOV*4@gHV>}F!G`$o%7 z#CD%Jy&u>bEMVOQ9pQi|cv|2psh|31@Avj6_kOH9Ny+WZyadUDJIXYGv)i0@s7qsx~jn!(=Z@MR~q+7^i)moeuo2`vmQ0t}JGp=0_ z_@zjGdYmOZWaeiD_m3xDV>tfnIm@JoZ{W_=-fp)0naoR&uFevAj!fPUP0ccYU}Jtt zyvEKj>6r)yweQ76KE0^TXXgi>GZ%EXjbbR_mtb2u^EZ#ZluLuVrIW_1YZqc*K16hE zg>m$j@F7?bni*G7zNaIylaQ zvFVH3>c>8YEt0C!MYD9TS3aww#Ew)Xs_0vaL|F`Bm+=r4*Rxf61Nmr{^PHYG7H$1@ zC3*TmcS(~R?`D-Cl`Z~V5Nozc?JRvuYu(bX9ObSY7_EYjhNAO}0SrUiQy)deY8h5- zNcn{fOR8;F+B{Lt@k4y##5W-J5v-H{8?ij-_dRTuWruaf?1hzwUY$|79oPy&tL{60 z@Y1et(<=>)B%uhgp{*@KDi|x_ z`SepcB1y`hg}rom;5vm=zL*h*Lz(3LxT%lTOLK&cY*n@Tcj;R|W5J^FDfWEL68bNNoLh4le_sr|${pUSIfZk?@D+E0+pUq)U>^&`8f2h@*Z6Y$AuZ>K(C z+|hD0*7WJhQmL)lTC!eK%=YvyhyJ$9b9hV#+e`zd;Z zX{SWD!zQZ_ADHHhuHjp-NQ+Nu^rKU#VJzAlm=*7OF7-@aSu24Sx6qI!hD0dm-+8!t zoRaK1FudIzy}W+%RDIi961Nn2MqhOU&+6j*xOz?!Ds)!Y#xS^NmBTAY5lNu;0MR}zia6{I^R>aOZTePsA%-9pvR7qQ)2f;8 zcrHwpZx5@yynx7o9qzQJKFjB-O!8IJuDUP!kf(W0Vxrn`k`{g7d$Td8(Apq+R`xbB zepR^kbG#GibFRCU$tQ`(%#>Yv@h2ekc>&ghTzEMw z^1XH>HOhEoR>LS9zHNRqTLpcYAD9H z_5Dz)jo3fcvBeoUy+tp6ut-&n3=W>y^if*!*U%%%a9Gh2&sr$Hbv5y2m)RO1r$4z( z$OezWB{FiP5e;LA$=aYBy%}ZKO^@7!#N}=aryR2r^frDmorQwr)Ef|%8+*44ofz$` zB6!K>p*$DJI7{pE+u5dSh#pR5IE>6W#NS-~gFXatQRV1UtgVP+XGNxw2?_49G>?%Y z$_eT{B~oms-+ zd1&Z;#0&Vb7joMM)i$fmyxc2#UCLGDsAlrOJ0XSzN%`Av#(?ZHkw7(LmS^dk;0VxKYOt_Yxqw(^vm>U&xt7qm-TBc8t)_Qxg1#EQWK z`BQ3IMc>_LkV)*$$z5LD!?nQ*vUu*K4nr-r;OD%8sZ*~gecxh3tfPCnUg(-x#j~iJLYSiTYE^GJk_cq#e`ThVyE30(R<-yRvtom)gKYb zF1YXx&O7A*?cX+fwbYfn9mTYh)rEoPYlznLNs4D#!A9Q}w8Kn95#(Z*(Mp$5U(oJ} zKjMbpfnZ)IUp&dTI?8pq;ls9^7jU0%s%anvQp_uEwAQ=i5-QgtY=Mhiw8s@2hK;At zJ;(4|Q`NL}%skY9X-`XHtGk{HehIy!BYIo2>3LIL-2xrm4x*>p8OpHK53yVY#&&&)m7IFn@R)p-j7mPP#i@YWe3F?9)ysS==)0Y4H3}PWV_vx-c$E~ z>o(X0Bzu5Od{w2)~Wv@sp(n)u?y6LADZNv~eviSZBVQLdNy z0X80ygY{@H@Lijs!jWt8Oq5OhAlCa;nVwq$qs8}5?kUR(8pyj_%{A+b0tl7r#kdRa zHp0|B7*0``vn^+QH41Wy_`Xu3XHH_mOVYVH8$Bh*k5Rr=4fc1j_)uzj8Q3?XZJH{M zjT|BGa|^#w)`g4X!!;7oXis0#;YwWMd|~3Y1Nkogq6}FHYR|PDXH7*Yp$FIEbycV2 zj(6CT!215sN^V;Js5AH@uG`(@qW5)en-2BTB&F**ghuIPVJvKYZuAbNZ_5cPVqkc+Jg!OIiI}2zK+P>o+ zzK|vYt(X3Kx?$x*=pHrcuOaxS;F{b(fR3dU9rE_je>w6;|3_*$+ zE)LNzWj`AHl8rWwXjSLCa6uxvN|+D5h;X2>gFLzjM)b36@O?ZDtFVONRM+HOK2{&@ zqr?!0wEK?hyuIB`^YAapvT%W;I*W~*Qq1=>N$3ZNbySB4++sAxJ=b5oQp`ek<=%bA zA?@f_Z2h}4?_rO%=>*Y{!>C*ttule(%W?E7FSn5CogMu-<@Hag*eHJ*TNI>nKvWkX z+xZYE_5`FMCDzICis1108R22C#`#l0dX};{er$_V|5F09Mo{rn=(+W-r}sljpQry# zW|~P)%(S>h>%T-xL&&tE=D6|NVbyqU0rYt752oOtgLXRD#Rg{+fLvGw!~T^3(pwl94h(AW66^@jCK zfbG%z+U(;`rgO_x%j;8md-A>mUEH|=dsp_8pDvCZ`~EpC)<#M3n0+80TE-TxJj$O# z6nH+)hg~&G1eZyVR3na;4sIWI8fh(UYRa(Aj%dtai@mbA zCi2jBt#8ElAg0=G4nnYCxBwQL24Z0>ch{;B=?Ie{JxQa}DzmYS`%pd*p4iCu*MKPN z^c%R4hOXCuP;W2D-b(}9mPqwLamlYH;-LKbE{cumIQ{!|=B@OS9m$kxIaE|Alx$W;vFKs>(waTP7 zfx_JaR;_KKhZ9;W=r18?sOP{W8SP8`sRN(_4^v>Ck26ga1SZ z=%RD%TM6-r3_2^^(V2KoRJ+p+UKjv9x1diGk zy>a{^q2==0s#6hM3_>PTgdjKH=%{ne6YYl?Ejipy}2eGoWAm?Cj`;S|Tnr;2KYh9Fkmm%EC zdR=)JDJ3|f0k`(J=Vx}StaM{@y73Y_-463Iqd+?3e*ZUy3(}Pu97hGJd29miTOV?k zzEdntQRh6$=HF%^G?G0!pyhV~u&8RjhO}}*LFaE_Sa>dRM7r7Cbv0IG`R(e(=)#iO za_*zIdHctu7vzPP8R6P(V^oAg{^v*uD&BN?9JvtV8zfssl7n(qbF9+k^!_ae^VF!c zEnEHAL>9dytqRx1scfUSG?ykGftMv&mVz}X>w&ktz~0_cB`yiOhyKW;+>#vinYm-P*Q(yjA9t{U~dA*A#3aZZ1CCSX0* z;{FU6B*m6Z9}K20VJz|%OggNax}Nl+`m(_?r_B^yYm3(^7IyYD zAi$WQV>4{5n>`Y1N^N-QLD+2uLpGMa2P4wI+%=d$;1dj7f)iXiJNyi2SbO0*)$uK|s)leu3TbrEbhmYB@C5rss|)i9>@Y2kVGJt4c7IXqyG&DVchX~J zAFp^$X~OrsyctN@v9-|*?1${00noL5L>t3N-UnOJZR0)e42Dc}mu6XCe{^nT|LY;s z;cNB{?qq~LE(XyiVVeWn7`bjAGuq9vi`wvqYe^R~&4j7Kyy!Od8C2Euy$eLF})LShY{$$H)d_dlZ1Jh4^ur0lW86ms&_Btk8Le8xWXB2^hEH4jrhcs^74)Fs->m+ zScP5m>=!HFcorua6YqBi%1{R#4cvI9xf=4$1I$M>8AWPgnWDU$tvno{%IG8X+cHTk z_%|qv&IiRGswnS}isVH9pP|q1Wr0VMG-)Uofq0get&)0B+TH^~tZhVFuGNfi{DQx) z-276Cl@biLny&z#gFcS!k1c=L2BQP>+IO4PM{@RfsL9f(p z(Nd`s`qd28IoczyeFADQr>hb!YL9J^MwYFEUV!NNZ5&lVaTR7MC0T|ND`E#SJK6*{ zNCo_*>MGhJt+2m8hn4;0kGAx6w3Cbqd4$0~lqP5`(SbaY8Slir4Be8Kq zPE9CM{3jPc)D!{xAzz+$r)p#D*>%X50ODRx>W>}x1v*TzBGyXz9+S5 z#ALd4p|?7XNaqBnv({@}MiBQT{pas^!VTX_gdzYPcLoPqNT~24YEB0 zPX$Z%Nii&<8lrxiebSJLljsWHDH+wY1kKdLoa-;MahTEKK50?A*7g!L(S+b3Sv!WT zy5-~6H~(7%*Ye3{?!qm^Wyx%xS&49!NFuf2gV4 zKFfMj)+lipQ9mKDj;Le)s}Y~O^XXVL7&OW2PH+7zAwg0n#Y19o+JAP`-ptARbMgd& zYxPy2cZg)gue+FN)nkONipM`ef9|{iYQDdY7x4Zy^a5`5$Q&p#!}`O<{<=y1_F%f} zkMjS&KhdW!dRee!3`O12ZX2%b5VPyV<`A*OU=DNq0@Hs(q5iErk3!Fc^FoIKCP>&! zYh*zHfN4t$faPccD0Xa>v!vh*%~O1Ok%{Eu(%=tF4k^E(-)_W&EAV=JiZRFESHOsJ zQB^e)5V^Nnp zL;k*8w=SyQ_>Q%e;+OKo*>;SX>y^TpE^0M`bJ!w$6*1qeS-av)&S>T6trlj9*)RrY zI!E_t6?rsVdj@{^u377!1UGrc%wjcPbg|H;X*+IRk$?f8EzF)2(9`b?c&Jmt!$;Ht zxOtHILBq+$UoWzX5r1dX`}4xThB4aOQKl}7`lTdW;AP z+RsMyot5Q|oV)M;UA)J9KJz4fw-1=eY~M>)lF-${nWzNv>!kTYbz_VCOx1&U|8a?1 zSXBMN4BAC6lufnh{Pa)ZPn6e*jMZyarr)NtJ<9;Q~xN_Wb!os2NcEwse^=X2xsP@oVSZpF0NzIRK=Yb?!Fr=9OoY}jaKuWv@joE$ zk_JIu{7p@W0MAnaz*jU~Mc+G_U#7xC{0D>SVO=Y@!byCN&)Dqe2i~8z2?MYVlV-eT zRsK;9T*_2~|Ynu~cx#nR^tqcr@P{ zX+ZX;{5g1jc1zXK54>39i_F!=d3;4$Nd7LbB?X$pqxnT(Vnfh&4D>(g-aBxg^X%EJ z!=E)W(7(PtFRfls*2Vh!iW`-(+%c3PAI$??PDI-fAC9MgfF|#F{C1go5PeBc+6$H{ z8oi(0sW({fO!=Qg+fKDq%fmZpe_FoJsBY3XS`5NMbDQIL`wP%YQX%=IbNvJMuQz7x|Gqdi8=XdB~X=T&0!@Zdp3Xm4r-#21v;( z8znXZIoTHXo#k=9mAItt(%)RVXGIM_UnPc^bdD89CIGa!(t=I?LAvRQESUb`c8S1j zukynFTqO~pKM<=`?Nwg3c(-8sG_WB_kbR~9q%pszVNcSurp7#U2@Z;WP|uvt`-~V< z)p1)f!M4|2FdrUI)M-KpK+{M;Xk^C%Nkp$mGqJyEH1pd> zSb8z!y&dlT`KT5Uqvl)6)5p1q3MyDT?<=+oVOqkV?f32IE}8xItDZ{*<7Y?Qrh*XT zKBv;1#rpFM?SM}btS?IcTYw2RcD`g5Vg z1Ym&Ac-7%xv1M4iL*wPpuH7nN=xhjeE6=;5XfWxq-_k~xZ0TzCey4d?g6+F2j_*0_ zYmOrhcgbW=<#&u6R*9&+ta+E|=Ft8UYi*+U5p}s+j;Z8k$V1^S?EULrS0@GYoxWC+ zH<7G)wnK#t37#two{Kr9Hg#{#Xq8xjrRI|S6r0>u%E=W!s4P=C7Zm~O-&pCLalh?) zCfSpTr_Nq78Zh)mj?9$Wk>GjwLl1ke`gcsFz)R{K9a8<4ml`ZZ9vwf9I zT*us-P|!=w2swJ`_mlvjoHwy0)XP`s*R!7 zkGC!J){5Ah*yyBaN-QKv4c!sBDD+Hl0 zFGd9m-&9@D9uV#YI}6dX<}uv9pJ^!Ip%9Rm-U&I+U+zk5`=6b1q2Yrh8`@clgSXr z3Uv;Z-d`%hIeUM$)sOx}m-NODME4+1uT-!wW^5o+?dG}!Ui|45hc`TH%dzU8jsRU5 z0-m+PEng*OJKR@&u>NwfJ6W#J`RFlzf55;q=jaQ0OUQdSaMYoRLn~b|I@6u*JkR@3 zLyfx!VJb2$MPGbZPH}qWE5(WlbUjp>BbMe>0h;{S=nldMSlO6P>fHB~n?=lPp35Zz zHxj9_1dMiUt^;W5dlDj2XF z^tw2c0g9*ehU#*1g6DbKtEN$-95O0#)prIvwsU}EzAH8C*gMAE3RPB zY7ZE`H9Wi(O|SVjq)efN7qMxK^z~hun6DJqtKcWE7Adi%p-AC8rQeSDo8mV_M zFT?}$D%S(zojN7nGHNb2GIOB@WDj>t_bK=zjwOrLu__U^?3f1*UwQ145JcNj7{3mS zb2H~c#f?QI=cG_Uh6GlmOZK0PAJjiDPx|s zl)^}c(xoMv#qTLk`?_sGShQxAi4gw4F+a02sBq93g;YqXUiI8wy1c^MTi-L)QfO>L z-u{GM0G%Aah2O}n__j>mua6?OHIUxa`a4oqL7d$+q;A7## z&ZaNqfM8J|>AI#xg1h-GRr?lh}ZSJj-e7wt$ui#;|= z7Ub0@>9WAQgz-QYx zHh<7;HPslT_Bh`@Jx;iBQew}NO>92a{~Thmzr*{Z2Y6h+o=?LvR@^T5B-&ZA0u=$v zGEES-GqCrn=Hz^}kx0LeJ}X8wm^-q_5);5od?tBqD4{X=e{|vj0~F7MSqvAlU_{33 zY~Hm+<{=seEE<((Hw2jnO+_%y^%=1lJ}zY6m> zJ7w4~EL#AdWt)iHE>ajwC`9cJ~hES8+GC+bJ>k zD<<*`syyx*dcV>gS_E%^6EoD)UWD#~+*4$Vk+xmpuv>HHVBPD(iGx^xxOv=beuU3S ze&}xNS|%8y)%UefL;A+(!CpE<_)tN!K3j8P4scv}OIOWDSGE*LtLIE0x)a8)rPSMtkhHikN{0;T9{ zT>5;?`*VgRE>7vOe)hdpBIw!VkPjP-h^#KDaFPt>pG;p!F|K!ExI|WU-yA`^c*f;(OEb@GF z<^3|R%HrU&79BAW7E$4$Uu~E{NOfDf!}%CW{b1c)6539$2>u#16LoK`=}i~H(lhZof;K4&SH-E zn+$n<_RR7a4k#IZQe#!D-*4+RKp zZ|E-Vt>z5Yz{*zj;N@H<*~v>?biXof`(E4|EBb`HJ@gb@eKu-|mghiY$9UbJr+l zWnK13t$7y`y8ZxUC6%322#GZT0FsCIDjr~?aPx1S@TC+&9Xpv^Dk?3thOzoI9(HRfW5g(lKtm!AMAs*Gz-AJE_M3cIw@gl8hz zFFr(fBOa)-Fh*vQ+5~a!c`@GyoI6YWnJslxcRZL@^L&44wduJ)P`7N|d2p~+pF#tk zI1F$3N)iHP)G77W`JnBM6e5qh#*j77)l!jHga(g2yKhu;;W0DIFBfhko%S{9*C0tr zJ3RrN1Of5Jcy!TM6Gn2h)1$lbXViWov; zZ%^|y&IAugQ>L+%L#*&8X3Z@MhpV2qG-zbRLR8{GGXG6-de`9XSer>ZHs$>xExPIQ z^sa1r-v!D$VZVd)Q&M+6Wu_~pPbYm|eA$#%6G7*+m`vkQnAz62zVFm~h?>s57Ajo1 ziBmW(E`1Mt90JOZI72=a%{$#J3D;a{Xqg92KSrWd8=z2lX2{eA?FKx-sF8*`>a)W$8Iq44z3sE%Dfb`fgG@? zm5|@cvf-ul$x{!v@th99QtliaiG^uBpye}~T!BBX{(izmzkKAA^5cRx+L^*34!1rm zSq(N1R1$xN%Z;>*XI$pf=U`B+G2S}deQDV|=7XXEK*#)_dn{1~58J=5flxNvlCDtttr_JgqB z{cu=koJ6gv6vGJz@uKKG)lUlI0~OcR{ttWa8P?R+t$`{VWQ!syO+dg-6Ht0DvIV6h zgx*UKLhn5S3Q`1>PUt0+0HJq+h=6o4K}yFdl>!x5S3ej2$Fz#Q|$0kaOa7NaN(Gz6s+%Q;N>+n zLC1;ByJRdhceL&W;vlb69HNZS&WjIsM};OcRw59}0vI>rQB8UPi2?_#;fA;lJ( z+-|Cz0>w9#8N-*QSnK1RrC2GZfQZcQ(9Yz+dc#K0$sqE=gzb}DE-mD4We?^4GpDC= z%5hEk8}Cjf8v$pn(nkX(J;x;~GGnb!vOJ^`W|U@A5npwZllpiOEdJQ-XM0g73E?99LpI z$J=$s6*b6{l0Hzgn#ZUWJ-XPS#;NQy^-pWugLgcpk25&Qm4$v(5(zqg%(v?a>nW}L zdQvlGZh_8mz725O-}HEl=YapRNdu-9kAe0Cx%INvd73d#kiMRtP3_^VnuPr+i_cTl zFoqIp<`}{Opq5;x_wMw@bsj_6d+}T)H`ugBZSqHu<%Sb)UN*FR*`yPsj4LQw!cFw| zJ#+dfNn%u$;Fg`m)GG4I2E_l^y>M`E!nJ=I+(mZ5EjVw+YF(F0oCkV{K%y8?L!yP+ z5y=k2ZnJ>3Ig&BlT(517Lx^PNvDGuO71HMr9@GbVsGQ~8kL4pv7NF6P8yTdT?zUpj zSP@4fOj2}5=}su~f+8pk>twU@AYdmouOH~eWYu!jDaiXxSn|bT=4Ays3Q*-l^6_nlL35w?r1wGmA z*oYD&i3f%5B0fJ8RKn5vxa#%b4)e3=zmo2Cl{(AH);MIENFUC{Z2CMVW1~@b;uc-$ z?8P8V{^+-otDEm?)OY}@1_b8vzma(AE683a8f#Q1V`*x)D)uXtM~xWYuULC?-d1Vt z+7WLP6rNDthyyJV1~!-|bzJoleY(<6tS1{!F$yKfdR)M87C{4-6h6pC4tozowOBdx zal(hsKNp)Q^q7?NC!tyi4l9j@{`YI5_(ZEV%FFqLxz-S;q@VD8;=Fta+mTYU8EIqo z2k;k$JSwfZ?sfs_-2qQUwfSxxR|eC19s+8KS0-6|IL;s*=|4dA18RC3@j)#H`lf1> z-b7UZ#@k&uHUd_Si;qtf#uD$0RAZ_Q5-EobjE60idq|eYJXe~F86c+`T)WFvO{1sj zM@b4S#ra!=E`;L<^hK(5_fSguZd22@v(_ojC~wcfZRJ7$3K@~|ZcrL5(aHCA=c!N> zw&`qPJq_vWZ-45UyF15&*{RH#dl4NHy2MM26SQKelZi9XXC-o$UVgvtan{(0=;*6Zmlr;w z;AkA7-styLkTy)GJ&o@Uzo0+VJ;j#7kbrU!_fJ!k+#PgcmhMIuYsTpivtN~C4dsnM zgzXxTy5cFT`We-9ZxRsd68e9kYV}DH^hpZs%4MnJ9SFa}$LN`Q3e~kiS4ai^lF?NX zkVybO^$=@Kdt3Hn99sF)q-Sz(@MtjkzA!OL?g{dH!%pC*I*JVvF1uXauwAEZ?A4)W%PnEMCweM8 zD;-!^R0RZ6(sFcRl8v;W2M+9IQmp~g1IVzPF$GS-dI0&7cxEWi!Km8kIeT}%rR22N`8{G*cnkP-B&=h-%=iimA$CMX; zrDdQJs)il}DpB6vp{$CE8fSfmnDGT2royM2`ji+FyV~6_&ypg19_4ihxOV6aVdn4! zwotpj<9TyRrt1@jWeq;QJ9$E;yK=PwuwlqpPyW1G(}~Y96>f4rA$s{w9cBJ^y(qNX zl^&_%&ETF+Ir311-|rvPlt6;N&piC_Fx;a7_G2&vPiftfL6aPDXAtL5m0=)W-|O>*_!+@ z8P}xfQMaK}H+T0=;ft*D7c_}q)fp$%7mnb3=o8nvVT!Bl2sMVA6n%^5Mg{3>-8Z;= z-^9FG+@A@r=E}&;6D0^A>8n*CwbB&=dC6`c&p-mCUnTAZQzu7trEE*HIR}FR&gGWJ z8_LI(BVq?~AS3neE7MPtDX!I-B@(XN{HZ$u&p=48dh9IDFMSNSp(lHVc2Ct|{ppKj zB)QBSqRMWR@7g*hS1Dn-Ai0n>b(2*uXK$udY?`cuXsYb*TDB)V>yRW_Ea zXlATLy*VhF5E|kqNLEMtS@W{$C@6XF5A5L>74h#7QQecV<_BYmNRZ|Hj0rpXn4sVkl-X?5h=v$9fF2S*7e+0Yl8msu{ zBkKp34NEy$=`sD_gPG9zD~8P*kF0nOkMiyGg+028)a`H3zSrTVk-nUnsn_N9dOV!r zMVPO$^RR2x+scwOkNA>9vZ**5ni@)0DT&Mv^(k?*nf@G_ye6;S@iVIlVe}WgKV9k8 zR1mzV&2Y2p+8|{aLORX{dcr5&pdNT_a?YBtwL-Oo&o_-EIai2%oPkp=OT^YOzX&3wS(zEjkP89lw zfousg@r_V2XG10W+MN|~j?ej#il-eL7BKtj^YK>rBbl>C(EJkVxnX7l9wd)?EzZlh1N7*sHVkRn`azXfa=l5tMFd5?|b%3te8l-SVLXuoqOyQ6h| ztiMqm7YJF`*sUK`;t@cSG&)nXH=qjfg5FQwIeah*Npi5u4mnur^k?MEsa~rdmj@3% z*K@%pFOXAfsXgiI3jg9HWcA7t=A9$uYVTaK3&5Bk*J(=!HQ8xR(F^rYWA3%Ip8ZH` zuAIO34$`$c;FPN@4QOspxV?!T%7%f-T;-##Gn8cI{aJ5UV<#vrSl8~Cgrdp4acJHGiDG1Qj!Y{0Z#g*86hE?pQ>n_Hx46(*9kwcEzx(@>d zpa#;Z#8U%iX62fX?L6#@;5ZK&>Ocg$7f*aOE{)__en(h zkeEgLMPYG2&}EZ!@hauOZl_=?YQp5w>0jLY$h@HU>xO8uUcWr$^v*f|T;C1y!eg_y z$@fg-RNox_*!1iI;}``$+a`>Aj`!G=YmCR*kJT7->@3#nbsa17mrr`aQ4{5KZw`31YBSmP3y*sfwJXJ~hEMif_2 z$&4mv(Hd5rJ%1KUI#(LOX`l5Ch~Z{KXD}+|P&*3wgBG$=d6yU!q4WC)GxGpb`CX%| zAc>iCLlQICM1tC5*U)+C$^L#Vr*6>gPJoGjjI%}^1fHphGP!0fca{jhX}4ase{$@n zvfxrogt?wpWLSK)evFZV!D0589V2x!@8$yem|=}gRR->Luo2mipvkHJG`UJu@v`2b z?CVz-G*9C`VHH4mw4dD6W;Fp{et^%Y890wgZt;oUDDdWKRge{tZs@0Sv(Zgp^>RMK zS2!MrUjFE}J8IJ4I2Cv~I=O*u2wB?Y+Ty#kzdv`wvv7B0W%cq7L| zxOaDnioBPmc2JX^_W0@ktTpp8>-oww60Z`D+SR_@tOy>(!_eqM`~urx?>6i7G~qK& z&?uv8SN22K)6E~Gq9N`t>=C9zdy_)KOeI*cwVRHWvXF{3AsE~!t?>rTpUn}rIrm} z)@41pEVt|;CRLq;wqgUcG4Vwh38X#~M416So@H&x6d55wb%$NY z=$IKGe)T}sJ;EdbUHq=70+?B|yrD(U*2bLC|flt&zSeKsuWa?RAeM;LHpOBD*05;=x`m5A8DcqF>t4(}0P2TRI z=cE&st!J{@M2UV|z5PnNG0|Yh_+7v4>5fa|5vyOp&ZOOIy-z>72xF&6@AhDrd~aXb zTOoKN_u3^5!Hy3;+OJU;e?0V91c_kNOHWByODz8Q^CW8oy%!?9+RCiCjo7o`xA`cw zrZVs>)q2~<$C1A?e(c+)89v`X=o~x&CP4_^KIEy03AM+8c5TlT4cmP*lXwrC;mLx!o~c%*|6r3)U6ZAN}jxk9CS2LuDVYe?6!CtlE8LY+A7kMGRPbV_jR^ zG;ryte~zw%#^TmE#6Hx9yQ(FAGx6AB2q-64U83$qnk3aAgtsZ6WY1aCBz9rWS$sU% z87-BKSF9;J8|MTwyf<-ZbE?q`o_^VpvC+)@o~}y2OM+=y&z3*#qy1#Su@3BJTzM*Z z#LSC9n!kj~XnYPFx(#j&7#luk7r&hRmmq96ZYyzD{Klbg%0zVUgNsL{fdzAIO7@q% z3jEE@YNN!{=X8vqscJ5;2WB6i!Q!-*_V&VFcfxp5`K|k&5gk;fbhdq&ZW!2xp2F=M zgZpQyg`OvuOTZX}&<|F-!QPJ@Y=dEO0d#HvO3j6VpKdH2yRXd(F+HcMF6i2rcRO-9 zh4$}mkUKOK*DO_ONv}&H>xljrgeMLzZZb^sf3VKD7jOA>gw%;F^$|?k5JH zK8p73H8NzI#I8_?Hmh1{*WJ9e$Gg(fT(TnQp!v{>*j=5TIbA)}91b~%<|{A;;^Wf= zR*dlM85YKKF)5kZYsVG^_kCN}6x`uEFDpeXv#ZkBb^lnXztP8?^EtyZtkdw;kvg9*}DIJsP(-;r~)f?)N{PEvPen>llP ziXEoXQJk4nSd(2u;fbnu;Cz9EWWq^03Wu`o*lJWuKb&OtT(i1*(CRV4*CYiXFtWHv zX*b0rO??x!jjyPEEcEmS(?|Qf$SYPV?D!|GuLi#wK^N2`A;lM&{K4)-qjTX=z2{tm z`b)|m3r%@h^eTRSpy;`(uhy;Atg^6bz5hHZxg07WpxRb!gr???D}XCU&Dp!%?>ZPz ze<=Bs7t{Z0TnDa0sokiZ@E+; zm2gVb7K%o-`gEo8*}aFQ$>$R}Th*|dP=h3n+ln1?Un62_Fru6WDr%jsN%r~*&Gb70 z;`S?4c}i2p8^r!cgDyJP3>ltp5AFBu5BOP~w;6dpxIt!huh1i@TqbUQt~{oam#F$a zop1O!*>GKCN8G@D*75)#%OuT-xpzI%)XohczsJZe@yEj@f}*fVYlgjPKl?b3gQiDLfM?gcziI6>2ByV=+85UE zW-EegEy@C~^5YPWz456+0w#Wf1Cb}siwtN`%THe{XSasf_gGu7y~Lwc29YNHMOE#E zOLPnNCpefVU(KXXFtd?ME`#*Ucm~#EZ7kWfU$ix<@p(cGaS#`^%yCuwHTA(^LU>jc zq$tgRVzM@j_HmogsOe!$F@R7sdqcD3X=V3QW$iNrxM?wF;)l zzlsKlTj$1U7glo7N@y2fBcnKep_$91?fhzMwj*@+!#B5FnC|IPJkgf}G>Tm&$y1qBoz`8RfHllLBnQqg-AYRus>y#>4%K-BWi5ACa-{jDGH@ z(oQEdF1hCQXG_V&`Qs|zl#Ir5Xa>Pt?6Y5lLJkiMYf%(ig(le)e9Gxkl1`Bn4%TBx z5>dvGf)_7C6u@XLewEe(nrRyVofCqc+ikN%Y=j4yzOt)F6(+!CbhTb^qER3NH8p~! zy4ZNen-iC;F$aKikXtTe0H0-V=QC3ChwdE7wcgYDB;;{CH(e=Y=-A7dYP@fML^>Yr zvtPxz+uHuaz2zs9_us5z5T5DCVo3?1@!0rU@cd}d{}T|YTEl86*`1HzIlnLSSp4-x z@nphEk?GPHM6kb9+l+1~tf%nN7Y3BCk3cC3*nnBI7HN#|Ep1T;M@bTyX$N3{d z-gr%r^60r_LlcURb#~Iiyz2;((osLU**$~&-9i`13+~E0Kjb|B)C&DT2XlCSPx*NC ze)Z|1Zlt#P=~S&%-kb0;gcEChG(i6GJxB|i2k7AO29|>EV>WqYtxA>ef~Xnw;g32k zI-&#t`drZ#t2HuH6lBAxech8?UZuGJ*&{1^&&ubMeb)!Qclt*4!=r{Z^(S?o+r!;| zus?v{j_wuG9W4~)TFf!(`o58dYF?|n{pyof+gYm-saiY$A>1YVv!K?BbttpPpCORN z0ubXe{gB<+t(1!MhR`m~{QH`piyLk=4(Gb6pp4zu^x4v(@O}3~*LR(-tbX_$03)_2 zpQKPh&}x|#JeE$|IzZu`#;pS}s~)NJ&#ceGUMF2%D5Zqyo}CmyT-zu$YkkJTR`RjY zXQDCOVSfcf#rq2p_w%!iqN2goK}3X2<1>#cy^2wl3OH9%s*gkJ_GYRb!F2NY!dpEM znI`SsVd|=#?5ruykz#KyTa5`$^)ikh*ztde>SsvwE)-e*0TLfG{fb9M2?4~PE}tW1 zCILHj7SdM65gq;Bt2Zh*)i838n?R;XGN3e>?<%T4_@Mv!jjOb)tG7pbZ>*(g%4NRM zyHejmuwsKn5@^;N{4g8yyc!_=cC7c6`sZi=Nh2dOs@AYJG8vhE5PW!0;S2^UWQeXk zemn@efbee^ZkLLT)|Ml)wJR;G%t7f@dWHEb&p*?Amae8wNj!Ga{m&?WqWf}+9@;te z`kRCLw)jPm5(QtC{bZM)kVoU;GHjza>v4pIyQag6$Zs-BvLwFJ@bkjdKlI&!*(y_ z5ZB9fM z^>2SJvStq3jY{+t7X4kU_l9~XnXOxC;U_v%BkN*NZ_y~er>7Y=W_x;}*WAD@^~dg1 zC6`fIi7KYo04EDNSON;wy5g6Tv;X>=kDRX^$jDse!bj-lhPJpfHvT4YgsbFCf0VH^ zlH2;nP5)tPgJJ;rylVZu^M`-@^M7Ht58+$@`Mj;EI)&ly?*7frS6=}Ha);`b+Ry%+ z{sY(v@W_@z+Wj#X@UM53DW<-m3tHh}AP2;PfBv|r7Ff8nCm`KPKNF#WyH=@Yn6?z= zcC*Ldch;=x3;9F-?_u~m6=pd_=2|yE=+WmUTi=kItzGpW%h=ynm(x3y zrM!PndtY}bqv*~T-CC!odz^NGzqad!IyVVVyr!}Km2r+eS(^J5g^i5@A_=c7wP}x% z%cXxZZ{W`x)@SLvZN6mOs*Z705OvTkH3hX=u?aYy+^X<;=#Fq{l(PHH+n`aAd3e3e zWzf(r@42{1b#@RLLQ!dj>G8Bcy)z+Fv`vb4@23o||5=pY=Dsi&37;3R)dd?$Zr^Xn z-(DuWpuO~2UjSkmt6AkWaFrjjcg)FppZRY<^&bOIBX2sqN+x?zp)7^2mx&h4_Rxp> zd8y(k2vMoaArdL-N|y-K?S$njFmL>$#zHL#;~FxE`|YJQXg0vS;>= z=WN4onsxc}oiAVLG#v+u%&KU$)TFAj&LJlH7I@Wn9Ny`fLg_}&3}1!W;=cAyi2cq0 z|D4m2nNJd^F9ud*<~40wMuAdk!6GfqDpJIDx3c4}%?1o8Tt+tiTy543XO`c6+;#;p zq3hOrLBE-)m*s#ov5R-Kf7cK8D8MeiiB!=4I$_TQUIR?e{6^Vt-tP+;XTaELJCd4y zH-A1|zM)I|M;PeeTik!15PlnQ3Pk!=$$s7ezne%1(3opaihkD#_3Oa0)aDH4{ax=L z0*z^tF{k}qC%%2pSBT>G{ax>011W2q=dL1t*9jU0VAPybMlbw2yI;s$1RC@3t?utS zks$(%TKH^_@$Y(X4m2h_@Du0nI`Q9E_}^Ffy9xXsD;(MbwZA!Pzu0gh&%ZsXQAN}e z3Haq|-4?QC?y6$-g@a*(jl_m^l(5G>y`fW-%{W|Tbr)auEr{%baPT6Jj&{J-d|855n3o}= z{>PnpI3OhX5K&`iBjz#jsnW4Ikb{?3O_vu9qmn-UteixEly>>7RH-nqh=LMnx=<$G}raC0XF%ssG-glP`Ai)NsiVNM*8+I`xb6ww`MM>QRHE zq}LOj#8u{ejK_c^3j*A4!5BKLg2$M+R*txpFVwEr7Y8qe#oS9OvuLJ(>lnGyqMHnK zj_P2x(zC3vFlx3@cVR&wi$&-Sqaej_7(p(5|7i@09~;TaD)K$^rpUZPiT{Td0Kk<4 zkFOi40CACQA+ukyO3#zTDMVe>=IvvWY+5g>=)d5(_tEwAA!8qI?2z7&<{XK(fjbD_ zH;!z03|^n80|)~xxZDI2%7sJf{VCVbQJapzjQ7n^Fw|z2Qt*p}DXQyEeu|}vM;BTj2ZA3;O zzV-^^trb-J)1{@^Z*0&>JvdH_H60ql7+RMwc?28Wo!KpwqY|Pw@=D|2c$$6%khOf| zy3LUX3uOrqFf(9Ycq4p?U$S~!$P7ioSTo_%PqeOWOF)G(xcizc&q)xO)r<+D?NPqU*-Nkf91j)ozi*A~9 zztPk-Klk%~t(_qsbwT_f13FriG7pd@OkBLZ%1-vT*vv8%`FR_s4beq-JGc!Mhg@LxFhLr^Y!Tv8CX{m+WY%G>?6;Xg8o}e9lMQsl>D?Y-{`J znAA6(gMCWjT%aCi^ihbkDb4ksL5JEIT&zsoFvgk1eaBgT)GR=JzwrY4Tw!GzU)m*! z#RvS*6B1}eCHhb#^yH6od27y|>^5urZBl5^1#)Gd-6IN>Y2^whi79}1j?CKGeoOA%yt8V3vicza ziHG_!Sy~*b?Ts&N_$EnuV7ae>Jx+Hp4EvaO}_{+_J+6(z{J3ft&~ueH@V@P zF;hoL{ZfY?imJzKH72#hHp1A_`E(4uRcfc=Jzmeq3aO8Lxn$$Fy2aK5b8Mo%-*A*R z^uOk%6mML<**{96tz)*Ft;-4sgQKUdhJS4)~wd_QqzDVIR+G1+mY_YIbp~k zF;)U(A>v-qoABuvyW5v4kBT~uKu{6P?ePWkE7vEiXIW2ckT#R`RRg8{%Jw6>!golM zgsNJVsiapgYwBx*^^S=hYOFf8?of z8eyu=OE#f;bB*N^!?^#x5ika8Oy?NZn@5 zp<;(~d;@{xVf86AgQ`iKIO)dNASFGiNWyqzH1$jvlj~Y9Nm6TfW4!OWCC|g=W&$LA z?3e;V2Zs!V5|@HNj?I#Mt46YF!V_v#D$mhiBUgNHO;XjcKKi7{3X;X0IRedZBmMV^ zAyY2%=K8tCOWQ^KYOXQWrudhpBXO`y<-26UhDY0oPhz5MmsE=`Q38%6L*ITd1mE9tvuHyfb!z#rgWU&LsXts5qXlpR*Z z_etOfAE@V^blHV`(X&ov@hHx;N1q<^`)#PPf0-u%Ih?Qvjm+2ZHWN6##>=hD=@LUo zInjz|IjhbRqc1GqWcFNr$RYSKh_+FB+xt=;K#BCoP;8|p$_(-}tX@A?=;p=w4QXnU zQe8orbq~t2?QQHFb#qdzVWV2<<1PQt*t58?#D^9Wmb%eO{5z(>u6<1({J_E0T{#9w zdjr>9Py#GdPW57tu0$N4r3J~qJ*XRxGV+r^U4u_NvkUVk?h0FBQe5{Ka9I(&O1e28 zCxgi;pCslnSzsz9mcu-|Y&qqRj`6%7Cl}8_LB0|Jkde~Nflc9k0WoM21@x#`3Bz(S z9&006%$*>_QxG*Mzb6ti9oI$Bbb9+{ z#n!s-`uFA1X%gk{$>%$M10wVoAN=NLA<~S#^E86G?$y160}qgPV^RJ-d4RlS*w>t= zF)tkg0N&sxkrP3~1L2%_8WPqS!xpMEUS+_Wvd1Rov3i-I=vn5n-53zxuh5e;++Rpf zV9f?|en^;j&kQ9jEzE&}_9>L_3QI~lHC5Yxym$vYhsBsc(zCqMyqA}(uL_}^WGUwd zqiydR`3;c4&05c$Upa6DCdTdx2H(&pLhN8gUyVnWwKJaI*o9B}3}7U^+w!OFrTV$N zq0U&Y&x{~+ah0%uX5bS^e^(%`#Aje%_o}W#>a51&K_h0K8eXf-rG-KSlAme{swT3E z=3b!v_nbI$*5pRR=XcFk@8CqXv-L(&dj~PH%z07O`BjjB4|5Bh&9$T6zCajRRnbH< z??B zH@^H(5~Sp;Uq8Dan!1cHkcor%uf1)bYVsv>pWhMaQCoGANs!HRrkb}9LO(xmg*zker~GOc>qWG}uR_xmMc{x^02+J8N-TZh?N{#*21fH`C>_>owm7yb z{Y1@tShtIU0jxFO$S7!=_@yE(YpLec|2XVaXsuGJ#>7u!i&VPiJxUIV+6(c8hB`!A z_@lwzBl{XGEsdDYz#;_MAT`tZGQb38_x@VqfaaFlZ6uuOq#Ic)HWTJ`QoVvq*KJ)a z??x*A*;VlQP}Sd)e@$r7bj-1`ppKBda@=d72Iyw?5BH7<8JZjoP#+m;4kw0NHdNR7 ze>G_h2@AYjqbwNd&_Gn(A6^`rbXi%O@l{zo|Jqg<>jKmBP2CN$*cfZd+p0hdRVBGV zicmhKX(!Joyu621MbNO%3!+|=?a33i2=0%&v6626lC5vj=4wq_NbsLx)RN3F*lRrf z9$B0VamS=>;_J2JekX^C)0f@+)g-p&Mnbc{?E?wC6qshmgFnuvkE?c*eA2hB=&cc2 zskV0%!j?x69gf8(;5KNuyxQIljFoh6f&a}<=->;|aIuS}avk%+2O7uG(qbQLnWmW> z>xbAYK3DPgiwsrT4gr}PICj?~W>2yT+P7cPgW3l6g4v8uF^gdqd0G-Jsx3vv;yaWf zcR4CLDZ9}jD`Q1jb1&YHN1u)jO<7VkL%m&(l{IzmOZ>&6W45S!6^-4dMf4rIQyG~XCoCepvbWnY? z81(U7MZq-8+y$kbGG|Ylp~v0F#0}rE8Ot}lOhOl)f6IB=;zN)d2Mq(OW!ze~Nx)bE z8|>Xi)TLly>vUh#fEr4_WFglyT?*^tlRc(m0=4&;^xt@CzV95Zk>ozUyJ*jUP%X%K zby=!^%vjIccdbt0OB~6wR55n}ByaZvO(&0X=I{pKES?0ML6a3&{BKR>S zb70{EUtktkn?vN?l68EsYYzP4+m+KM3I6#(%@9%`(GjiE$b^EUcyXHPGudwHo zbA*NpD~3kG3y?W!m5B?oTN-<(a6SG@4T(L*Q2f4EeKP9@aWm7?m(yzXCtrK9Zs;i7 zL#7)$un?nrsRQj}2wbhd*+h##$vjQqBd_S#SAZ7cE04|fvKN)ayAF8 z79pL(RY$dt@~tVsy$fZ1UF4rjobX?;2k%dMxFbT5Cl5F3I@ViarjPN1?wY1^Q@-m0 zovkRE&vkprT(^VotrahkEu>qlH|&?ZoK~oveH~`le=`eF1m^Hv58j_T-)GhAzyENu z!JeAt1WM>s=$#2=eh)Iy;9Br8_R+_+?zygz=g#?jn-0WHxLyr@Zk0RG8OMceEceDG zvGr6>Hr&0QOcHaOrIvRdpEZNvZ9XUhPSra}rsi4dJ_KEv&?+P#& z1RIT0ab0-mic_xe>NGciAmVHs`q>=5i?0NWJ#z3TWAxcuote7VXq%WhY+~8(z8jAq zJ6rRT*1R$4xSLmY?VhyydmH|t>16Xz-(}5O<8dLI6?3cQR+IQ>*Uck@-Pq7A^rlL) z&B%n%WW2ca)<`QHg84M2azcnx0fq%2wNaFGEF)@)?C5b_h!RSEMo{)@opt)9>VX($ z=)o2n+M#JzIrv;0bPrUcRP{MFF4ue~AjB+}yyZtE`v(f?ZhT|wk}ugeb?sP(Sb(Sq zPu4vt&9fs6mVKg}y0dQ(hgZKbzr~iOxX7oOp)@K$C{Tm|M-e@HluHDiy!4?hfCY$j zxNP|E!vNVA$$WH9QQ=CcQR;)rofGn_p*qzL#2S%FIN%)NZHJ2r2Gl_MG51uI>)B-G zcM+!RVk^ho2Vwes5gBtXI8+^ZAftpl;Q#zHYFROG)Xf4`9V&L!a#E{ucpB=NJ|RTo^XN2pZG zU4zVb($8e5UGwr&=UQ9ylt1UId6ozOB5%vBhsx@4%Uqf`{-34$&yr>Vl7f#L1_^b! zxp(Es`UTy}{EsN@%J8i{0907Cy8zNum3iC>k_h-DM+IxxK=m&cgp}CmbpW7Ys^=63 zdu_?hoT^L!czw2+G*AXMFenyEVeR;-x}Lf?srCrTN>y-rwxI?ot}hh=S5CEML=KKr zR<yuvc@H)3@7~>2*0Tw-8Oay~qSLm^fEZCJ zasI2bB$J4Ka(76bU4>=mGaxwa_nnXd2S1#15wiOg@c(nr;Vjv!WFZGrfB<2-j6pIC zBxjK!aJ?pWaH1$%PX;G1@56bkTzFB@r`Y&m40+yo`rH!T98of$8j#h60w$(Z^cU$=f#1F@FvW9$6mt>I z3Mp&V$MGQd>ozSxAkGm%~oDO%a#KOc6!dzR|1WU*#L6T?N%G?f*~QkS0toP#>inv3W)3lwW4QdN!HQ?%34oX+QC!h2Q*;aF0!1 zDuj+(pQbNDJk!1sxXs#TE0qwS6XEQrGolj&;#$p}|MZK^XN-{iC-qvCZvw!xmJnw;MB(o(D*DTk%3l3pCY04ul|@WsqL`wD z>YB)>?+#2#f`j>R(J1bvtQ>WCB?3v@H%Tjta~P+!k^(4e^NKFN5^mHjNcSc^d0lY~ zc`rNg^9d#}i)bcGAR^Atr<9f`WM9AJxhj+GZluY&jNlOr0=X~Qx|aMpJOY7|dn0Wr zp6f|7Cn%!Fdr>&{48Qv_az;Wp)4OAb={f53q{B2m?UvWpfO7vHxttXlI^0Tz%LGgxzlR#r4mQn?v6E0`=7KuWg!16Xy1K}3TV|@ zxGU<{R%rzSKvxnMvUVgkHkR~=I2&yPPw&l2eqffxAnf4xpxE11Utwi$vCMuG#dD(J zSYZE3WGfw-bj7WqeOcwgfTaI#D2G*w(|`~HpPZUh_kI0Jc9HdaDvadFf2IDotv6a# zrDFz7tjRh`_w;)s0`3E3y3`crUv4L3L^I;E(uV^8P%B0Ob}Cg9Sx)+VKmj7NZYu-= z`75Ka9!c3!xQrrMW&OoSmLos>VCRpT52!yc zY5uZM0B40&GQ<0MIhKHLCdkfpUI3s%zHg#1_}%={iU1s^*aYoQ9^w9%$-0XGr<$_Q z>ShwC4F&(L_oN4SPeAe3*K2bV8c|Gi0EyOV^n5R@`SX2a<;lY9NGjNGPEwDl)d8mI zbwT=Zj#h9vXMhq zJ`J{&+|D9JxP<`pA$^Z+r6B4OlV|+`?7aN*R(EBm3&n1G4Ed0ai2k~ssNV@}i%3X( zbs-^d1p)5pQMog=Hve|PXEg|{T<)- zzog;+^1gq!&HcaUeP>jPG??)^lL1oOwg6%3E*{{f_5woScF6)ZcEg6QlobQ=Oi4}q zW391qacyh1uZ$h{R!4E)IRdSr($9zWAu6tmndC6&F&4$OBkJn%^9D3X7U-j7&ovl! zvNyhg3+F=ijriNx6xfWq2v)Q+LP%wk?Cb69lZEV)onf@@GsQKNW<2!=<&}BDOXElt zZ*T9h`6`!9YuvNVLQu#g&x|#)X`ibL6=x=}6N~0wM;O$U zFeG5AUQIT5Dv8eb3a!NQMKiSTtXH^xT+mNn{=A(#hH+^nB)3QroptkAmO~W=O&RvJ{Ms~aUEQgkbgT! zU->)Ft4`G@)%)0)*wbDel>ianpBz}l?>9{v7;{}o6oqKtg=qDE19)ek{wPE#X3VPx zBw{4=f0ZY`BU=R-%WEJdLJz4e)_<`V)}N>uVSfJm9KK2*1m!cgV)DpA<%W6isT zEdqo$3EaC3CYC(8>K0SD;Opt(5@j_vlFK644IUvh92 z&*2`I8T!^1flq!FA02sxS@UM73+MAV`wT_*(DEkv_4+wN;p&HcABE8!9p(<^{R6i* z#Gpgxn(hvt3v~A%e1{GcbYxlUwGR(gt;KHW&CmL3toA#@UCsZUO!k&H(6+N^n+j#t z8Uk*m94D9d_Z%CT@Wx2Kd)2<1)5s!OjTcr)WZ}nMaK&zt_t2X}3R1KATgR5=UMwlD zp18H>+pty-V?NqZ9g%nJ6Wu4l1IPX(EL4qod!I-poowVW$v*-96uh4V($>4{w{g#9 zaz8Va+3I_3B78TmrlHQvN?y8uTImL)r0%QTSF>5Cx_dN#`m6v%bf7YW10XDH;nrv? zwsDEc(QR*8eh|d8>CzE!j zd0Y@pxUwLXgviI^1?m1V$U_cYqm~+;y{FN^=O`a=)jZeGqmSdkph*GMMfA&E0Rg9t zrUVn-8uh?Xwsbxm^^W{F7=(rr#1wT!uh127cIo(Umm6V`yahprn7A zVmp6TpribILRpFZ6W$AwxTgIn;pXw1xno#xs#r1*=1I1Qk}CfibXbqNtq@h-R?hm+ zq9Y*AX=0_yo_eR^^yJiftlB1~6xzffyw1db@F}y!?^M5@HZ;vIxo!z=5L5#c+<-BN zOncR^4M$>hkHX#`Z&`%k<_Lgo|G-FXmiBJ>ho6e4h{Qz-Y0*_tj z0qP?#8#4K`49-=(l3WMsHH<%JM+C5fZ7I5%PTFBO)Y1P#)>no_wYF^=D4mLkbPEUw z2uKVdNT+lT(#=pq4+sc|NVl}o-Q7s1bPq^LHv=;ad~3hYcJKFjzvEax;LnBz}Svt z*ctNlhW<2{1FM9#50HY6e(BfqCD~#HJ8&O=Q6KvhU}+cC*-Iz*9@L*YP}t4&8n~`{ z=6arfPBAV4)H%hHxcOycORok2)^r+CF@RRIRSS$9FpJsT-n2ricI#;D-tY8%kKi+c zp=-p1lcqh{Rlj9aje_8dJ`-*BD>jjF=Zjzd(tTG~26c&?pYb|qbruCsA>ZLwJOZY{ zONQV@;9*!Xn730whmQSQ;GqE!0H{++0F&2IhmrvNSwm>ROQh#*AmK2y%i(2ShMV(IvJ*S|^VIEngLchbm72TU zzzl^%L*Mv@tAvH?xz)2?evbkg!$H*k1T&=Z7)k5+r1U+@8AyKFPfjlMs-60LQI zaWC!?Be(s0!~ka;+Qxu}CjWO^sZF4G4c`4?yv?%-t;n+aeQ|suahl1sHk&aDH3)g)f#kcvMND1OUyO2eBBNgkGCY0eRQXS;GBt z@iiLG7mETIdJ$rD&ax>1rkIo-R_jU)3*e51yZ2RGA-KrG4?nrlNJ|mw+dFZ(E}faN zg(4!mKt&W7CNejAC}gg8H#Rmnm1H}Acfm5xP(6T+HSI-8mT_eWrI*>e;MFwun;`4^ zn(un^+7s?|YuZ1%=Q%yM1+dnc5M1zm?|myUZ2x>+iuLf^Q1m!ER(*5JG}w2G@~BMG z1okjKXx4peX_$pyCu7m|1|6}VL6R>IJ_IP}0su6vb7Hsr-xZ7+`Aq4DMf26VGj>a! zMUQ3iuZ|a}+?PqsdM;1dQnbp~zuK0S+11D=l6k{9ZKWmWAI`4C@|ady50oQ^Eclt~ za59ZwGh`l zzi3|NFJc)ve|Pb-YP2P4tgO;90^}xMypP6@2YGablXfAmudY(HUTz$`dZU8xG^wH? zd>at9|8fET{YiJa0MJ(@T{>3_NYO9k=W6EWo}TbF@|2_uR{|1tPt7FEtHP&2Jr>Mt zwi6JQ+10>zbKh@-92#}tTyFu=bNQjilw5OCj*ul&|uHK zpTr$R!uU`&>4~VH`E&_+X+o2VsYf+ezq{UbG}zO;!{iYB5Vw0==rCoT2sU%Q*$yx* zYfFFnw5fNx?X&ZwlPG%9bh8T~^K}8z#LDy1b-1SMOE6)fef7`a)h(Hfob}v1+s4sz zRu)1zFy++^hwx*8LXe0_{S|U>G#~@erBUIFi&+3TDNlyfI9T@)+blFnU?vY>_O>wR zRYR_jcE9~vI6n?D`D}k?vx{&Uf9C^J+4s>8I~v{gO$XyMcmO|)(({LLu4KsP)#wK5 zQ=l6su2TQOnM2ElG!P?>4qrMMVa)K%DfC|dHjQMl^G2TwFgLqHyw3NC&$xg2qk#Un z2DArn4-hM*iwLF)cwZQG;OE#p+}^5#`w9R}ML@rTuhUT> zsixWWNB>^T^T1C0L?y+VpkY#R9iWpUP;ekX)&XE%MnC92t$}Di^)AU4G7yRY1|)%; z;S6@5^mR9Y1eMf9D9o5z(mu^VC}vfAILGorv?K`Ud4GZV;OBr{KI|nguZ6jDi!xDj#-F$VyxgGU& zal?yP`;B;Qeq`v50*pN=*&tE*RM>ra{CXc^yRe@-H+lrIXe?22+vN~QI-e=4*YcuY zmt!ILOzE7Dbl{vVYW?>0o-`rI)z2&%Z4jZ`F`loO&qCuj>$+EeU62Pb--~nysZl_$ zLW-jfGQ9|^?d#_anFlXPhbGiD4C$`Y`5kkLCwWJO1hTmPPf}E)^cOMpn?w0|z5C2# z$9Kn@ib`F6%x^KWs)tkCNN^J@cUU}iq5IyQA0g@vdhrELK-j}XW1{-@<|ReDT5UL7 zv=W-*wglaJKg|#+y2^!1u8M4EiVe|Y4B8IX4_ahW^4Ew4}NDKXWJgk9F)g;^GJ8=ZXHf%M5SIZ(fP&C0j=0)B?ntitA=RGxq!vPZ*wgEN z%Dy@n9Nn_vKJL&mNf*_+u-@NxSrjq>+pCbOF|>dAQc#DFyP#Bm(yd_xnEPeBp6lhD z=k1j@RJZ8tB79xjG+aN=?_6(U#-clvdbZ@g2}~7wwPx>V^XsiXWWX2E3*!|%;eA@S zK)qm3=02sf%bLR8fZj^B98Ti?sX`r&EWNR*qCFSX;x}_vmrmi4iA}Lt*smI0uE71t z0{M4V$Nx}$GMTZmrI~Cx-_4nh;afR}J)_PR60%J7bbEut$Z{xl1*<@S%x$C`pI^QH z=HMDT<8;XPQGS2Myv(81dpLKjnj%5ae%`gtv(vUn%a#eSgFsX1CNmIge8~n%nn`Xn z&J&~GBQ?#E&X=?D8UPO0KG9HubzNvx`xL(dxGsw-_iKTap^>?@wTG=n*nDJwQDH%X zLrT8yuaODp#8|bW3PSy*s#qye)3n&>7j=d}cPGT|^hAwXhbw4<8_ogG=r8cREWN2H zeh!IVh~WHZSkS`?b@;{F#V?f;Bfl>xVS*8=57;};{b0sqMQeS2M9SpbvTN9X5# z&8ZrQ-%~Rj4&pI)Di5ZEN;=%(xz`;Gs#kG%|QAPSSzAJ2QpXAKb7^_Mm-V#ks>(v zeYV&&ho`3~9MWhlb9$mMI_ISi`3leqyS`NF3cgdpc12w;Js}o5ybcRsIc)Ik_A+cS zw~lGf9J=f5qsr&Oe7avSE3)iyB%fNSQi!A8EGo(@9o51+*iOS;kPd<^wKmSE;lRs*nXcS*5^C0uFhx2~J z7T7U%wJ80-{Sf5N_?SCgpo~8DxdAKTqp6z11nc0uw3p?6)0R0baH$%YeG3KX-HVh2 zhrT&|NSmqKTnS?$IqTxdPu|~L<6Z3 z?uu@;!|m95R_xU!3gszWa+%q;z!Y|!;NmF-SU^^`#TC*6#?|%PF^X7qg$#sdMOdT5nt#xiTl7mwe>rv>=5uOQ2d z0Od`@LzDaXVqf4T&>y&H0+H1~>SmSv^JW{NGfM8Gb@i~$Xrr2T4^V0W znmG0S0uAS(mz0cU-w|Hp2K-7Y3r#?y!*pe*dd3?i8tD3}^qj3y#9 zi9_33>Boa!A8!7WrnvGL*l7r;Y<=GK9pRHPqS4k1>rcMwJ>W)`t*hM)Z=s!}CfDuM zN{T6I;r+>2^$`_#D0^x1ysy^Q?4dO-&=IUY(dpZEmaSyYAq{$?ng;jnUD^yd9hIAn z-1*)A_5XWGHrQH*wpBC$X8WwZU%k_DRP@=7@js>hzEyybwSl;@6btK6SBYH8R%^)uG>+CDfq zAYy!5QK71#iiaV5sc|$h+Rm)OsQB1ULqojrO=D9^4n@MI@Tl&{-lF%`^zwTy(XzJP zp;SSak>uN<#X%8N>*29ll627LR!9p zY531@GaqnF>KA=XK?#G_>0`34LwlR_Oh_$qUC4@5 zw-WRZ2^`K=Ei}8%6VfdXfRV$x#p>!rhrH;K)&kuN)AtTeK=!(8e)p zoW7`(Ei--CapAs1C)rBAGd`97(~q4P0VDl(^<{;6Ha_V&A!wbod^OW;-@!@n3B}P1 zm%0o;>Qak6+u{rJiGWW6hp$~Ie+&anUj1u=hJ`T&kyqZ?&$OAf0uobDA0HSt zfmkzq7RNsJRs7Kc(3a1Kl~3f2K35qhq>P=W;3lwd2%CHjpi#lUQLFySh=ScD+9R{K zs8=MkcmM191ihCiReX|QJAlm$9)IYAtQZXA+|evh>z1VKz%yH@JDjBEJ`(X~`Lu-F zUjS2o$VJD}h5J$zOEQ%k7E?-c(3*xk*SgS2lo9#C=ApebbptNQbaw4%G6zK7BDTU^ z%D2#st@}`9Gm)2d9W}@(UPY9g78rCJ7fvDI+3c9oNGR&qrW;QIv0{g`o=3>E^$|i* zX$YP3Z#B-bCr3_XMPmrfVLdrXQne5teL%X7RS&?NRbq; zfAgY`r`%I)^>9DpyBF!ze14(V{3GY-vH*v^QQlzdrBCJ)Gefr5c8gBE_TvCjYIyO% zXznC_Lug|ojzixqxfJ8kV&s9vM>6$vK3gprZcI4-Cd#<~n`exepJ;(jMS$Rt-Zyv( zq``3GgAEhFRf`1_P9;b8tTaEh%fB@3NdkMU>UtuR$qc0L5!X0P6rDo@+7Y9B4Vx>CbzcR&N9uIAdcowg%H`W~bBe_h%n{$_O_UwKIr8TEX*nK+Rs zCwzoGnP%7dLk=1Aj0@q9qiKbi{;Kr^Ej}jbb(VC%vj`JT3#qFFW)=>3A6_^whLd@C z$a@MOx-a{7oxFW>|L8Hf$fbI3EiF$Lhx;gzdIJh~wBaLy&R}~V3`trNO(ddau$NV~ zch+b?JQ(G~qF0t6-MR4Eg>n$p(~yPl1DHDNXo~0?_XoK&zDK1`92N=PaOA3HbSur0 zVH3MZkUPAfTPY7`O+&d5E_1#5T*H{;Ltchg=UO??ZtoE{hnr9)OR(Bqo5GD=JQ;$aZmfxbw!{r1K4bY%c<&f|wlqd3Bopf$UdX00-q+?!)=Z0Olf-=lG*+DC`_81*6{U)B zdBio5o$a6cs-hccX!`g|w`9;>wvid)JKqU@d~h4`L=cr*+1oUVlL^bgtc? z#+3ZZ_{)Wa^SqyalRnH#nFHKByZthf2P>w_@2Vp&f7e%aSBt}Z{0BF+=T}w2hMz># zzlRez4NKkWIxgGq-aN`kxkpN<8zwFB+W_w$*Q2b}^YQ)|%gXt>#XC zx!oJVHR@rFI%Ee0csUgnMEJ9ikwgm5ErYyEemP#G^(P$mx~{}Y4(@fGq&FSLSgSMt zHUKY+u&NRe=9?>Q?&?nL^)Ad13-Ln-JhqQd{3vz5Hc}l}bEz|jPY4DnCZkBGUy1o3 zN?OwoP_%_BQk4A_k~NCSJf5$j=SuJdGh2}-yjNFwhs|L1R{^IX`GO96vSt0nAfYCW zl~)ljbVW`xJcXARY9zm%B=a&WB+AgZov}%HABp%N(&`>hz%|kVD`=A>}I?VU69J?3z9EB(!kzxp0P)7RbLt3sH zug}(AW*|Tx$6H~dQT?$!!d@}oN@AE4`iyq;Or_r-xJ@Y}mJZv{zYyM!?0?FB!1%9= zmgA9ZUT#`jQUd1FzS$@%&%o;|#1g~=sh!@$dRF>@2S`7qcxY*R;3naY1_Lo~^F4+9pQU)x8$_<7-Z<&P<&?M|H z)DHH?$|0ZS1_!D)btF9CwV2?yP*qf1EXm+rbO-7MYNOi3d`%Vv>P(qT2cc42@Onb2X5+)Z}fp zkdIN^=E@%x?59o?xlktq!q7UP&}7Gy{uNLV*1eLUXZJ^kdfxLvjRhZEQe@Pu1?~gJFVc*{`ENZ zpF|$~`#AKD2iH=b`;o&uz6a^E&+7T+!_>GAwF(l@<(#Zq#rXzH&B+7Zi3^3%U*qp% z5)M>bPe3`9Dg9p9I&Ci>KMWm>V&24%A7(v%`p-Qzh%$BJajZl>{`uh#7-|YDNBu4* zzoVuqJ$H{M`JHql!_@o7^*7;+-8?K?Q#Njz>K$JOR;vL4v4J7o+uzKDaseW4@O?E} z?UkziV3!wmVSnyMi^O=YC6yuQ{m9dJOGG*VS%Puc`le z81{sAKN-8v##!>&%FFf%s!(^7Faxv3>ci*Y=A`PjU1g5X8!_L=gT`7~*Z={K}iZ!yY06;GA~8iMIcD zDIW;$!FuguPq#Rc`O&rRQv)^no&uq9Y?^9p8qaCYJG4sFr1%|X85e3HiAKFcQ3q)d z{N1OUBZjoG%qsZx!;1|rIf4}fz61rNMPoeyL1B}{5e?6^9{329)BjH>A@@5YHxeV9;<+lyi~vbaR-UlwBjLW_P;(==e0dS5hFT z33q9EWgr|3dJ zKlW~as$f-LfqJFml3Rg(mPd9YPT|shOoD+iW3g>hht`6UI8H+~tzrX?Ffy)K;ER<5 zzOo#jtIF}l^C`7lSJn*s3^BhqxuZ!;g9#;3`@(JmKN)(n#r=KMO7utp;ehz{;n;<( zV~ee5K=^-PlDyJp>e*bSdCFr>1J&@X)USpWC13lwLkbcKH+J}T&3^qTs07@fpxa*3 zh`Cm+opr;({B%rI6iCzlg!b?KD}R)!W1iOm{dYUD82l^P2|iZTtfT}0t)!tlYA@WG zIW(Y1*Pv8Y-bW8@m@1jPHRCzdg~lZ5!Gxx|4K_7H;A;NiPntyWzSq|EqKHPQZNEGG z_Jv<0`C+HYPBr;ScOtxxS4E5yvE-dNC|XtFxJq)h(iLfTnoyG4rx%g&QWtrF(pdI8 zRI;A?maKVD4OHsfJTDp&&9+M4+9G0d=uLZ+qg0TDFUqw z*6}CF(&LfCtF83OZ<^)yRib|f(6iVt@y3(`cmK^eh@|f+)KL?f+IH}f7ODm<#Kx`aDV`!BmDYH=eHH%l8bDxG$=jMHcwh^V!iSii0LoSC_H=l|J2UwhP63 ziwz3kOBEHetU{~kPWj6Q>4A85)53lQt~O?c_?dVv?<{W>h5SObhHopTb+@g^1o3N} z>4yZ!aUlUpeR0o))$8()R6?#gU|30_Vj90Xj zPZpbz8!%5rkcC5Kq2#BGNCD~3WA6{7gFIi5jewVrA zzQw+a(OholsZAlJDUkmptPjMiABs92Md8tiO0-h*C_>s!9UuipWy$jIK)i}`W zcQj$-k5*ST3pKEdcB8-d^y#;Vjx8Lv&dBUcma+;uGwPrkB>sM)4#3Tk;&Dtf{W1Bp z{qHfHe=2) z9ue7xRok4~BzBGGD6kkdiPEq2s>HOQE(TK|XNn`k=0(sgs=9$ghMO3C3Vznn;`mxn zdeM<9vide8hE<)}mc`i+G@VoRmua^^^UC0wV&OK|1-i(7gJ$XpI-O*TF9vih*tX}v z7>!z+y%w$yH&aRzv$NI5n?_lXlynZz9goPlVWz*2Cx+Aq6HV26o#a0r8Zd7xjw12t znn7`@nk_nRpg?%weQnvy{q?@&cfo~bJALnJHriP^G#+>rPw3J$2n}JqRfc{uwD?%% z5&C6jzW(?TI$f0tofyRauV8`}j1g^KDOVgUPZ6G$h1sVH;x3=y3Pn-&oZxFc`Il(?O%J z=mg#RKh{_R+1pH#TEMp?XB z9=N@AF3Afn{>gh8Ka!%0tL4uq9U>D?T=;Sqe*b=eLhiw*n6&s^)h&6~F@=B^**E8{ft>Eb z@W(}XJe>b=3~F!M5nN7e&(`VwwH@O3`Y}iO_j%1K=)&xMw_zsqg1_gUFvkd3Dl1|uk0ux+wYMLb z=#b1SOuEbCC$VsY#&rF48u}=&4~I!PXBy|Rtg^?!{^e)VL4EAmXqLhBJImWozYIQ& zslzlmx{=`I^xLA8)i0L(pHJ%Okb!7UObLzhIQjCAUz2U8j}oT)UPHb2nYft0$gwrX zlV`A;rEu3r<6!@-e7=Mj%P7;W;i;KgrTF?XVq;d+iQe?CzM53ud)~O-%+cWFi(4EMP*Q+J$85>78-=hDMmy6qA zja9MnO?u${i(_Dj(?TSgiwQH+>)52rd8oCBiiHStID5L-v=6?VD9x->YGR>r84bqt zjg&W7MvO>`II;idXuJI~t8|RvMxUF;#laH0`CtO}wn!)RXc>fZRTp&0!+O4IgL1B^vYFB=+O^fIi>q7q zk^1%PS20gjrHo;SxZT8N3Amf><7SCo+9J(~i{Sa%vWzcZQzrNNI2*V*e{MbIZ?ixz z@xk~5amT>FL6EF?wtdYJ78|gMMU+}t*gq8g;DrWSU6Ys+Q-GZ`mdibGZE<7XnC#x2 zhB(f{bO)v(vebi`P&RPF4a3jv9wq%^3+{=`*blD2wBck?Sc2x^FDb@W_gbUi+kE!kd7qFd22wa>($D+ND^H6izv0TKJ-RV#oou1 zJSD>Iy}UStbM;2jBxZ*ls}&hUJHAS0p3hg!D&~t=Y67+7(Ox$re8q|)+DJbEQX49# z`L!^V^7i=!rq&f-iN4Nmt5#vYSbWiNKT*0 z7^16rL@+SUq>!Uol#qK_`6BU!VJ<3s@ZD<@G$HE1Qckw-pbqUVyvUl&6D0GgST84L z3eJMY>~Dpka(vN3YUS^Jb?Y3yzjtfby&bC9!KfVnzO_4EV2TNhBS@zuM-3%cneWwO zPiGC6+I+j1s;PyQyq9kCMK8yW5JF=mLkJW`wU@{C4PBHJKt1QX0{uYGt@+Sw+{Hp& z+pu=*;-A<-6fngnp&@@Fbs%1+eRYpefSulSb1(XIt2xB}!NmRM{CU4}#w1CJ!lq5D zy9M#+Oq_i^hML{UgvAEuph>U|p=iEH({%Bh(i~Uk-kNarg)i{6R@4nk8&q{QVgZ7k zq7zRi`EYygds8zI7c3zFePlA9MSN;yW_VoP+#THfYqfn1=Zn#{r}UB`=zOt*eOiC- z(e$OIFqi`wtAQ-iZ(RT2@bXhB0+Dna5E%q>MIuGtZw|$!%K6t3rGHh=Q<}j~lY7p|gv%7k zBEpeZN{XepTJ2W14CGbbMzs756mJU;+fZtNe#>g>pc5)OM7ZCtQH%kYB78jScMC)G zZ3T)&H+wpJ@11I^X8GuL*nKz}aIW-%?)K%$k2ThH87epF{=rV}0%%tZZ}8Tr&t3Qa z&;NRkqeFVfy-81N)QE&&NkWCy_C#82Q&`DgaEg0{jGo8~vKXM>4lQPvZdeRE+> z;|1z6{V84QA47LO&@aw)1csOmCfEn~#(tNP6^DZuy1zGkV%lsP11932=YTx|3kG@!6bedI`Z@Bk| z3b!olshdtM1Q`X$W(!YgmV6gV)ao5wo4ef(7Mc96uM(d)oM=0+x!2(gHTA7w===lZ z6Y*j^efX3&@$>zEg1S%U_v)rbxa#UYEs^gLFm5C?$eh^zw3__Q?Y!Ri^OI3TR|sKI zv#cpJBhNV@N0rp+*FWj{_(Q-DTdxT@KK^G5Z|~g$m2$Ce$g}*|OD+{Pcw5V49y^hz zGRV~xviq`JHVsdtF!&aka zVt5k)Od55*R8&m2GC;bF^T3%|6E?D`{RR^_Gzf|Yj6sQ{?Aim91^w7l?is_JqhdaJ z>vHv#ry4<|v=o0%=Tjg7X6xai4N>{Ctwi1!BWZ2V%g`0NJ)=vHyaQ(#TA!bvI>Z;w z5$^s4pAKj$rF=|%PhspgVL23MzfimD7HP>z&n4nm^~3W0OYtwu&nfx;wo=)j?{pVx z7_a?75P;Pa3HcklxDB!}@E4^jX4OFj^`6JOL{|63TR{y=Y%~kh>uT3!u{bA>fnifT zK*OPKi&yxRR&5Ml&Z&}8v3==34({3;I1xyM!LHwM@`3y!L!NS443ABrB?K5a?WEgk zQe!t;Efo`^EO+DMnX@sa#Hd28+^`o;J}5`4bFX*ObyL6YAZj3fK+eFxpgg4N#+<&~ zZAn_U$su-cG5$G#%*3$8v&(iNYq%z?Zm5_Fzp(9Lw2t!* ziR<&}eCE~K?3rU$5Tn+t!`&alRMnWsqfl!%w+`SNLYSn!d9g7uW(yZomC!*gLC2#^ zsGVAm$at=%OMxc8P!+x&l}pf{K$>=#=u@LkeD;kK4|K;)x6&tP5ALU`oT?aed5IUp zZb?w^uB7sny_(VI$DBzPccg4+$)++?#i)!J~t4 zK3f5+L==#C6|^CxZn@lvc(h8uWh(5ZJzo+Hi|1=$d8x0X8G+FOiJwUaK2o@=?x$(q zTW-WpIp@AQ-N4?NeSn*&cTUCuUOoQhaj@vQjs9uLiJ%=J53ru&Sq4=UOLLX%X>;YG zE~v)z&E*rLfGSF(_Rk4BF2h#6#IQ^|I zw1?TjZ`Kb#I%b$w`<6*ZChyT{tQvfMRZMoE{@KG@zt*A3S7xQ3A%^uAEckn1ry#F< z*}}l@(EyHKv&l+yr8CM&(Dx)YRZ%FfIe0YBns`@LO*uJJ)M?DryjW;F2^+5k5?mW-!Qu%DJ}yjLM%ZznQ7en7tJ>xPN623~KC1;sxmtQGsMRB^z| z)8TR>vx`M3&hP!n3~!iz4YA5ZP{vD!MCzevt?cKUHK}30MLFnoggfi+jXu)}a?Z&? zxfpiMI@|H{?V;diMI^veV9bMULEE}amJ8$OI@{KXu8kr~CEun}3$ zjS7z(9L=BgnLm*ⅆ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 literal 230119 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

    vMnc+fCkFH9{;i6)Z4ljTg}6Nh5^4* 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

    ySo;O)4X!m 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 z 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 Re&DHCigK#5r4OG4{Vyeb+{^#~ From a13f52fbdb1a9832f6a880fdc6efd859c66bd7f1 Mon Sep 17 00:00:00 2001 From: Volodymyr Krasnikov <129072588+volodk85@users.noreply.github.com> Date: Thu, 22 Feb 2024 08:36:45 -0800 Subject: [PATCH 154/250] be code compatible with serverless (#105746) Related to https://github.com/elastic/elasticsearch/pull/105676 --- .../org/elasticsearch/persistent/PersistentTasksService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksService.java b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksService.java index 9fcf34048c030..5d3624238d0ce 100644 --- a/server/src/main/java/org/elasticsearch/persistent/PersistentTasksService.java +++ b/server/src/main/java/org/elasticsearch/persistent/PersistentTasksService.java @@ -156,6 +156,11 @@ void sendUpdateStateRequest( execute(request, UpdatePersistentTaskStatusAction.INSTANCE, listener); } + @Deprecated(forRemoval = true) + public void sendRemoveRequest(final String taskId, final ActionListener> listener) { + sendRemoveRequest(taskId, null, listener); + } + /** * Notifies the master node to remove a persistent task from the cluster state. Accepts operation timeout as optional parameter */ From dbf72e1dad3e7e452c2601a27ee56a90a390ac5a Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Thu, 22 Feb 2024 18:06:18 +0100 Subject: [PATCH 155/250] Reduce InternalVariableWidthHistogram in a streaming fashion (#105748) --- .../InternalVariableWidthHistogram.java | 101 ++++++++---------- 1 file changed, 43 insertions(+), 58 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalVariableWidthHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalVariableWidthHistogram.java index e8201ffb86317..3478773464feb 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalVariableWidthHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalVariableWidthHistogram.java @@ -8,9 +8,11 @@ package org.elasticsearch.search.aggregations.bucket.histogram; -import org.apache.lucene.util.PriorityQueue; +import org.apache.lucene.util.NumericUtils; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.util.LongObjectPagedHashMap; +import org.elasticsearch.core.Releasables; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.AggregationReduceContext; import org.elasticsearch.search.aggregations.AggregatorReducer; @@ -18,7 +20,7 @@ import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; import org.elasticsearch.search.aggregations.KeyComparable; -import org.elasticsearch.search.aggregations.bucket.IteratorAndCurrent; +import org.elasticsearch.search.aggregations.bucket.MultiBucketAggregatorsReducer; import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation; import org.elasticsearch.search.aggregations.support.SamplingContext; import org.elasticsearch.xcontent.XContentBuilder; @@ -324,59 +326,6 @@ private Bucket reduceBucket(List buckets, AggregationReduceContext conte return new Bucket(centroid, bounds, docCount, format, aggs); } - public List reduceBuckets(List aggregations, AggregationReduceContext reduceContext) { - PriorityQueue> pq = new PriorityQueue<>(aggregations.size()) { - @Override - protected boolean lessThan(IteratorAndCurrent a, IteratorAndCurrent b) { - return Double.compare(a.current().centroid, b.current().centroid) < 0; - } - }; - for (InternalVariableWidthHistogram histogram : aggregations) { - if (histogram.buckets.isEmpty() == false) { - pq.add(new IteratorAndCurrent<>(histogram.buckets.iterator())); - } - } - - List reducedBuckets = new ArrayList<>(); - if (pq.size() > 0) { - double key = pq.top().current().centroid(); - // list of buckets coming from different shards that have the same key - List currentBuckets = new ArrayList<>(); - do { - IteratorAndCurrent top = pq.top(); - - if (Double.compare(top.current().centroid(), key) != 0) { - // The key changes, reduce what we already buffered and reset the buffer for current buckets. - final Bucket reduced = reduceBucket(currentBuckets, reduceContext); - reduceContext.consumeBucketsAndMaybeBreak(1); - reducedBuckets.add(reduced); - currentBuckets.clear(); - key = top.current().centroid(); - } - - currentBuckets.add(top.current()); - - if (top.hasNext()) { - Bucket prev = top.current(); - top.next(); - assert top.current().compareKey(prev) >= 0 : "shards must return data sorted by centroid"; - pq.updateTop(); - } else { - pq.pop(); - } - } while (pq.size() > 0); - - if (currentBuckets.isEmpty() == false) { - final Bucket reduced = reduceBucket(currentBuckets, reduceContext); - reduceContext.consumeBucketsAndMaybeBreak(1); - reducedBuckets.add(reduced); - } - } - - mergeBucketsIfNeeded(reducedBuckets, targetNumBuckets, reduceContext); - return reducedBuckets; - } - static class BucketRange { int startIdx; int endIdx; @@ -530,16 +479,40 @@ private static void adjustBoundsForOverlappingBuckets(List buckets) { @Override protected AggregatorReducer getLeaderReducer(AggregationReduceContext reduceContext, int size) { return new AggregatorReducer() { - final List aggregations = new ArrayList<>(size); + + final LongObjectPagedHashMap bucketsReducer = new LongObjectPagedHashMap<>( + getBuckets().size(), + reduceContext.bigArrays() + ); @Override public void accept(InternalAggregation aggregation) { - aggregations.add((InternalVariableWidthHistogram) aggregation); + InternalVariableWidthHistogram histogram = (InternalVariableWidthHistogram) aggregation; + for (Bucket bucket : histogram.getBuckets()) { + long key = NumericUtils.doubleToSortableLong(bucket.centroid()); + ReducerAndExtraInfo reducer = bucketsReducer.get(key); + if (reducer == null) { + reducer = new ReducerAndExtraInfo(new MultiBucketAggregatorsReducer(reduceContext, size)); + bucketsReducer.put(key, reducer); + reduceContext.consumeBucketsAndMaybeBreak(1); + } + reducer.min[0] = Math.min(reducer.min[0], bucket.bounds.min); + reducer.max[0] = Math.max(reducer.max[0], bucket.bounds.max); + reducer.sum[0] += bucket.docCount * bucket.centroid; + reducer.reducer.accept(bucket); + } } @Override public InternalAggregation get() { - final List reducedBuckets = reduceBuckets(aggregations, reduceContext); + final List reducedBuckets = new ArrayList<>((int) bucketsReducer.size()); + bucketsReducer.iterator().forEachRemaining(entry -> { + final double centroid = entry.value.sum[0] / entry.value.reducer.getDocCount(); + final Bucket.BucketBounds bounds = new Bucket.BucketBounds(entry.value.min[0], entry.value.max[0]); + reducedBuckets.add(new Bucket(centroid, bounds, entry.value.reducer.getDocCount(), format, entry.value.reducer.get())); + }); + reducedBuckets.sort(Comparator.comparing(Bucket::centroid)); + mergeBucketsIfNeeded(reducedBuckets, targetNumBuckets, reduceContext); if (reduceContext.isFinalReduce()) { buckets.sort(Comparator.comparing(Bucket::min)); mergeBucketsWithSameMin(reducedBuckets, reduceContext); @@ -547,9 +520,21 @@ public InternalAggregation get() { } return new InternalVariableWidthHistogram(getName(), reducedBuckets, emptyBucketInfo, targetNumBuckets, format, metadata); } + + @Override + public void close() { + bucketsReducer.iterator().forEachRemaining(entry -> Releasables.close(entry.value.reducer)); + Releasables.close(bucketsReducer); + } }; } + private record ReducerAndExtraInfo(MultiBucketAggregatorsReducer reducer, double[] min, double[] max, double[] sum) { + private ReducerAndExtraInfo(MultiBucketAggregatorsReducer reducer) { + this(reducer, new double[] { Double.POSITIVE_INFINITY }, new double[] { Double.NEGATIVE_INFINITY }, new double[] { 0 }); + } + } + @Override public InternalAggregation finalizeSampling(SamplingContext samplingContext) { return new InternalVariableWidthHistogram( From c60706cdd9881d33faa0d8928f3e033453cfd421 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine <58790826+elasticsearchmachine@users.noreply.github.com> Date: Thu, 22 Feb 2024 12:35:12 -0500 Subject: [PATCH 156/250] Forward port release notes for v8.12.2 (#105752) --- docs/reference/release-notes.asciidoc | 2 + docs/reference/release-notes/8.12.2.asciidoc | 58 +++++++++++++++++++ .../release-notes/highlights.asciidoc | 39 +++++++++++-- 3 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 docs/reference/release-notes/8.12.2.asciidoc diff --git a/docs/reference/release-notes.asciidoc b/docs/reference/release-notes.asciidoc index e548459f31216..e3c8da281f2a1 100644 --- a/docs/reference/release-notes.asciidoc +++ b/docs/reference/release-notes.asciidoc @@ -8,6 +8,7 @@ This section summarizes the changes in each release. * <> * <> +* <> * <> * <> * <> @@ -63,6 +64,7 @@ This section summarizes the changes in each release. include::release-notes/8.14.0.asciidoc[] include::release-notes/8.13.0.asciidoc[] +include::release-notes/8.12.2.asciidoc[] include::release-notes/8.12.1.asciidoc[] include::release-notes/8.12.0.asciidoc[] include::release-notes/8.11.4.asciidoc[] diff --git a/docs/reference/release-notes/8.12.2.asciidoc b/docs/reference/release-notes/8.12.2.asciidoc new file mode 100644 index 0000000000000..2be8449b6c1df --- /dev/null +++ b/docs/reference/release-notes/8.12.2.asciidoc @@ -0,0 +1,58 @@ +[[release-notes-8.12.2]] +== {es} version 8.12.2 + +Also see <>. + +[[bug-8.12.2]] +[float] +=== Bug fixes + +Application:: +* Fix bug in `rule_query` where `text_expansion` errored because it was not rewritten {es-pull}105365[#105365] +* [Connectors API] Fix bug with crawler configuration parsing and `sync_now` flag {es-pull}105024[#105024] + +Authentication:: +* Validate settings before reloading JWT shared secret {es-pull}105070[#105070] + +Downsampling:: +* Downsampling better handle if source index isn't allocated and fix bug in retrieving last processed tsid {es-pull}105228[#105228] + +ES|QL:: +* ESQL: Push CIDR_MATCH to Lucene if possible {es-pull}105061[#105061] (issue: {es-issue}105042[#105042]) +* ES|QL: Fix exception handling on `date_parse` with wrong date pattern {es-pull}105048[#105048] (issue: {es-issue}104124[#104124]) + +Indices APIs:: +* Always show `composed_of` field for composable index templates {es-pull}105315[#105315] (issue: {es-issue}104627[#104627]) + +Ingest Node:: +* Backport stable `ThreadPool` constructor from `LogstashInternalBridge` {es-pull}105165[#105165] +* Harden index mapping parameter check in enrich runner {es-pull}105096[#105096] + +Machine Learning:: +* Fix handling of `ml.config_version` node attribute for nodes with machine learning disabled {es-pull}105066[#105066] +* Fix handling surrogate pairs in the XLM Roberta tokenizer {es-pull}105183[#105183] (issues: {es-issue}104626[#104626], {es-issue}104981[#104981]) +* Inference service should reject tasks during shutdown {es-pull}105213[#105213] + +Network:: +* Close `currentChunkedWrite` on client cancel {es-pull}105258[#105258] +* Fix leaked HTTP response sent after close {es-pull}105293[#105293] (issue: {es-issue}104651[#104651]) +* Fix race in HTTP response shutdown handling {es-pull}105306[#105306] + +Search:: +* Field-caps should read fields from up-to-dated shards {es-pull}105153[#105153] (issue: {es-issue}104809[#104809]) + +Snapshot/Restore:: +* Finalize all snapshots completed by shard snapshot updates {es-pull}105245[#105245] (issue: {es-issue}104939[#104939]) + +Transform:: +* Do not log warning when triggering an `ABORTING` transform {es-pull}105234[#105234] (issue: {es-issue}105233[#105233]) +* Make `_reset` action stop transforms without force first {es-pull}104870[#104870] (issues: {es-issue}100596[#100596], {es-issue}104825[#104825]) + +[[enhancement-8.12.2]] +[float] +=== Enhancements + +IdentityProvider:: +* Include user's privileges actions in IdP plugin `_has_privileges` request {es-pull}104026[#104026] + + diff --git a/docs/reference/release-notes/highlights.asciidoc b/docs/reference/release-notes/highlights.asciidoc index a3345c8dc3d74..92cd447a48deb 100644 --- a/docs/reference/release-notes/highlights.asciidoc +++ b/docs/reference/release-notes/highlights.asciidoc @@ -28,13 +28,40 @@ Other versions: endif::[] -// The notable-highlights tag marks entries that -// should be featured in the Stack Installation and Upgrade Guide: // tag::notable-highlights[] -// [discrete] -// === Heading -// -// Description. + +[discrete] +[[improve_storage_efficiency_for_non_metric_fields_in_tsdb]] +=== Improve storage efficiency for non-metric fields in TSDB +Adds a new `doc_values` encoding for non-metric fields in TSDB that takes advantage of TSDB's index sorting. +While terms that are used in multiple documents (such as the host name) are already stored only once in the terms dictionary, +there are a lot of repetitions in the references to the terms dictionary that are stored in `doc_values` (ordinals). +In TSDB, documents (and therefore `doc_values`) are implicitly sorted by dimenstions and timestamp. +This means that for each time series, we are storing long consecutive runs of the same ordinal. +With this change, we are introducing an encoding that detects and efficiently stores runs of the same value (such as `1 1 1 2 2 2 …`), +and runs of cycling values (such as `1 2 1 2 …`). +In our testing, we have seen a reduction in storage size by about 13%. +The effectiveness of this encoding depends on how many non-metric fields, such as dimensions, are used. +The more non-metric fields, the more effective this improvement will be. + +{es-pull}99747[#99747] + +[discrete] +[[ga_release_of_synonyms_api]] +=== GA Release of Synonyms API +Removes the beta label for the Synonyms API to make it GA. + +{es-pull}103223[#103223] + +[discrete] +[[flag_in_field_caps_to_return_only_fields_with_values_in_index]] +=== Flag in `_field_caps` to return only fields with values in index +We added support for filtering the field capabilities API output by removing +fields that don't have a value. This can be done through the newly added +`include_empty_fields` parameter, which defaults to true. + +{es-pull}103651[#103651] + // end::notable-highlights[] From ad65def11a155f95edd2ffc563f93465034461e7 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 22 Feb 2024 17:38:03 +0000 Subject: [PATCH 157/250] Bump versions after 8.12.2 release --- .buildkite/pipelines/intake.yml | 2 +- .buildkite/pipelines/periodic-packaging.yml | 16 ++++++++++++++++ .buildkite/pipelines/periodic.yml | 10 ++++++++++ .ci/bwcVersions | 1 + .ci/snapshotBwcVersions | 2 +- .../src/main/java/org/elasticsearch/Version.java | 1 + .../org/elasticsearch/TransportVersions.csv | 1 + .../org/elasticsearch/index/IndexVersions.csv | 1 + 8 files changed, 32 insertions(+), 2 deletions(-) diff --git a/.buildkite/pipelines/intake.yml b/.buildkite/pipelines/intake.yml index b5981d5ef40f3..3283e691f121c 100644 --- a/.buildkite/pipelines/intake.yml +++ b/.buildkite/pipelines/intake.yml @@ -48,7 +48,7 @@ steps: timeout_in_minutes: 300 matrix: setup: - BWC_VERSION: ["7.17.19", "8.12.2", "8.13.0", "8.14.0"] + BWC_VERSION: ["7.17.19", "8.12.3", "8.13.0", "8.14.0"] agents: provider: gcp image: family/elasticsearch-ubuntu-2004 diff --git a/.buildkite/pipelines/periodic-packaging.yml b/.buildkite/pipelines/periodic-packaging.yml index bb2b4748b36ef..5e7c1a0960789 100644 --- a/.buildkite/pipelines/periodic-packaging.yml +++ b/.buildkite/pipelines/periodic-packaging.yml @@ -1873,6 +1873,22 @@ steps: env: BWC_VERSION: 8.12.2 + - label: "{{matrix.image}} / 8.12.3 / packaging-tests-upgrade" + command: ./.ci/scripts/packaging-test.sh -Dbwc.checkout.align=true destructiveDistroUpgradeTest.v8.12.3 + timeout_in_minutes: 300 + matrix: + setup: + image: + - rocky-8 + - ubuntu-2004 + agents: + provider: gcp + image: family/elasticsearch-{{matrix.image}} + machineType: custom-16-32768 + buildDirectory: /dev/shm/bk + env: + BWC_VERSION: 8.12.3 + - label: "{{matrix.image}} / 8.13.0 / packaging-tests-upgrade" command: ./.ci/scripts/packaging-test.sh -Dbwc.checkout.align=true destructiveDistroUpgradeTest.v8.13.0 timeout_in_minutes: 300 diff --git a/.buildkite/pipelines/periodic.yml b/.buildkite/pipelines/periodic.yml index 141975568d353..bedf559e98ff4 100644 --- a/.buildkite/pipelines/periodic.yml +++ b/.buildkite/pipelines/periodic.yml @@ -1152,6 +1152,16 @@ steps: buildDirectory: /dev/shm/bk env: BWC_VERSION: 8.12.2 + - label: 8.12.3 / bwc + command: .ci/scripts/run-gradle.sh -Dbwc.checkout.align=true v8.12.3#bwcTest + timeout_in_minutes: 300 + agents: + provider: gcp + image: family/elasticsearch-ubuntu-2004 + machineType: n1-standard-32 + buildDirectory: /dev/shm/bk + env: + BWC_VERSION: 8.12.3 - label: 8.13.0 / bwc command: .ci/scripts/run-gradle.sh -Dbwc.checkout.align=true v8.13.0#bwcTest timeout_in_minutes: 300 diff --git a/.ci/bwcVersions b/.ci/bwcVersions index 4c6349a86b800..8b454fa92ab02 100644 --- a/.ci/bwcVersions +++ b/.ci/bwcVersions @@ -114,5 +114,6 @@ BWC_VERSION: - "8.12.0" - "8.12.1" - "8.12.2" + - "8.12.3" - "8.13.0" - "8.14.0" diff --git a/.ci/snapshotBwcVersions b/.ci/snapshotBwcVersions index 96c111fd46948..d85a432684495 100644 --- a/.ci/snapshotBwcVersions +++ b/.ci/snapshotBwcVersions @@ -1,5 +1,5 @@ BWC_VERSION: - "7.17.19" - - "8.12.2" + - "8.12.3" - "8.13.0" - "8.14.0" diff --git a/server/src/main/java/org/elasticsearch/Version.java b/server/src/main/java/org/elasticsearch/Version.java index c1e12faab9cf8..241af6e7b6c45 100644 --- a/server/src/main/java/org/elasticsearch/Version.java +++ b/server/src/main/java/org/elasticsearch/Version.java @@ -166,6 +166,7 @@ public class Version implements VersionId, ToXContentFragment { public static final Version V_8_12_0 = new Version(8_12_00_99); public static final Version V_8_12_1 = new Version(8_12_01_99); public static final Version V_8_12_2 = new Version(8_12_02_99); + public static final Version V_8_12_3 = new Version(8_12_03_99); public static final Version V_8_13_0 = new Version(8_13_00_99); public static final Version V_8_14_0 = new Version(8_14_00_99); public static final Version CURRENT = V_8_14_0; diff --git a/server/src/main/resources/org/elasticsearch/TransportVersions.csv b/server/src/main/resources/org/elasticsearch/TransportVersions.csv index 8efe3b01eefd4..b392111557615 100644 --- a/server/src/main/resources/org/elasticsearch/TransportVersions.csv +++ b/server/src/main/resources/org/elasticsearch/TransportVersions.csv @@ -111,3 +111,4 @@ 8.11.4,8512001 8.12.0,8560000 8.12.1,8560001 +8.12.2,8560001 diff --git a/server/src/main/resources/org/elasticsearch/index/IndexVersions.csv b/server/src/main/resources/org/elasticsearch/index/IndexVersions.csv index 43220565ab871..f2da9fcaf60ce 100644 --- a/server/src/main/resources/org/elasticsearch/index/IndexVersions.csv +++ b/server/src/main/resources/org/elasticsearch/index/IndexVersions.csv @@ -111,3 +111,4 @@ 8.11.4,8500003 8.12.0,8500008 8.12.1,8500010 +8.12.2,8500010 From 8fd6e30be044852a45d500ba36bc9924b318fd82 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 22 Feb 2024 17:43:26 +0000 Subject: [PATCH 158/250] Prune changelogs after 8.12.2 release --- docs/changelog/104026.yaml | 5 ----- docs/changelog/104870.yaml | 7 ------- docs/changelog/105024.yaml | 6 ------ docs/changelog/105048.yaml | 6 ------ docs/changelog/105061.yaml | 6 ------ docs/changelog/105066.yaml | 5 ----- docs/changelog/105070.yaml | 5 ----- docs/changelog/105096.yaml | 5 ----- docs/changelog/105153.yaml | 6 ------ docs/changelog/105183.yaml | 7 ------- docs/changelog/105213.yaml | 5 ----- docs/changelog/105228.yaml | 6 ------ docs/changelog/105234.yaml | 6 ------ docs/changelog/105245.yaml | 6 ------ docs/changelog/105258.yaml | 5 ----- docs/changelog/105293.yaml | 6 ------ docs/changelog/105306.yaml | 5 ----- docs/changelog/105315.yaml | 6 ------ docs/changelog/105365.yaml | 6 ------ 19 files changed, 109 deletions(-) delete mode 100644 docs/changelog/104026.yaml delete mode 100644 docs/changelog/104870.yaml delete mode 100644 docs/changelog/105024.yaml delete mode 100644 docs/changelog/105048.yaml delete mode 100644 docs/changelog/105061.yaml delete mode 100644 docs/changelog/105066.yaml delete mode 100644 docs/changelog/105070.yaml delete mode 100644 docs/changelog/105096.yaml delete mode 100644 docs/changelog/105153.yaml delete mode 100644 docs/changelog/105183.yaml delete mode 100644 docs/changelog/105213.yaml delete mode 100644 docs/changelog/105228.yaml delete mode 100644 docs/changelog/105234.yaml delete mode 100644 docs/changelog/105245.yaml delete mode 100644 docs/changelog/105258.yaml delete mode 100644 docs/changelog/105293.yaml delete mode 100644 docs/changelog/105306.yaml delete mode 100644 docs/changelog/105315.yaml delete mode 100644 docs/changelog/105365.yaml diff --git a/docs/changelog/104026.yaml b/docs/changelog/104026.yaml deleted file mode 100644 index d9aa704de1dbd..0000000000000 --- a/docs/changelog/104026.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 104026 -summary: Include user's privileges actions in IdP plugin `_has_privileges` request -area: IdentityProvider -type: enhancement -issues: [] diff --git a/docs/changelog/104870.yaml b/docs/changelog/104870.yaml deleted file mode 100644 index 65bc9a964eb3e..0000000000000 --- a/docs/changelog/104870.yaml +++ /dev/null @@ -1,7 +0,0 @@ -pr: 104870 -summary: Make `_reset` action stop transforms without force first -area: Transform -type: bug -issues: - - 100596 - - 104825 diff --git a/docs/changelog/105024.yaml b/docs/changelog/105024.yaml deleted file mode 100644 index 96268b78ddf5d..0000000000000 --- a/docs/changelog/105024.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 105024 -summary: "[Connectors API] Fix bug with crawler configuration parsing and `sync_now`\ - \ flag" -area: Application -type: bug -issues: [] diff --git a/docs/changelog/105048.yaml b/docs/changelog/105048.yaml deleted file mode 100644 index d865f447a0a93..0000000000000 --- a/docs/changelog/105048.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 105048 -summary: "ES|QL: Fix exception handling on `date_parse` with wrong date pattern" -area: ES|QL -type: bug -issues: - - 104124 diff --git a/docs/changelog/105061.yaml b/docs/changelog/105061.yaml deleted file mode 100644 index ae8a36183e0e7..0000000000000 --- a/docs/changelog/105061.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 105061 -summary: "ESQL: Push CIDR_MATCH to Lucene if possible" -area: ES|QL -type: bug -issues: - - 105042 diff --git a/docs/changelog/105066.yaml b/docs/changelog/105066.yaml deleted file mode 100644 index 95757a9edaf81..0000000000000 --- a/docs/changelog/105066.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 105066 -summary: Fix handling of `ml.config_version` node attribute for nodes with machine learning disabled -area: Machine Learning -type: bug -issues: [] diff --git a/docs/changelog/105070.yaml b/docs/changelog/105070.yaml deleted file mode 100644 index ff4c115e21eea..0000000000000 --- a/docs/changelog/105070.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 105070 -summary: Validate settings before reloading JWT shared secret -area: Authentication -type: bug -issues: [] diff --git a/docs/changelog/105096.yaml b/docs/changelog/105096.yaml deleted file mode 100644 index bfc72a6277bb1..0000000000000 --- a/docs/changelog/105096.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 105096 -summary: Harden index mapping parameter check in enrich runner -area: Ingest Node -type: bug -issues: [] diff --git a/docs/changelog/105153.yaml b/docs/changelog/105153.yaml deleted file mode 100644 index 6c6b1f995df4b..0000000000000 --- a/docs/changelog/105153.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 105153 -summary: Field-caps should read fields from up-to-dated shards -area: "Search" -type: bug -issues: - - 104809 diff --git a/docs/changelog/105183.yaml b/docs/changelog/105183.yaml deleted file mode 100644 index 04ec159cf02d0..0000000000000 --- a/docs/changelog/105183.yaml +++ /dev/null @@ -1,7 +0,0 @@ -pr: 105183 -summary: Fix handling surrogate pairs in the XLM Roberta tokenizer -area: Machine Learning -type: bug -issues: - - 104626 - - 104981 diff --git a/docs/changelog/105213.yaml b/docs/changelog/105213.yaml deleted file mode 100644 index 40595a8166ef2..0000000000000 --- a/docs/changelog/105213.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 105213 -summary: Inference service should reject tasks during shutdown -area: Machine Learning -type: bug -issues: [] diff --git a/docs/changelog/105228.yaml b/docs/changelog/105228.yaml deleted file mode 100644 index 7526a3caa81d9..0000000000000 --- a/docs/changelog/105228.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 105228 -summary: Downsampling better handle if source index isn't allocated and fix bug in - retrieving last processed tsid -area: Downsampling -type: bug -issues: [] diff --git a/docs/changelog/105234.yaml b/docs/changelog/105234.yaml deleted file mode 100644 index eac54b948d4f6..0000000000000 --- a/docs/changelog/105234.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 105234 -summary: Do not log warning when triggering an `ABORTING` transform -area: Transform -type: bug -issues: - - 105233 diff --git a/docs/changelog/105245.yaml b/docs/changelog/105245.yaml deleted file mode 100644 index f6093f2c7435e..0000000000000 --- a/docs/changelog/105245.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 105245 -summary: Finalize all snapshots completed by shard snapshot updates -area: Snapshot/Restore -type: bug -issues: - - 104939 diff --git a/docs/changelog/105258.yaml b/docs/changelog/105258.yaml deleted file mode 100644 index e31e6ec0de749..0000000000000 --- a/docs/changelog/105258.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 105258 -summary: Close `currentChunkedWrite` on client cancel -area: Network -type: bug -issues: [] diff --git a/docs/changelog/105293.yaml b/docs/changelog/105293.yaml deleted file mode 100644 index 33eb3884a7e53..0000000000000 --- a/docs/changelog/105293.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 105293 -summary: Fix leaked HTTP response sent after close -area: Network -type: bug -issues: - - 104651 diff --git a/docs/changelog/105306.yaml b/docs/changelog/105306.yaml deleted file mode 100644 index 7b75c370901ab..0000000000000 --- a/docs/changelog/105306.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 105306 -summary: Fix race in HTTP response shutdown handling -area: Network -type: bug -issues: [] diff --git a/docs/changelog/105315.yaml b/docs/changelog/105315.yaml deleted file mode 100644 index 207e72467a689..0000000000000 --- a/docs/changelog/105315.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 105315 -summary: Always show `composed_of` field for composable index templates -area: Indices APIs -type: bug -issues: - - 104627 diff --git a/docs/changelog/105365.yaml b/docs/changelog/105365.yaml deleted file mode 100644 index 265e6dccc3915..0000000000000 --- a/docs/changelog/105365.yaml +++ /dev/null @@ -1,6 +0,0 @@ -pr: 105365 -summary: Fix bug in `rule_query` where `text_expansion` errored because it was not - rewritten -area: Application -type: bug -issues: [] From 89c61cde4a1c4003aeacb3740530029f3f5c1504 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 22 Feb 2024 19:24:33 +0100 Subject: [PATCH 159/250] Fix noisy logging from HealthNodeTaskExecutor (#105695) No need to log more than debug here on node shutdown. This causes an incredibly amount of log spam in some tests that frequently restart nodes. --- .../health/node/selection/HealthNodeTaskExecutor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutor.java b/server/src/main/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutor.java index 209bb45891dea..cc908cd7cad2c 100644 --- a/server/src/main/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutor.java +++ b/server/src/main/java/org/elasticsearch/health/node/selection/HealthNodeTaskExecutor.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.features.FeatureService; import org.elasticsearch.health.HealthFeatures; +import org.elasticsearch.node.NodeClosedException; import org.elasticsearch.persistent.AllocatedPersistentTask; import org.elasticsearch.persistent.PersistentTaskParams; import org.elasticsearch.persistent.PersistentTaskState; @@ -166,6 +167,10 @@ void startTask(ClusterChangedEvent event) { new HealthNodeTaskParams(), null, ActionListener.wrap(r -> logger.debug("Created the health node task"), e -> { + if (e instanceof NodeClosedException) { + logger.debug("Failed to create health node task because node is shutting down", e); + return; + } Throwable t = e instanceof RemoteTransportException ? e.getCause() : e; if (t instanceof ResourceAlreadyExistsException == false) { logger.error("Failed to create the health node task", e); From 8090c611ba57d9888fa2f01fd62b8bbd087adadd Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Thu, 22 Feb 2024 16:11:28 -0500 Subject: [PATCH 160/250] PainlessExecute RCS2 test fails on assert in AuthorizationDenialMessages (#105665) When testing the painless/execute API under RCS2, when you query against a remote cluster before any permissions have been granted (in the remote_indices section), an AssertionException was tripped in [the AuthorizationDenialMessages.remoteActionDenied method]. To fix this, we use the SecurityActionMapper.action method in SecurityServerTransportInterceptor.sendWithCrossClusterAccessHeaders. --- ...eClusterSecurityRCS2PainlessExecuteIT.java | 25 +++++++++++++------ .../SecurityServerTransportInterceptor.java | 7 +++++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2PainlessExecuteIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2PainlessExecuteIT.java index b24122c1302fc..2fca49191d51b 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2PainlessExecuteIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityRCS2PainlessExecuteIT.java @@ -179,6 +179,15 @@ public void testPainlessExecute() throws Exception { String responseBody = EntityUtils.toString(response.getEntity()); assertThat(responseBody, equalTo("{\"result\":[\"test\"]}")); } + { + // TEST CASE 2: Query remote cluster for index1 - should fail since no permissions granted for remote clusters yet + Request painlessExecuteRemote = createPainlessExecuteRequest("my_remote_cluster:index1"); + ResponseException exc = expectThrows(ResponseException.class, () -> performRequestWithRemoteSearchUser(painlessExecuteRemote)); + assertThat(exc.getResponse().getStatusLine().getStatusCode(), is(403)); + String errorResponseBody = EntityUtils.toString(exc.getResponse().getEntity()); + assertThat(errorResponseBody, containsString("unauthorized for user [remote_search_user]")); + assertThat(errorResponseBody, containsString("\"type\":\"security_exception\"")); + } { // update role to have permissions to remote index* pattern var updateRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE); @@ -202,7 +211,7 @@ public void testPainlessExecute() throws Exception { assertOK(adminClient().performRequest(updateRoleRequest)); } { - // TEST CASE 2: Query remote cluster for secretindex - should fail since no perms granted for it + // TEST CASE 3: Query remote cluster for secretindex - should fail since no perms granted for it Request painlessExecuteRemote = createPainlessExecuteRequest("my_remote_cluster:secretindex"); ResponseException exc = expectThrows(ResponseException.class, () -> performRequestWithRemoteSearchUser(painlessExecuteRemote)); String errorResponseBody = EntityUtils.toString(exc.getResponse().getEntity()); @@ -212,7 +221,7 @@ public void testPainlessExecute() throws Exception { assertThat(errorResponseBody, containsString("\"type\":\"security_exception\"")); } { - // TEST CASE 3: Query remote cluster for index1 - should succeed since read and cross-cluster-read perms granted + // TEST CASE 4: Query remote cluster for index1 - should succeed since read and cross-cluster-read perms granted Request painlessExecuteRemote = createPainlessExecuteRequest("my_remote_cluster:index1"); Response response = performRequestWithRemoteSearchUser(painlessExecuteRemote); String responseBody = EntityUtils.toString(response.getEntity()); @@ -220,7 +229,7 @@ public void testPainlessExecute() throws Exception { assertThat(responseBody, equalTo("{\"result\":[\"test\"]}")); } { - // TEST CASE 4: Query local cluster for not_present index - should fail with 403 since role does not have perms for this index + // TEST CASE 5: Query local cluster for not_present index - should fail with 403 since role does not have perms for this index Request painlessExecuteLocal = createPainlessExecuteRequest("index_not_present"); ResponseException exc = expectThrows(ResponseException.class, () -> performRequestWithRemoteSearchUser(painlessExecuteLocal)); assertThat(exc.getResponse().getStatusLine().getStatusCode(), is(403)); @@ -230,7 +239,7 @@ public void testPainlessExecute() throws Exception { assertThat(errorResponseBody, containsString("\"type\":\"security_exception\"")); } { - // TEST CASE 5: Query local cluster for my_local_123 index - role has perms for this pattern, but index does not exist, so 404 + // TEST CASE 6: Query local cluster for my_local_123 index - role has perms for this pattern, but index does not exist, so 404 Request painlessExecuteLocal = createPainlessExecuteRequest("my_local_123"); ResponseException exc = expectThrows(ResponseException.class, () -> performRequestWithRemoteSearchUser(painlessExecuteLocal)); assertThat(exc.getResponse().getStatusLine().getStatusCode(), is(404)); @@ -238,7 +247,7 @@ public void testPainlessExecute() throws Exception { assertThat(errorResponseBody, containsString("\"type\":\"index_not_found_exception\"")); } { - // TEST CASE 6: Query local cluster for my_local* index - painless/execute does not allow wildcards, so fails with 400 + // TEST CASE 7: Query local cluster for my_local* index - painless/execute does not allow wildcards, so fails with 400 Request painlessExecuteLocal = createPainlessExecuteRequest("my_local*"); ResponseException exc = expectThrows(ResponseException.class, () -> performRequestWithRemoteSearchUser(painlessExecuteLocal)); assertThat(exc.getResponse().getStatusLine().getStatusCode(), is(400)); @@ -247,7 +256,7 @@ public void testPainlessExecute() throws Exception { assertThat(errorResponseBody, containsString("\"type\":\"illegal_argument_exception\"")); } { - // TEST CASE 7: Query remote cluster for cluster that does not exist, and user does not have perms for that pattern - 403 ??? + // TEST CASE 8: Query remote cluster for cluster that does not exist, and user does not have perms for that pattern - 403 ??? Request painlessExecuteRemote = createPainlessExecuteRequest("my_remote_cluster:abc123"); ResponseException exc = expectThrows(ResponseException.class, () -> performRequestWithRemoteSearchUser(painlessExecuteRemote)); assertThat(exc.getResponse().getStatusLine().getStatusCode(), is(403)); @@ -257,7 +266,7 @@ public void testPainlessExecute() throws Exception { assertThat(errorResponseBody, containsString("\"type\":\"security_exception\"")); } { - // TEST CASE 8: Query remote cluster for cluster that does not exist, but has permissions for the index pattern - 404 + // TEST CASE 9: Query remote cluster for cluster that does not exist, but has permissions for the index pattern - 404 Request painlessExecuteRemote = createPainlessExecuteRequest("my_remote_cluster:index123"); ResponseException exc = expectThrows(ResponseException.class, () -> performRequestWithRemoteSearchUser(painlessExecuteRemote)); assertThat(exc.getResponse().getStatusLine().getStatusCode(), is(404)); @@ -265,7 +274,7 @@ public void testPainlessExecute() throws Exception { assertThat(errorResponseBody, containsString("\"type\":\"index_not_found_exception\"")); } { - // TEST CASE 9: Query remote cluster with wildcard in index - painless/execute does not allow wildcards, so fails with 400 + // TEST CASE 10: Query remote cluster with wildcard in index - painless/execute does not allow wildcards, so fails with 400 Request painlessExecuteRemote = createPainlessExecuteRequest("my_remote_cluster:index*"); ResponseException exc = expectThrows(ResponseException.class, () -> performRequestWithRemoteSearchUser(painlessExecuteRemote)); assertThat(exc.getResponse().getStatusLine().getStatusCode(), is(400)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java index 162cabf5297ce..ca08f63a09bb0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java @@ -47,6 +47,7 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.security.Security; +import org.elasticsearch.xpack.security.action.SecurityActionMapper; import org.elasticsearch.xpack.security.audit.AuditUtil; import org.elasticsearch.xpack.security.authc.ApiKeyService; import org.elasticsearch.xpack.security.authc.AuthenticationService; @@ -385,7 +386,11 @@ private void sendWithCrossClusterAccessHeaders( ) ); if (roleDescriptorsIntersection.isEmpty()) { - throw authzService.remoteActionDenied(authentication, action, remoteClusterAlias); + throw authzService.remoteActionDenied( + authentication, + SecurityActionMapper.action(action, request), + remoteClusterAlias + ); } final var crossClusterAccessHeaders = new CrossClusterAccessHeaders( remoteClusterCredentials.credentials(), From f0e43173eac4549ed78375b991ebd61c0cc13d2e Mon Sep 17 00:00:00 2001 From: David Roberts Date: Thu, 22 Feb 2024 21:18:21 +0000 Subject: [PATCH 161/250] [ML] Fix DataFrameAnalyticsConfigProviderIT.testUpdate_UpdateCannotBeAppliedWhenTaskIsRunning (#105754) The test failed 1 in 32 times because if the value used in the update matched the random value used in the original config created by DataFrameAnalyticsConfigTests.createRandom then the update would be a no-op and no exception would be thrown. Fixes #58814 --- .../ml/integration/DataFrameAnalyticsConfigProviderIT.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/DataFrameAnalyticsConfigProviderIT.java b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/DataFrameAnalyticsConfigProviderIT.java index 3b84c5d86c00c..e29cd4545846c 100644 --- a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/DataFrameAnalyticsConfigProviderIT.java +++ b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/DataFrameAnalyticsConfigProviderIT.java @@ -332,7 +332,6 @@ public void testUpdate_ConfigDoesNotExist() throws InterruptedException { assertThat(exceptionHolder.get(), is(instanceOf(ResourceNotFoundException.class))); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/58814") public void testUpdate_UpdateCannotBeAppliedWhenTaskIsRunning() throws InterruptedException { String configId = "config-id"; DataFrameAnalyticsConfig initialConfig = DataFrameAnalyticsConfigTests.createRandom(configId); @@ -354,8 +353,10 @@ public void testUpdate_UpdateCannotBeAppliedWhenTaskIsRunning() throws Interrupt AtomicReference updatedConfigHolder = new AtomicReference<>(); AtomicReference exceptionHolder = new AtomicReference<>(); + // Important: the new value specified here must be one that it's impossible for DataFrameAnalyticsConfigTests.createRandom + // to have used originally. If the update is a no-op then the test fails. DataFrameAnalyticsConfigUpdate configUpdate = new DataFrameAnalyticsConfigUpdate.Builder(configId).setModelMemoryLimit( - ByteSizeValue.ofMb(2048) + ByteSizeValue.ofMb(1234) ).build(); ClusterState clusterState = clusterStateWithRunningAnalyticsTask(configId, DataFrameAnalyticsState.ANALYZING); From 92c2b36e6998d4da8ca18a97e9893bbcd31bc38b Mon Sep 17 00:00:00 2001 From: Athena Brown Date: Thu, 22 Feb 2024 17:31:03 -0700 Subject: [PATCH 162/250] Decouple enrollment token version from node version (#104018) This commit decouples the version used in enrollment tokens from node version, as part of the larger effort to make versioning more granular. The changes are relatively minimal, as the version encoded into enrollment tokens is not actually used anywhere as far as I can tell, either in Elasticsearch or Kibana, apart from checks that it is present. That said, I've been around the block enough times to know better than to remove a perfectly good version field that's already in something like this. --- .../test/EnrollNodeToClusterTests.java | 2 -- ...ackagesSecurityAutoConfigurationTests.java | 2 -- .../xpack/core/security/EnrollmentToken.java | 19 +++++++---- .../ExternalEnrollmentTokenGenerator.java | 2 +- .../InternalEnrollmentTokenGenerator.java | 3 +- ...ExternalEnrollmentTokenGeneratorTests.java | 5 +-- ...InternalEnrollmentTokenGeneratorTests.java | 5 ++- .../tool/CreateEnrollmentTokenToolTests.java | 33 ++++++++++--------- 8 files changed, 37 insertions(+), 34 deletions(-) diff --git a/qa/packaging/src/test/java/org/elasticsearch/packaging/test/EnrollNodeToClusterTests.java b/qa/packaging/src/test/java/org/elasticsearch/packaging/test/EnrollNodeToClusterTests.java index 3ca61ccacae17..eac86cf7aac23 100644 --- a/qa/packaging/src/test/java/org/elasticsearch/packaging/test/EnrollNodeToClusterTests.java +++ b/qa/packaging/src/test/java/org/elasticsearch/packaging/test/EnrollNodeToClusterTests.java @@ -8,7 +8,6 @@ package org.elasticsearch.packaging.test; -import org.elasticsearch.Version; import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.packaging.util.Archives; import org.elasticsearch.packaging.util.Distribution; @@ -93,7 +92,6 @@ private String generateMockEnrollmentToken() throws Exception { EnrollmentToken enrollmentToken = new EnrollmentToken( "some-api-key", "e8864fa9cb5a8053ea84a48581a6c9bef619f8f6aaa58a632aac3e0a25d43ea9", - Version.CURRENT.toString(), List.of("localhost:9200") ); return enrollmentToken.getEncoded(); diff --git a/qa/packaging/src/test/java/org/elasticsearch/packaging/test/PackagesSecurityAutoConfigurationTests.java b/qa/packaging/src/test/java/org/elasticsearch/packaging/test/PackagesSecurityAutoConfigurationTests.java index fa68da1725edc..9dab7e5eb8d16 100644 --- a/qa/packaging/src/test/java/org/elasticsearch/packaging/test/PackagesSecurityAutoConfigurationTests.java +++ b/qa/packaging/src/test/java/org/elasticsearch/packaging/test/PackagesSecurityAutoConfigurationTests.java @@ -8,7 +8,6 @@ package org.elasticsearch.packaging.test; -import org.elasticsearch.Version; import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.common.Strings; import org.elasticsearch.common.ssl.PemKeyConfig; @@ -300,7 +299,6 @@ public void test73ReconfigureCreatesFilesWithCorrectPermissions() throws Excepti final EnrollmentToken enrollmentToken = new EnrollmentToken( "some-api-key", "b0150fd8a29f9012207912de9a01aa1d1f0dd696c847d3a9353881f9045bf442", // fingerprint of http_ca.crt - Version.CURRENT.toString(), List.of(mockNode.getHostName() + ":" + mockNode.getPort()) ); Shell.Result result = installation.executables().nodeReconfigureTool.run( diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/EnrollmentToken.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/EnrollmentToken.java index 466caa11771a5..fb1d178250aa3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/EnrollmentToken.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/EnrollmentToken.java @@ -24,6 +24,10 @@ import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg; public class EnrollmentToken { + // This was previously a version string, e.g. 8.12.0, but treated exclusively as a string everywhere, never parsed into a version. + // Arbitrarily set to 9 when decoupling this from node version. + public static final String CURRENT_TOKEN_VERSION = "8.14.0"; + private final String apiKey; private final String fingerprint; private final String version; @@ -64,19 +68,22 @@ public List getBoundAddress() { PARSER.declareStringArray(constructorArg(), ADDRESS); } + EnrollmentToken(String apiKey, String fingerprint, String version, List boundAddress) { + this.apiKey = Objects.requireNonNull(apiKey); + this.fingerprint = Objects.requireNonNull(fingerprint); + this.version = Objects.requireNonNull(version); + this.boundAddress = Objects.requireNonNull(boundAddress); + } + /** * Create an EnrollmentToken * * @param apiKey API Key credential in the form apiKeyId:ApiKeySecret to be used for enroll calls * @param fingerprint hex encoded SHA256 fingerprint of the HTTP CA cert - * @param version node version number * @param boundAddress IP Addresses and port numbers for the interfaces where the Elasticsearch node is listening on */ - public EnrollmentToken(String apiKey, String fingerprint, String version, List boundAddress) { - this.apiKey = Objects.requireNonNull(apiKey); - this.fingerprint = Objects.requireNonNull(fingerprint); - this.version = Objects.requireNonNull(version); - this.boundAddress = Objects.requireNonNull(boundAddress); + public EnrollmentToken(String apiKey, String fingerprint, List boundAddress) { + this(apiKey, fingerprint, EnrollmentToken.CURRENT_TOKEN_VERSION, boundAddress); } public String getRaw() throws Exception { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGenerator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGenerator.java index c884435cdd04b..6d780adf49acb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGenerator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGenerator.java @@ -72,7 +72,7 @@ protected EnrollmentToken create(String user, SecureString password, String acti final String fingerprint = getHttpsCaFingerprint(sslService); final String apiKey = getApiKeyCredentials(user, password, action, baseUrl); final Tuple, String> httpInfo = getNodeInfo(user, password, baseUrl); - return new EnrollmentToken(apiKey, fingerprint, httpInfo.v2(), httpInfo.v1()); + return new EnrollmentToken(apiKey, fingerprint, httpInfo.v1()); } private static HttpResponse.HttpResponseBuilder responseBuilder(InputStream is) throws IOException { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/InternalEnrollmentTokenGenerator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/InternalEnrollmentTokenGenerator.java index 446dfa7e7e310..ff973ce4319f6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/InternalEnrollmentTokenGenerator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/InternalEnrollmentTokenGenerator.java @@ -9,7 +9,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoMetrics; @@ -198,7 +197,7 @@ private void assembleToken(EnrollmentTokenType enrollTokenType, HttpInfo httpInf apiKeyRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); client.execute(CreateApiKeyAction.INSTANCE, apiKeyRequest, ActionListener.wrap(createApiKeyResponse -> { final String apiKey = createApiKeyResponse.getId() + ":" + createApiKeyResponse.getKey().toString(); - final EnrollmentToken enrollmentToken = new EnrollmentToken(apiKey, fingerprint, Version.CURRENT.toString(), tokenAddresses); + final EnrollmentToken enrollmentToken = new EnrollmentToken(apiKey, fingerprint, tokenAddresses); consumer.accept(enrollmentToken); }, e -> { LOGGER.error("Failed to create enrollment token when generating API key", e); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGeneratorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGeneratorTests.java index 669f67d80c1f8..e46c05d9c9683 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGeneratorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGeneratorTests.java @@ -19,6 +19,7 @@ import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xcontent.json.JsonXContent; import org.elasticsearch.xpack.core.security.CommandLineHttpClient; +import org.elasticsearch.xpack.core.security.EnrollmentToken; import org.elasticsearch.xpack.core.security.HttpResponse; import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.hamcrest.Matchers; @@ -152,7 +153,7 @@ public void testCreateSuccess() throws Exception { ).getEncoded(); Map infoNode = getDecoded(tokenNode); - assertEquals("8.0.0", infoNode.get("ver")); + assertEquals(EnrollmentToken.CURRENT_TOKEN_VERSION, infoNode.get("ver")); assertEquals("[192.168.0.1:9201, 172.16.254.1:9202, [2001:db8:0:1234:0:567:8:1]:9203]", infoNode.get("adr")); assertEquals("ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d", infoNode.get("fgr")); assertEquals("DR6CzXkBDf8amV_48yYX:x3YqU_rqQwm-ESrkExcnOg", infoNode.get("key")); @@ -164,7 +165,7 @@ public void testCreateSuccess() throws Exception { ).getEncoded(); Map infoKibana = getDecoded(tokenKibana); - assertEquals("8.0.0", infoKibana.get("ver")); + assertEquals(EnrollmentToken.CURRENT_TOKEN_VERSION, infoKibana.get("ver")); assertEquals("[192.168.0.1:9201, 172.16.254.1:9202, [2001:db8:0:1234:0:567:8:1]:9203]", infoKibana.get("adr")); assertEquals("ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d", infoKibana.get("fgr")); assertEquals("DR6CzXkBDf8amV_48yYX:x3YqU_rqQwm-ESrkExcnOg", infoKibana.get("key")); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/InternalEnrollmentTokenGeneratorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/InternalEnrollmentTokenGeneratorTests.java index 3a4e5a404eace..888483613a187 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/InternalEnrollmentTokenGeneratorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/InternalEnrollmentTokenGeneratorTests.java @@ -9,7 +9,6 @@ import org.elasticsearch.Build; import org.elasticsearch.TransportVersion; -import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; @@ -155,7 +154,7 @@ public void testCreationSuccess() { assertThat(token.getApiKey(), equalTo("api-key-id:api-key-secret")); assertThat(token.getBoundAddress().size(), equalTo(1)); assertThat(token.getBoundAddress().get(0), equalTo("192.168.1.2:9200")); - assertThat(token.getVersion(), equalTo(Version.CURRENT.toString())); + assertThat(token.getVersion(), equalTo(EnrollmentToken.CURRENT_TOKEN_VERSION)); assertThat(token.getFingerprint(), equalTo("ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d")); } @@ -209,7 +208,7 @@ public void testRetryToGetNodesHttpInfo() { assertThat(token.getApiKey(), equalTo("api-key-id:api-key-secret")); assertThat(token.getBoundAddress().size(), equalTo(1)); assertThat(token.getBoundAddress().get(0), equalTo("192.168.1.2:9200")); - assertThat(token.getVersion(), equalTo(Version.CURRENT.toString())); + assertThat(token.getVersion(), equalTo(EnrollmentToken.CURRENT_TOKEN_VERSION)); assertThat(token.getFingerprint(), equalTo("ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d")); } diff --git a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java index 27affc69f5fbc..e86e709a662c6 100644 --- a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java +++ b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java @@ -11,6 +11,7 @@ import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; +import com.unboundid.util.Base64; import org.elasticsearch.cli.Command; import org.elasticsearch.cli.CommandTestCase; @@ -57,6 +58,8 @@ @SuppressWarnings("unchecked") public class CreateEnrollmentTokenToolTests extends CommandTestCase { + private static final String KIBANA_API_KEY = "DR6CzXkBDf8amV_48yYX:x3YqU_rqQwm-ESrkExcnOg"; + private static final String NODE_API_KEY = "DR6CzXkBDf8amV_48yYX:4BhUk-mkFm-AwvRFg90KJ"; static FileSystem jimfs; String pathHomeParameter; @@ -126,15 +129,13 @@ public void setup() throws Exception { this.externalEnrollmentTokenGenerator = mock(ExternalEnrollmentTokenGenerator.class); EnrollmentToken kibanaToken = new EnrollmentToken( - "DR6CzXkBDf8amV_48yYX:x3YqU_rqQwm-ESrkExcnOg", + KIBANA_API_KEY, "ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d", - "8.0.0", Arrays.asList("[192.168.0.1:9201, 172.16.254.1:9202") ); EnrollmentToken nodeToken = new EnrollmentToken( - "DR6CzXkBDf8amV_48yYX:4BhUk-mkFm-AwvRFg90KJ", + NODE_API_KEY, "ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d", - "8.0.0", Arrays.asList("[192.168.0.1:9201, 172.16.254.1:9202") ); when(externalEnrollmentTokenGenerator.createKibanaEnrollmentToken(anyString(), any(SecureString.class), any(URL.class))).thenReturn( @@ -153,14 +154,15 @@ public static void closeJimfs() throws IOException { } } - public void testCreateToken() throws Exception { - String scope = randomBoolean() ? "node" : "kibana"; - String output = execute("--scope", scope); - if (scope.equals("kibana")) { - assertThat(output, containsString("1WXzQ4eVlYOngzWXFVX3JxUXdtLUVTcmtFeGNuT2cifQ==")); - } else { - assertThat(output, containsString("4YW1WXzQ4eVlYOjRCaFVrLW1rRm0tQXd2UkZnOTBLSiJ9")); - } + public void testCreateKibanaToken() throws Exception { + String kibanaToken = Base64.decodeToString(execute("--scope", "kibana").trim()); + assertThat(kibanaToken, containsString(KIBANA_API_KEY)); + + } + + public void testCreateNodeToken() throws Exception { + String nodeToken = Base64.decodeToString(execute("--scope", "node").trim()); + assertThat(nodeToken, containsString(NODE_API_KEY)); } public void testInvalidScope() throws Exception { @@ -189,7 +191,6 @@ public void testUserCanPassUrl() throws Exception { EnrollmentToken kibanaToken = new EnrollmentToken( "DR6CzXkBDf8amV_48yYX:x3YqU_rqQwm-ESrkExcnOg", "ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d", - "8.0.0", Arrays.asList("[192.168.0.1:9201, 172.16.254.1:9202") ); when( @@ -200,7 +201,7 @@ public void testUserCanPassUrl() throws Exception { ) ).thenReturn(kibanaToken); String output = execute("--scope", "kibana", "--url", "http://localhost:9204"); - assertThat(output, containsString("1WXzQ4eVlYOngzWXFVX3JxUXdtLUVTcmtFeGNuT2cifQ==")); + assertThat(Base64.decodeToString(output.trim()), containsString(KIBANA_API_KEY)); } @@ -227,9 +228,9 @@ public void testUnhealthyClusterWithForce() throws Exception { String scope = randomBoolean() ? "node" : "kibana"; String output = execute("--scope", scope); if (scope.equals("kibana")) { - assertThat(output, containsString("1WXzQ4eVlYOngzWXFVX3JxUXdtLUVTcmtFeGNuT2cifQ==")); + assertThat(Base64.decodeToString(output.trim()), containsString(KIBANA_API_KEY)); } else { - assertThat(output, containsString("4YW1WXzQ4eVlYOjRCaFVrLW1rRm0tQXd2UkZnOTBLSiJ9")); + assertThat(Base64.decodeToString(output.trim()), containsString(NODE_API_KEY)); } } From b95cb8c40dc701cbce18308cdcd5538fd60facb9 Mon Sep 17 00:00:00 2001 From: David Turner Date: Fri, 23 Feb 2024 06:51:38 +0000 Subject: [PATCH 163/250] Introduce `TransportGetSnapshotsAction#GetSnapshotsOperation` (#105609) This commit introduces a class to encapsulate each invocation of `TransportGetSnapshotsAction`, avoiding the need to pass around a huge list of parameters between all the methods that make up the implementation. --- .../get/TransportGetSnapshotsAction.java | 983 +++++++++--------- 1 file changed, 465 insertions(+), 518 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java index ca910a8d94078..4996096492354 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java @@ -109,598 +109,514 @@ protected void masterOperation( ) { assert task instanceof CancellableTask : task + " not cancellable"; - getMultipleReposSnapshotInfo( - request.isSingleRepositoryRequest() == false, - SnapshotsInProgress.get(state), + new GetSnapshotsOperation( + (CancellableTask) task, TransportGetRepositoriesAction.getRepositories(state, request.repositories()), + request.isSingleRepositoryRequest() == false, request.snapshots(), request.ignoreUnavailable(), - request.verbose(), - (CancellableTask) task, + SnapshotPredicates.fromRequest(request), request.sort(), - request.after(), - request.offset(), - request.size(), request.order(), request.fromSortValue(), - SnapshotPredicates.fromRequest(request), - request.includeIndexNames(), - listener - ); + request.offset(), + request.after(), + request.size(), + SnapshotsInProgress.get(state), + request.verbose(), + request.includeIndexNames() + ).getMultipleReposSnapshotInfo(listener); } /** - * Filters the list of repositories that a request will fetch snapshots from in the special case of sorting by repository - * name and having a non-null value for {@link GetSnapshotsRequest#fromSortValue()} on the request to exclude repositories outside - * the sort value range if possible. + * A single invocation of the get-snapshots API. + *

    + * Decides which repositories to query, picks a collection of candidate {@link SnapshotId} values from each {@link RepositoryData}, + * chosen according to the request parameters, loads the relevant {@link SnapshotInfo} blobs, and finally sorts and filters the + * results. */ - private static List maybeFilterRepositories( - List repositories, - GetSnapshotsRequest.SortBy sortBy, - SortOrder order, - @Nullable String fromSortValue - ) { - if (sortBy != GetSnapshotsRequest.SortBy.REPOSITORY || fromSortValue == null) { - return repositories; - } - final Predicate predicate = order == SortOrder.ASC - ? repositoryMetadata -> fromSortValue.compareTo(repositoryMetadata.name()) <= 0 - : repositoryMetadata -> fromSortValue.compareTo(repositoryMetadata.name()) >= 0; - return repositories.stream().filter(predicate).toList(); - } - - private void getMultipleReposSnapshotInfo( - boolean isMultiRepoRequest, - SnapshotsInProgress snapshotsInProgress, - TransportGetRepositoriesAction.RepositoriesResult repositoriesResult, - String[] snapshots, - boolean ignoreUnavailable, - boolean verbose, - CancellableTask cancellableTask, - GetSnapshotsRequest.SortBy sortBy, - @Nullable GetSnapshotsRequest.After after, - int offset, - int size, - SortOrder order, - String fromSortValue, - SnapshotPredicates predicates, - boolean indices, - ActionListener listener - ) { - // Process the missing repositories - final Map failures = ConcurrentCollections.newConcurrentMap(); - for (String missingRepo : repositoriesResult.missing()) { - failures.put(missingRepo, new RepositoryMissingException(missingRepo)); + private class GetSnapshotsOperation { + private final CancellableTask cancellableTask; + + // repositories + private final List repositories; + private final boolean isMultiRepoRequest; + + // snapshots selection + private final String[] snapshots; + private final boolean ignoreUnavailable; + private final SnapshotPredicates predicates; + + // snapshot ordering/pagination + private final GetSnapshotsRequest.SortBy sortBy; + private final SortOrder order; + @Nullable + private final String fromSortValue; + private final int offset; + @Nullable + private final GetSnapshotsRequest.After after; + private final int size; + + // current state + private final SnapshotsInProgress snapshotsInProgress; + + // output detail + private final boolean verbose; + private final boolean indices; + + // results + private final Map failuresByRepository = ConcurrentCollections.newConcurrentMap(); + private final Queue> allSnapshotInfos = ConcurrentCollections.newQueue(); + private final AtomicInteger remaining = new AtomicInteger(); + private final AtomicInteger totalCount = new AtomicInteger(); + + GetSnapshotsOperation( + CancellableTask cancellableTask, + TransportGetRepositoriesAction.RepositoriesResult repositoriesResult, + boolean isMultiRepoRequest, + String[] snapshots, + boolean ignoreUnavailable, + SnapshotPredicates predicates, + GetSnapshotsRequest.SortBy sortBy, + SortOrder order, + String fromSortValue, + int offset, + GetSnapshotsRequest.After after, + int size, + SnapshotsInProgress snapshotsInProgress, + boolean verbose, + boolean indices + ) { + this.cancellableTask = cancellableTask; + this.repositories = repositoriesResult.metadata(); + this.isMultiRepoRequest = isMultiRepoRequest; + this.snapshots = snapshots; + this.ignoreUnavailable = ignoreUnavailable; + this.predicates = predicates; + this.sortBy = sortBy; + this.order = order; + this.fromSortValue = fromSortValue; + this.offset = offset; + this.after = after; + this.size = size; + this.snapshotsInProgress = snapshotsInProgress; + this.verbose = verbose; + this.indices = indices; + + for (final var missingRepo : repositoriesResult.missing()) { + failuresByRepository.put(missingRepo, new RepositoryMissingException(missingRepo)); + } } - final Queue> allSnapshotInfos = ConcurrentCollections.newQueue(); - final var remaining = new AtomicInteger(); - final var totalCount = new AtomicInteger(); - - List repositories = maybeFilterRepositories(repositoriesResult.metadata(), sortBy, order, fromSortValue); - try (var listeners = new RefCountingListener(listener.map(ignored -> { - cancellableTask.ensureNotCancelled(); - final var sortedSnapshotsInRepos = sortSnapshots( - allSnapshotInfos.stream().flatMap(Collection::stream), - totalCount.get(), - sortBy, - after, - offset, - size, - order - ); - final var snapshotInfos = sortedSnapshotsInRepos.snapshotInfos(); - assert indices || snapshotInfos.stream().allMatch(snapshotInfo -> snapshotInfo.indices().isEmpty()); - final int finalRemaining = sortedSnapshotsInRepos.remaining() + remaining.get(); - return new GetSnapshotsResponse( - snapshotInfos, - failures, - finalRemaining > 0 - ? GetSnapshotsRequest.After.from(snapshotInfos.get(snapshotInfos.size() - 1), sortBy).asQueryParam() - : null, - totalCount.get(), - finalRemaining - ); - }))) { - for (final RepositoryMetadata repository : repositories) { - final String repoName = repository.name(); - getSingleRepoSnapshotInfo( - snapshotsInProgress, - repoName, - snapshots, - predicates, - ignoreUnavailable, - verbose, - cancellableTask, - sortBy, - after, - order, - indices, - listeners.acquire((SnapshotsInRepo snapshotsInRepo) -> { + /** + * Filters the list of repositories that a request will fetch snapshots from in the special case of sorting by repository + * name and having a non-null value for {@link GetSnapshotsRequest#fromSortValue()} on the request to exclude repositories outside + * the sort value range if possible. + */ + private List maybeFilterRepositories() { + if (sortBy != GetSnapshotsRequest.SortBy.REPOSITORY || fromSortValue == null) { + return repositories; + } + final Predicate predicate = order == SortOrder.ASC + ? repositoryMetadata -> fromSortValue.compareTo(repositoryMetadata.name()) <= 0 + : repositoryMetadata -> fromSortValue.compareTo(repositoryMetadata.name()) >= 0; + return repositories.stream().filter(predicate).toList(); + } + + void getMultipleReposSnapshotInfo(ActionListener listener) { + List filteredRepositories = maybeFilterRepositories(); + try (var listeners = new RefCountingListener(listener.map(ignored -> { + cancellableTask.ensureNotCancelled(); + final var sortedSnapshotsInRepos = sortSnapshots( + allSnapshotInfos.stream().flatMap(Collection::stream), + totalCount.get(), + offset, + size + ); + final var snapshotInfos = sortedSnapshotsInRepos.snapshotInfos(); + assert indices || snapshotInfos.stream().allMatch(snapshotInfo -> snapshotInfo.indices().isEmpty()); + final int finalRemaining = sortedSnapshotsInRepos.remaining() + remaining.get(); + return new GetSnapshotsResponse( + snapshotInfos, + failuresByRepository, + finalRemaining > 0 + ? GetSnapshotsRequest.After.from(snapshotInfos.get(snapshotInfos.size() - 1), sortBy).asQueryParam() + : null, + totalCount.get(), + finalRemaining + ); + }))) { + for (final RepositoryMetadata repository : filteredRepositories) { + final String repoName = repository.name(); + getSingleRepoSnapshotInfo(repoName, listeners.acquire((SnapshotsInRepo snapshotsInRepo) -> { allSnapshotInfos.add(snapshotsInRepo.snapshotInfos()); remaining.addAndGet(snapshotsInRepo.remaining()); totalCount.addAndGet(snapshotsInRepo.totalCount()); }).delegateResponse((l, e) -> { if (isMultiRepoRequest && e instanceof ElasticsearchException elasticsearchException) { - failures.put(repoName, elasticsearchException); + failuresByRepository.put(repoName, elasticsearchException); l.onResponse(SnapshotsInRepo.EMPTY); } else { l.onFailure(e); } - }) - ); + })); + } } } - } - private void getSingleRepoSnapshotInfo( - SnapshotsInProgress snapshotsInProgress, - String repo, - String[] snapshots, - SnapshotPredicates predicates, - boolean ignoreUnavailable, - boolean verbose, - CancellableTask task, - GetSnapshotsRequest.SortBy sortBy, - @Nullable final GetSnapshotsRequest.After after, - SortOrder order, - boolean indices, - ActionListener listener - ) { - final Map allSnapshotIds = new HashMap<>(); - final List currentSnapshots = new ArrayList<>(); - for (SnapshotInfo snapshotInfo : currentSnapshots(snapshotsInProgress, repo)) { - Snapshot snapshot = snapshotInfo.snapshot(); - allSnapshotIds.put(snapshot.getSnapshotId().getName(), snapshot); - currentSnapshots.add(snapshotInfo.maybeWithoutIndices(indices)); - } + private void getSingleRepoSnapshotInfo(String repo, ActionListener listener) { + final Map allSnapshotIds = new HashMap<>(); + final List currentSnapshots = new ArrayList<>(); + for (final SnapshotInfo snapshotInfo : currentSnapshots(repo)) { + Snapshot snapshot = snapshotInfo.snapshot(); + allSnapshotIds.put(snapshot.getSnapshotId().getName(), snapshot); + currentSnapshots.add(snapshotInfo.maybeWithoutIndices(indices)); + } - final ListenableFuture repositoryDataListener = new ListenableFuture<>(); - if (isCurrentSnapshotsOnly(snapshots)) { - repositoryDataListener.onResponse(null); - } else { - repositoriesService.getRepositoryData(repo, repositoryDataListener); - } + final ListenableFuture repositoryDataListener = new ListenableFuture<>(); + if (isCurrentSnapshotsOnly()) { + repositoryDataListener.onResponse(null); + } else { + repositoriesService.getRepositoryData(repo, repositoryDataListener); + } - repositoryDataListener.addListener( - listener.delegateFailureAndWrap( - (l, repositoryData) -> loadSnapshotInfos( - snapshotsInProgress, - repo, - snapshots, - ignoreUnavailable, - verbose, - allSnapshotIds, - currentSnapshots, - repositoryData, - task, - sortBy, - after, - order, - predicates, - indices, - l + repositoryDataListener.addListener( + listener.delegateFailureAndWrap( + (l, repositoryData) -> loadSnapshotInfos(repo, allSnapshotIds, currentSnapshots, repositoryData, l) ) - ) - ); - } - - /** - * Returns a list of currently running snapshots from repository sorted by snapshot creation date - * - * @param snapshotsInProgress snapshots in progress in the cluster state - * @param repositoryName repository name - * @return list of snapshots - */ - private static List currentSnapshots(SnapshotsInProgress snapshotsInProgress, String repositoryName) { - List snapshotList = new ArrayList<>(); - List entries = SnapshotsService.currentSnapshots( - snapshotsInProgress, - repositoryName, - Collections.emptyList() - ); - for (SnapshotsInProgress.Entry entry : entries) { - snapshotList.add(SnapshotInfo.inProgress(entry)); + ); } - return snapshotList; - } - private void loadSnapshotInfos( - SnapshotsInProgress snapshotsInProgress, - String repo, - String[] snapshots, - boolean ignoreUnavailable, - boolean verbose, - Map allSnapshotIds, - List currentSnapshots, - @Nullable RepositoryData repositoryData, - CancellableTask task, - GetSnapshotsRequest.SortBy sortBy, - @Nullable final GetSnapshotsRequest.After after, - SortOrder order, - SnapshotPredicates predicates, - boolean indices, - ActionListener listener - ) { - if (task.notifyIfCancelled(listener)) { - return; + /** + * Returns a list of currently running snapshots from repository sorted by snapshot creation date + * + * @param repositoryName repository name + * @return list of snapshots + */ + private List currentSnapshots(String repositoryName) { + List snapshotList = new ArrayList<>(); + List entries = SnapshotsService.currentSnapshots( + snapshotsInProgress, + repositoryName, + Collections.emptyList() + ); + for (SnapshotsInProgress.Entry entry : entries) { + snapshotList.add(SnapshotInfo.inProgress(entry)); + } + return snapshotList; } - if (repositoryData != null) { - for (SnapshotId snapshotId : repositoryData.getSnapshotIds()) { - if (predicates.test(snapshotId, repositoryData)) { - allSnapshotIds.put(snapshotId.getName(), new Snapshot(repo, snapshotId)); + private void loadSnapshotInfos( + String repo, + Map allSnapshotIds, + List currentSnapshots, + @Nullable RepositoryData repositoryData, + ActionListener listener + ) { + if (cancellableTask.notifyIfCancelled(listener)) { + return; + } + + if (repositoryData != null) { + for (SnapshotId snapshotId : repositoryData.getSnapshotIds()) { + if (predicates.test(snapshotId, repositoryData)) { + allSnapshotIds.put(snapshotId.getName(), new Snapshot(repo, snapshotId)); + } } } - } - final Set toResolve = new HashSet<>(); - if (TransportGetRepositoriesAction.isMatchAll(snapshots)) { - toResolve.addAll(allSnapshotIds.values()); - } else { - final List includePatterns = new ArrayList<>(); - final List excludePatterns = new ArrayList<>(); - boolean hasCurrent = false; - boolean seenWildcard = false; - for (String snapshotOrPattern : snapshots) { - if (seenWildcard && snapshotOrPattern.length() > 1 && snapshotOrPattern.startsWith("-")) { - excludePatterns.add(snapshotOrPattern.substring(1)); - } else { - if (Regex.isSimpleMatchPattern(snapshotOrPattern)) { - seenWildcard = true; - includePatterns.add(snapshotOrPattern); - } else if (GetSnapshotsRequest.CURRENT_SNAPSHOT.equalsIgnoreCase(snapshotOrPattern)) { - hasCurrent = true; - seenWildcard = true; + final Set toResolve = new HashSet<>(); + if (TransportGetRepositoriesAction.isMatchAll(snapshots)) { + toResolve.addAll(allSnapshotIds.values()); + } else { + final List includePatterns = new ArrayList<>(); + final List excludePatterns = new ArrayList<>(); + boolean hasCurrent = false; + boolean seenWildcard = false; + for (String snapshotOrPattern : snapshots) { + if (seenWildcard && snapshotOrPattern.length() > 1 && snapshotOrPattern.startsWith("-")) { + excludePatterns.add(snapshotOrPattern.substring(1)); } else { - if (ignoreUnavailable == false && allSnapshotIds.containsKey(snapshotOrPattern) == false) { - throw new SnapshotMissingException(repo, snapshotOrPattern); + if (Regex.isSimpleMatchPattern(snapshotOrPattern)) { + seenWildcard = true; + includePatterns.add(snapshotOrPattern); + } else if (GetSnapshotsRequest.CURRENT_SNAPSHOT.equalsIgnoreCase(snapshotOrPattern)) { + hasCurrent = true; + seenWildcard = true; + } else { + if (ignoreUnavailable == false && allSnapshotIds.containsKey(snapshotOrPattern) == false) { + throw new SnapshotMissingException(repo, snapshotOrPattern); + } + includePatterns.add(snapshotOrPattern); } - includePatterns.add(snapshotOrPattern); } } - } - final String[] includes = includePatterns.toArray(Strings.EMPTY_ARRAY); - final String[] excludes = excludePatterns.toArray(Strings.EMPTY_ARRAY); - for (Map.Entry entry : allSnapshotIds.entrySet()) { - final Snapshot snapshot = entry.getValue(); - if (toResolve.contains(snapshot) == false - && Regex.simpleMatch(includes, entry.getKey()) - && Regex.simpleMatch(excludes, entry.getKey()) == false) { - toResolve.add(snapshot); - } - } - if (hasCurrent) { - for (SnapshotInfo snapshotInfo : currentSnapshots) { - final Snapshot snapshot = snapshotInfo.snapshot(); - if (Regex.simpleMatch(excludes, snapshot.getSnapshotId().getName()) == false) { + final String[] includes = includePatterns.toArray(Strings.EMPTY_ARRAY); + final String[] excludes = excludePatterns.toArray(Strings.EMPTY_ARRAY); + for (Map.Entry entry : allSnapshotIds.entrySet()) { + final Snapshot snapshot = entry.getValue(); + if (toResolve.contains(snapshot) == false + && Regex.simpleMatch(includes, entry.getKey()) + && Regex.simpleMatch(excludes, entry.getKey()) == false) { toResolve.add(snapshot); } } + if (hasCurrent) { + for (SnapshotInfo snapshotInfo : currentSnapshots) { + final Snapshot snapshot = snapshotInfo.snapshot(); + if (Regex.simpleMatch(excludes, snapshot.getSnapshotId().getName()) == false) { + toResolve.add(snapshot); + } + } + } + if (toResolve.isEmpty() && ignoreUnavailable == false && isCurrentSnapshotsOnly() == false) { + throw new SnapshotMissingException(repo, snapshots[0]); + } } - if (toResolve.isEmpty() && ignoreUnavailable == false && isCurrentSnapshotsOnly(snapshots) == false) { - throw new SnapshotMissingException(repo, snapshots[0]); + + if (verbose) { + snapshots(repo, toResolve.stream().map(Snapshot::getSnapshotId).toList(), listener); + } else { + assert predicates.isMatchAll() : "filtering is not supported in non-verbose mode"; + final SnapshotsInRepo snapshotInfos; + if (repositoryData != null) { + // want non-current snapshots as well, which are found in the repository data + snapshotInfos = buildSimpleSnapshotInfos(toResolve, repo, repositoryData, currentSnapshots); + } else { + // only want current snapshots + snapshotInfos = sortSnapshotsWithNoOffsetOrLimit(currentSnapshots.stream().map(SnapshotInfo::basic).toList()); + } + listener.onResponse(snapshotInfos); } } - if (verbose) { - snapshots( + /** + * Returns a list of snapshots from repository sorted by snapshot creation date + * + * @param repositoryName repository name + * @param snapshotIds snapshots for which to fetch snapshot information + */ + private void snapshots(String repositoryName, Collection snapshotIds, ActionListener listener) { + if (cancellableTask.notifyIfCancelled(listener)) { + return; + } + final Set snapshotSet = new HashSet<>(); + final Set snapshotIdsToIterate = new HashSet<>(snapshotIds); + // first, look at the snapshots in progress + final List entries = SnapshotsService.currentSnapshots( snapshotsInProgress, - repo, - toResolve.stream().map(Snapshot::getSnapshotId).toList(), - ignoreUnavailable, - task, - sortBy, - after, - order, - predicates, - indices, - listener + repositoryName, + snapshotIdsToIterate.stream().map(SnapshotId::getName).toList() ); - } else { - assert predicates.isMatchAll() : "filtering is not supported in non-verbose mode"; - final SnapshotsInRepo snapshotInfos; - if (repositoryData != null) { - // want non-current snapshots as well, which are found in the repository data - snapshotInfos = buildSimpleSnapshotInfos(toResolve, repo, repositoryData, currentSnapshots, sortBy, after, order, indices); + for (SnapshotsInProgress.Entry entry : entries) { + if (snapshotIdsToIterate.remove(entry.snapshot().getSnapshotId())) { + final SnapshotInfo snapshotInfo = SnapshotInfo.inProgress(entry); + if (predicates.test(snapshotInfo)) { + snapshotSet.add(snapshotInfo.maybeWithoutIndices(indices)); + } + } + } + // then, look in the repository if there's any matching snapshots left + final List snapshotInfos; + if (snapshotIdsToIterate.isEmpty()) { + snapshotInfos = Collections.emptyList(); } else { - // only want current snapshots - snapshotInfos = sortSnapshots( - currentSnapshots.stream().map(SnapshotInfo::basic).toList(), - sortBy, - after, - 0, - GetSnapshotsRequest.NO_LIMIT, - order - ); + snapshotInfos = Collections.synchronizedList(new ArrayList<>()); } - listener.onResponse(snapshotInfos); - } - } - - /** - * Returns a list of snapshots from repository sorted by snapshot creation date - * - * @param snapshotsInProgress snapshots in progress in the cluster state - * @param repositoryName repository name - * @param snapshotIds snapshots for which to fetch snapshot information - * @param ignoreUnavailable if true, snapshots that could not be read will only be logged with a warning, - * @param indices if false, drop the list of indices from each result - */ - private void snapshots( - SnapshotsInProgress snapshotsInProgress, - String repositoryName, - Collection snapshotIds, - boolean ignoreUnavailable, - CancellableTask task, - GetSnapshotsRequest.SortBy sortBy, - @Nullable GetSnapshotsRequest.After after, - SortOrder order, - SnapshotPredicates predicate, - boolean indices, - ActionListener listener - ) { - if (task.notifyIfCancelled(listener)) { - return; - } - final Set snapshotSet = new HashSet<>(); - final Set snapshotIdsToIterate = new HashSet<>(snapshotIds); - // first, look at the snapshots in progress - final List entries = SnapshotsService.currentSnapshots( - snapshotsInProgress, - repositoryName, - snapshotIdsToIterate.stream().map(SnapshotId::getName).toList() - ); - for (SnapshotsInProgress.Entry entry : entries) { - if (snapshotIdsToIterate.remove(entry.snapshot().getSnapshotId())) { - final SnapshotInfo snapshotInfo = SnapshotInfo.inProgress(entry); - if (predicate.test(snapshotInfo)) { - snapshotSet.add(snapshotInfo.maybeWithoutIndices(indices)); - } + final ActionListener allDoneListener = listener.safeMap(v -> { + final ArrayList snapshotList = new ArrayList<>(snapshotInfos); + snapshotList.addAll(snapshotSet); + return sortSnapshotsWithNoOffsetOrLimit(snapshotList); + }); + if (snapshotIdsToIterate.isEmpty()) { + allDoneListener.onResponse(null); + return; } + final Repository repository; + try { + repository = repositoriesService.repository(repositoryName); + } catch (RepositoryMissingException e) { + listener.onFailure(e); + return; + } + repository.getSnapshotInfo( + new GetSnapshotInfoContext( + snapshotIdsToIterate, + ignoreUnavailable == false, + cancellableTask::isCancelled, + (context, snapshotInfo) -> { + if (predicates.test(snapshotInfo)) { + snapshotInfos.add(snapshotInfo.maybeWithoutIndices(indices)); + } + }, + allDoneListener + ) + ); } - // then, look in the repository if there's any matching snapshots left - final List snapshotInfos; - if (snapshotIdsToIterate.isEmpty()) { - snapshotInfos = Collections.emptyList(); - } else { - snapshotInfos = Collections.synchronizedList(new ArrayList<>()); - } - final ActionListener allDoneListener = listener.safeMap(v -> { - final ArrayList snapshotList = new ArrayList<>(snapshotInfos); - snapshotList.addAll(snapshotSet); - return sortSnapshots(snapshotList, sortBy, after, 0, GetSnapshotsRequest.NO_LIMIT, order); - }); - if (snapshotIdsToIterate.isEmpty()) { - allDoneListener.onResponse(null); - return; - } - final Repository repository; - try { - repository = repositoriesService.repository(repositoryName); - } catch (RepositoryMissingException e) { - listener.onFailure(e); - return; - } - repository.getSnapshotInfo( - new GetSnapshotInfoContext(snapshotIdsToIterate, ignoreUnavailable == false, task::isCancelled, (context, snapshotInfo) -> { - if (predicate.test(snapshotInfo)) { - snapshotInfos.add(snapshotInfo.maybeWithoutIndices(indices)); - } - }, allDoneListener) - ); - } - private static boolean isCurrentSnapshotsOnly(String[] snapshots) { - return (snapshots.length == 1 && GetSnapshotsRequest.CURRENT_SNAPSHOT.equalsIgnoreCase(snapshots[0])); - } + private boolean isCurrentSnapshotsOnly() { + return snapshots.length == 1 && GetSnapshotsRequest.CURRENT_SNAPSHOT.equalsIgnoreCase(snapshots[0]); + } - private static SnapshotsInRepo buildSimpleSnapshotInfos( - final Set toResolve, - final String repoName, - final RepositoryData repositoryData, - final List currentSnapshots, - final GetSnapshotsRequest.SortBy sortBy, - @Nullable final GetSnapshotsRequest.After after, - final SortOrder order, - boolean indices - ) { - List snapshotInfos = new ArrayList<>(); - for (SnapshotInfo snapshotInfo : currentSnapshots) { - if (toResolve.remove(snapshotInfo.snapshot())) { - snapshotInfos.add(snapshotInfo.basic()); + private SnapshotsInRepo buildSimpleSnapshotInfos( + final Set toResolve, + final String repoName, + final RepositoryData repositoryData, + final List currentSnapshots + ) { + List snapshotInfos = new ArrayList<>(); + for (SnapshotInfo snapshotInfo : currentSnapshots) { + if (toResolve.remove(snapshotInfo.snapshot())) { + snapshotInfos.add(snapshotInfo.basic()); + } } - } - Map> snapshotsToIndices = new HashMap<>(); - if (indices) { - for (IndexId indexId : repositoryData.getIndices().values()) { - for (SnapshotId snapshotId : repositoryData.getSnapshots(indexId)) { - if (toResolve.contains(new Snapshot(repoName, snapshotId))) { - snapshotsToIndices.computeIfAbsent(snapshotId, (k) -> new ArrayList<>()).add(indexId.getName()); + Map> snapshotsToIndices = new HashMap<>(); + if (indices) { + for (IndexId indexId : repositoryData.getIndices().values()) { + for (SnapshotId snapshotId : repositoryData.getSnapshots(indexId)) { + if (toResolve.contains(new Snapshot(repoName, snapshotId))) { + snapshotsToIndices.computeIfAbsent(snapshotId, (k) -> new ArrayList<>()).add(indexId.getName()); + } } } } + for (Snapshot snapshot : toResolve) { + snapshotInfos.add( + new SnapshotInfo( + snapshot, + snapshotsToIndices.getOrDefault(snapshot.getSnapshotId(), Collections.emptyList()), + Collections.emptyList(), + Collections.emptyList(), + repositoryData.getSnapshotState(snapshot.getSnapshotId()) + ) + ); + } + return sortSnapshotsWithNoOffsetOrLimit(snapshotInfos); } - for (Snapshot snapshot : toResolve) { - snapshotInfos.add( - new SnapshotInfo( - snapshot, - snapshotsToIndices.getOrDefault(snapshot.getSnapshotId(), Collections.emptyList()), - Collections.emptyList(), - Collections.emptyList(), - repositoryData.getSnapshotState(snapshot.getSnapshotId()) - ) - ); - } - return sortSnapshots(snapshotInfos, sortBy, after, 0, GetSnapshotsRequest.NO_LIMIT, order); - } - private static final Comparator BY_START_TIME = Comparator.comparingLong(SnapshotInfo::startTime) - .thenComparing(SnapshotInfo::snapshotId); + private static final Comparator BY_START_TIME = Comparator.comparingLong(SnapshotInfo::startTime) + .thenComparing(SnapshotInfo::snapshotId); - private static final Comparator BY_DURATION = Comparator.comparingLong( - sni -> sni.endTime() - sni.startTime() - ).thenComparing(SnapshotInfo::snapshotId); + private static final Comparator BY_DURATION = Comparator.comparingLong( + sni -> sni.endTime() - sni.startTime() + ).thenComparing(SnapshotInfo::snapshotId); - private static final Comparator BY_INDICES_COUNT = Comparator.comparingInt(sni -> sni.indices().size()) - .thenComparing(SnapshotInfo::snapshotId); + private static final Comparator BY_INDICES_COUNT = Comparator.comparingInt(sni -> sni.indices().size()) + .thenComparing(SnapshotInfo::snapshotId); - private static final Comparator BY_SHARDS_COUNT = Comparator.comparingInt(SnapshotInfo::totalShards) - .thenComparing(SnapshotInfo::snapshotId); + private static final Comparator BY_SHARDS_COUNT = Comparator.comparingInt(SnapshotInfo::totalShards) + .thenComparing(SnapshotInfo::snapshotId); - private static final Comparator BY_FAILED_SHARDS_COUNT = Comparator.comparingInt(SnapshotInfo::failedShards) - .thenComparing(SnapshotInfo::snapshotId); + private static final Comparator BY_FAILED_SHARDS_COUNT = Comparator.comparingInt(SnapshotInfo::failedShards) + .thenComparing(SnapshotInfo::snapshotId); - private static final Comparator BY_NAME = Comparator.comparing(sni -> sni.snapshotId().getName()); + private static final Comparator BY_NAME = Comparator.comparing(sni -> sni.snapshotId().getName()); - private static final Comparator BY_REPOSITORY = Comparator.comparing(SnapshotInfo::repository) - .thenComparing(SnapshotInfo::snapshotId); + private static final Comparator BY_REPOSITORY = Comparator.comparing(SnapshotInfo::repository) + .thenComparing(SnapshotInfo::snapshotId); - private static long getDuration(SnapshotId snapshotId, RepositoryData repositoryData) { - final RepositoryData.SnapshotDetails details = repositoryData.getSnapshotDetails(snapshotId); - if (details == null) { - return -1; - } - final long startTime = details.getStartTimeMillis(); - if (startTime == -1) { - return -1; + private SnapshotsInRepo sortSnapshotsWithNoOffsetOrLimit(List snapshotInfos) { + return sortSnapshots(snapshotInfos.stream(), snapshotInfos.size(), 0, GetSnapshotsRequest.NO_LIMIT); } - final long endTime = details.getEndTimeMillis(); - if (endTime == -1) { - return -1; - } - return endTime - startTime; - } - private static long getStartTime(SnapshotId snapshotId, RepositoryData repositoryData) { - final RepositoryData.SnapshotDetails details = repositoryData.getSnapshotDetails(snapshotId); - return details == null ? -1 : details.getStartTimeMillis(); - } + private SnapshotsInRepo sortSnapshots(Stream infos, int totalCount, int offset, int size) { + final Comparator comparator = switch (sortBy) { + case START_TIME -> BY_START_TIME; + case NAME -> BY_NAME; + case DURATION -> BY_DURATION; + case INDICES -> BY_INDICES_COUNT; + case SHARDS -> BY_SHARDS_COUNT; + case FAILED_SHARDS -> BY_FAILED_SHARDS_COUNT; + case REPOSITORY -> BY_REPOSITORY; + }; - private static int indexCount(SnapshotId snapshotId, RepositoryData repositoryData) { - // TODO: this could be made more efficient by caching this number in RepositoryData - int indexCount = 0; - for (IndexId idx : repositoryData.getIndices().values()) { - if (repositoryData.getSnapshots(idx).contains(snapshotId)) { - indexCount++; + if (after != null) { + assert offset == 0 : "can't combine after and offset but saw [" + after + "] and offset [" + offset + "]"; + infos = infos.filter(buildAfterPredicate()); } + infos = infos.sorted(order == SortOrder.DESC ? comparator.reversed() : comparator).skip(offset); + final List allSnapshots = infos.toList(); + final List snapshots; + if (size != GetSnapshotsRequest.NO_LIMIT) { + snapshots = allSnapshots.stream().limit(size + 1).toList(); + } else { + snapshots = allSnapshots; + } + final List resultSet = size != GetSnapshotsRequest.NO_LIMIT && size < snapshots.size() + ? snapshots.subList(0, size) + : snapshots; + return new SnapshotsInRepo(resultSet, totalCount, allSnapshots.size() - resultSet.size()); + } + + private Predicate buildAfterPredicate() { + final String snapshotName = after.snapshotName(); + final String repoName = after.repoName(); + final String value = after.value(); + return switch (sortBy) { + case START_TIME -> filterByLongOffset(SnapshotInfo::startTime, Long.parseLong(value), snapshotName, repoName, order); + case NAME -> + // TODO: cover via pre-flight predicate + order == SortOrder.ASC + ? (info -> compareName(snapshotName, repoName, info) < 0) + : (info -> compareName(snapshotName, repoName, info) > 0); + case DURATION -> filterByLongOffset( + info -> info.endTime() - info.startTime(), + Long.parseLong(value), + snapshotName, + repoName, + order + ); + case INDICES -> + // TODO: cover via pre-flight predicate + filterByLongOffset(info -> info.indices().size(), Integer.parseInt(value), snapshotName, repoName, order); + case SHARDS -> filterByLongOffset(SnapshotInfo::totalShards, Integer.parseInt(value), snapshotName, repoName, order); + case FAILED_SHARDS -> filterByLongOffset( + SnapshotInfo::failedShards, + Integer.parseInt(value), + snapshotName, + repoName, + order + ); + case REPOSITORY -> + // TODO: cover via pre-flight predicate + order == SortOrder.ASC + ? (info -> compareRepositoryName(snapshotName, repoName, info) < 0) + : (info -> compareRepositoryName(snapshotName, repoName, info) > 0); + }; + } + + private static Predicate filterByLongOffset( + ToLongFunction extractor, + long after, + String snapshotName, + String repoName, + SortOrder order + ) { + return order == SortOrder.ASC ? info -> { + final long val = extractor.applyAsLong(info); + return after < val || (after == val && compareName(snapshotName, repoName, info) < 0); + } : info -> { + final long val = extractor.applyAsLong(info); + return after > val || (after == val && compareName(snapshotName, repoName, info) > 0); + }; + } + + private static int compareRepositoryName(String name, String repoName, SnapshotInfo info) { + final int res = repoName.compareTo(info.repository()); + if (res != 0) { + return res; + } + return name.compareTo(info.snapshotId().getName()); } - return indexCount; - } - - private static SnapshotsInRepo sortSnapshots( - List snapshotInfos, - GetSnapshotsRequest.SortBy sortBy, - @Nullable GetSnapshotsRequest.After after, - int offset, - int size, - SortOrder order - ) { - return sortSnapshots(snapshotInfos.stream(), snapshotInfos.size(), sortBy, after, offset, size, order); - } - - private static SnapshotsInRepo sortSnapshots( - Stream infos, - int totalCount, - GetSnapshotsRequest.SortBy sortBy, - @Nullable GetSnapshotsRequest.After after, - int offset, - int size, - SortOrder order - ) { - final Comparator comparator = switch (sortBy) { - case START_TIME -> BY_START_TIME; - case NAME -> BY_NAME; - case DURATION -> BY_DURATION; - case INDICES -> BY_INDICES_COUNT; - case SHARDS -> BY_SHARDS_COUNT; - case FAILED_SHARDS -> BY_FAILED_SHARDS_COUNT; - case REPOSITORY -> BY_REPOSITORY; - }; - - if (after != null) { - assert offset == 0 : "can't combine after and offset but saw [" + after + "] and offset [" + offset + "]"; - infos = infos.filter(buildAfterPredicate(sortBy, after, order)); - } - infos = infos.sorted(order == SortOrder.DESC ? comparator.reversed() : comparator).skip(offset); - final List allSnapshots = infos.toList(); - final List snapshots; - if (size != GetSnapshotsRequest.NO_LIMIT) { - snapshots = allSnapshots.stream().limit(size + 1).toList(); - } else { - snapshots = allSnapshots; - } - final List resultSet = size != GetSnapshotsRequest.NO_LIMIT && size < snapshots.size() - ? snapshots.subList(0, size) - : snapshots; - return new SnapshotsInRepo(resultSet, totalCount, allSnapshots.size() - resultSet.size()); - } - private static Predicate buildAfterPredicate( - GetSnapshotsRequest.SortBy sortBy, - GetSnapshotsRequest.After after, - SortOrder order - ) { - final String snapshotName = after.snapshotName(); - final String repoName = after.repoName(); - final String value = after.value(); - return switch (sortBy) { - case START_TIME -> filterByLongOffset(SnapshotInfo::startTime, Long.parseLong(value), snapshotName, repoName, order); - case NAME -> - // TODO: cover via pre-flight predicate - order == SortOrder.ASC - ? (info -> compareName(snapshotName, repoName, info) < 0) - : (info -> compareName(snapshotName, repoName, info) > 0); - case DURATION -> filterByLongOffset( - info -> info.endTime() - info.startTime(), - Long.parseLong(value), - snapshotName, - repoName, - order - ); - case INDICES -> - // TODO: cover via pre-flight predicate - filterByLongOffset(info -> info.indices().size(), Integer.parseInt(value), snapshotName, repoName, order); - case SHARDS -> filterByLongOffset(SnapshotInfo::totalShards, Integer.parseInt(value), snapshotName, repoName, order); - case FAILED_SHARDS -> filterByLongOffset(SnapshotInfo::failedShards, Integer.parseInt(value), snapshotName, repoName, order); - case REPOSITORY -> - // TODO: cover via pre-flight predicate - order == SortOrder.ASC - ? (info -> compareRepositoryName(snapshotName, repoName, info) < 0) - : (info -> compareRepositoryName(snapshotName, repoName, info) > 0); - }; - } - - private static Predicate filterByLongOffset( - ToLongFunction extractor, - long after, - String snapshotName, - String repoName, - SortOrder order - ) { - return order == SortOrder.ASC ? info -> { - final long val = extractor.applyAsLong(info); - return after < val || (after == val && compareName(snapshotName, repoName, info) < 0); - } : info -> { - final long val = extractor.applyAsLong(info); - return after > val || (after == val && compareName(snapshotName, repoName, info) > 0); - }; - } - - private static int compareRepositoryName(String name, String repoName, SnapshotInfo info) { - final int res = repoName.compareTo(info.repository()); - if (res != 0) { - return res; + private static int compareName(String name, String repoName, SnapshotInfo info) { + final int res = name.compareTo(info.snapshotId().getName()); + if (res != 0) { + return res; + } + return repoName.compareTo(info.repository()); } - return name.compareTo(info.snapshotId().getName()); - } - private static int compareName(String name, String repoName, SnapshotInfo info) { - final int res = name.compareTo(info.snapshotId().getName()); - if (res != 0) { - return res; - } - return repoName.compareTo(info.repository()); } /** @@ -881,6 +797,37 @@ private static Predicate filterByLongOffset(ToLongFunction after <= extractor.applyAsLong(info) : info -> after >= extractor.applyAsLong(info); } + private static long getDuration(SnapshotId snapshotId, RepositoryData repositoryData) { + final RepositoryData.SnapshotDetails details = repositoryData.getSnapshotDetails(snapshotId); + if (details == null) { + return -1; + } + final long startTime = details.getStartTimeMillis(); + if (startTime == -1) { + return -1; + } + final long endTime = details.getEndTimeMillis(); + if (endTime == -1) { + return -1; + } + return endTime - startTime; + } + + private static long getStartTime(SnapshotId snapshotId, RepositoryData repositoryData) { + final RepositoryData.SnapshotDetails details = repositoryData.getSnapshotDetails(snapshotId); + return details == null ? -1 : details.getStartTimeMillis(); + } + + private static int indexCount(SnapshotId snapshotId, RepositoryData repositoryData) { + // TODO: this could be made more efficient by caching this number in RepositoryData + int indexCount = 0; + for (IndexId idx : repositoryData.getIndices().values()) { + if (repositoryData.getSnapshots(idx).contains(snapshotId)) { + indexCount++; + } + } + return indexCount; + } } private record SnapshotsInRepo(List snapshotInfos, int totalCount, int remaining) { From 2ba37ffc38b4964079f06a2ebdbee95017103700 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Fri, 23 Feb 2024 07:54:41 +0100 Subject: [PATCH 164/250] Reduce InternalAdjacencyMatrix in a streaming fashion (#105751) --- .../adjacency/InternalAdjacencyMatrix.java | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/adjacency/InternalAdjacencyMatrix.java b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/adjacency/InternalAdjacencyMatrix.java index 8802ffd41571d..6e70e9263df47 100644 --- a/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/adjacency/InternalAdjacencyMatrix.java +++ b/modules/aggregations/src/main/java/org/elasticsearch/aggregations/bucket/adjacency/InternalAdjacencyMatrix.java @@ -11,11 +11,13 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.core.Releasables; import org.elasticsearch.search.aggregations.AggregationReduceContext; import org.elasticsearch.search.aggregations.AggregatorReducer; import org.elasticsearch.search.aggregations.InternalAggregation; import org.elasticsearch.search.aggregations.InternalAggregations; import org.elasticsearch.search.aggregations.InternalMultiBucketAggregation; +import org.elasticsearch.search.aggregations.bucket.MultiBucketAggregatorsReducer; import org.elasticsearch.search.aggregations.support.SamplingContext; import org.elasticsearch.xcontent.XContentBuilder; @@ -177,30 +179,38 @@ public InternalBucket getBucketByKey(String key) { @Override protected AggregatorReducer getLeaderReducer(AggregationReduceContext reduceContext, int size) { - Map> bucketsMap = new HashMap<>(); return new AggregatorReducer() { + final Map bucketsReducer = new HashMap<>(getBuckets().size()); + @Override public void accept(InternalAggregation aggregation) { - InternalAdjacencyMatrix filters = (InternalAdjacencyMatrix) aggregation; + final InternalAdjacencyMatrix filters = (InternalAdjacencyMatrix) aggregation; for (InternalBucket bucket : filters.buckets) { - List sameRangeList = bucketsMap.computeIfAbsent(bucket.key, k -> new ArrayList<>(size)); - sameRangeList.add(bucket); + MultiBucketAggregatorsReducer reducer = bucketsReducer.computeIfAbsent( + bucket.key, + k -> new MultiBucketAggregatorsReducer(reduceContext, size) + ); + reducer.accept(bucket); } } @Override public InternalAggregation get() { - List reducedBuckets = new ArrayList<>(bucketsMap.size()); - for (List sameRangeList : bucketsMap.values()) { - InternalBucket reducedBucket = reduceBucket(sameRangeList, reduceContext); - if (reducedBucket.docCount >= 1) { - reducedBuckets.add(reducedBucket); + List reducedBuckets = new ArrayList<>(bucketsReducer.size()); + for (Map.Entry entry : bucketsReducer.entrySet()) { + if (entry.getValue().getDocCount() >= 1) { + reducedBuckets.add(new InternalBucket(entry.getKey(), entry.getValue().getDocCount(), entry.getValue().get())); } } reduceContext.consumeBucketsAndMaybeBreak(reducedBuckets.size()); reducedBuckets.sort(Comparator.comparing(InternalBucket::getKey)); return new InternalAdjacencyMatrix(name, reducedBuckets, getMetadata()); } + + @Override + public void close() { + Releasables.close(bucketsReducer.values()); + } }; } @@ -209,21 +219,6 @@ public InternalAggregation finalizeSampling(SamplingContext samplingContext) { return new InternalAdjacencyMatrix(name, buckets.stream().map(b -> b.finalizeSampling(samplingContext)).toList(), getMetadata()); } - private InternalBucket reduceBucket(List buckets, AggregationReduceContext context) { - assert buckets.isEmpty() == false; - InternalBucket reduced = null; - for (InternalBucket bucket : buckets) { - if (reduced == null) { - reduced = new InternalBucket(bucket.key, bucket.docCount, bucket.aggregations); - } else { - reduced.docCount += bucket.docCount; - } - } - final List aggregations = new BucketAggregationList<>(buckets); - reduced.aggregations = InternalAggregations.reduce(aggregations, context); - return reduced; - } - @Override public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { builder.startArray(CommonFields.BUCKETS.getPreferredName()); From ae4e57d7461dca73d3c5c32873a3de5bdb46f75b Mon Sep 17 00:00:00 2001 From: Tim Grein Date: Fri, 23 Feb 2024 09:46:57 +0100 Subject: [PATCH 165/250] [Connectors API] Add tests for ConnectorStateMachine (#105736) --- .../connector/ConnectorStateMachine.java | 7 +++++ .../connector/ConnectorStateMachineTests.java | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorStateMachine.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorStateMachine.java index 39a12ba334c30..f722955cc0f9e 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorStateMachine.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorStateMachine.java @@ -42,6 +42,13 @@ public static boolean isValidTransition(ConnectorStatus current, ConnectorStatus return validNextStates(current).contains(next); } + /** + * Throws {@link ConnectorInvalidStatusTransitionException} if a + * transition from one {@link ConnectorStatus} to another is invalid. + * + * @param current The current {@link ConnectorStatus} of the {@link Connector}. + * @param next The proposed next {@link ConnectorStatus} of the {@link Connector}. + */ public static void assertValidStateTransition(ConnectorStatus current, ConnectorStatus next) throws ConnectorInvalidStatusTransitionException { if (isValidTransition(current, next)) return; diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorStateMachineTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorStateMachineTests.java index 372c874310162..d1f08f80d02f2 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorStateMachineTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/ConnectorStateMachineTests.java @@ -65,4 +65,31 @@ public void testTransitionToSameState() { assertFalse("Transition from " + state + " to itself should be invalid", ConnectorStateMachine.isValidTransition(state, state)); } } + + public void testAssertValidStateTransition_ExpectExceptionOnInvalidTransition() { + assertThrows( + ConnectorInvalidStatusTransitionException.class, + () -> ConnectorStateMachine.assertValidStateTransition(ConnectorStatus.CREATED, ConnectorStatus.CONFIGURED) + ); + } + + public void testAssertValidStateTransition_ExpectNoExceptionOnValidTransition() { + ConnectorStatus prevStatus = ConnectorStatus.CREATED; + ConnectorStatus nextStatus = ConnectorStatus.ERROR; + + try { + ConnectorStateMachine.assertValidStateTransition(prevStatus, nextStatus); + } catch (ConnectorInvalidStatusTransitionException e) { + fail( + "Did not expect " + + ConnectorInvalidStatusTransitionException.class.getSimpleName() + + " to be thrown for valid state transition [" + + prevStatus + + "] -> " + + "[" + + nextStatus + + "]." + ); + } + } } From 56716694b0977863fb2f23303b40cf967673c1ef Mon Sep 17 00:00:00 2001 From: Tim Grein Date: Fri, 23 Feb 2024 09:47:18 +0100 Subject: [PATCH 166/250] [Connectors API] Remove unused method (#105739) --- .../connector/syncjob/ConnectorSyncJobIndexService.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexService.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexService.java index b1c08d8b7fbb1..910f0605ef7aa 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexService.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexService.java @@ -24,7 +24,6 @@ import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.internal.Client; -import org.elasticsearch.common.UUIDs; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.engine.DocumentMissingException; import org.elasticsearch.index.query.BoolQueryBuilder; @@ -416,14 +415,6 @@ public void updateConnectorSyncJobIngestionStats( } - private String generateId() { - /* Workaround: only needed for generating an id upfront, autoGenerateId() has a side effect generating a timestamp, - * which would raise an error on the response layer later ("autoGeneratedTimestamp should not be set externally"). - * TODO: do we even need to copy the "_id" and set it as "id"? - */ - return UUIDs.base64UUID(); - } - private void getSyncJobConnectorInfo(String connectorId, ActionListener listener) { try { From 21b64ba2e492317224bef560d52a0bffb39a7cb1 Mon Sep 17 00:00:00 2001 From: Tim Grein Date: Fri, 23 Feb 2024 09:47:40 +0100 Subject: [PATCH 167/250] [Connectors API] Make default scheduling for all sync jobs more readable (#105755) --- .../connector/ConnectorScheduling.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorScheduling.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorScheduling.java index 233bea5d4a842..637957b8ce66e 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorScheduling.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/ConnectorScheduling.java @@ -30,14 +30,15 @@ public class ConnectorScheduling implements Writeable, ToXContentObject { - private final ScheduleConfig accessControl; - private final ScheduleConfig full; - private final ScheduleConfig incremental; - + private static final String EVERYDAY_AT_MIDNIGHT = "0 0 0 * * ?"; private static final ParseField ACCESS_CONTROL_FIELD = new ParseField("access_control"); private static final ParseField FULL_FIELD = new ParseField("full"); private static final ParseField INCREMENTAL_FIELD = new ParseField("incremental"); + private final ScheduleConfig accessControl; + private final ScheduleConfig full; + private final ScheduleConfig incremental; + /** * @param accessControl connector access control sync schedule represented as {@link ScheduleConfig} * @param full connector full sync schedule represented as {@link ScheduleConfig} @@ -238,12 +239,19 @@ public ScheduleConfig build() { } } + /** + * Default scheduling is set to everyday at midnight (00:00:00). + * + * @return default scheduling for full, incremental and access control syncs. + */ public static ConnectorScheduling getDefaultConnectorScheduling() { return new ConnectorScheduling.Builder().setAccessControl( - new ConnectorScheduling.ScheduleConfig.Builder().setEnabled(false).setInterval(new Cron("0 0 0 * * ?")).build() + new ConnectorScheduling.ScheduleConfig.Builder().setEnabled(false).setInterval(new Cron(EVERYDAY_AT_MIDNIGHT)).build() ) - .setFull(new ConnectorScheduling.ScheduleConfig.Builder().setEnabled(false).setInterval(new Cron("0 0 0 * * ?")).build()) - .setIncremental(new ConnectorScheduling.ScheduleConfig.Builder().setEnabled(false).setInterval(new Cron("0 0 0 * * ?")).build()) + .setFull(new ConnectorScheduling.ScheduleConfig.Builder().setEnabled(false).setInterval(new Cron(EVERYDAY_AT_MIDNIGHT)).build()) + .setIncremental( + new ConnectorScheduling.ScheduleConfig.Builder().setEnabled(false).setInterval(new Cron(EVERYDAY_AT_MIDNIGHT)).build() + ) .build(); } } From 10ec23a42cc009a877f39b3176eaa273e8296368 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 23 Feb 2024 19:51:04 +1100 Subject: [PATCH 168/250] Add metrics for retries by S3RetryingInputStream (#105600) This PR exposes retries in S3RetryingInputStream as metrics for easier observability. At the class API level, retries can happen when either opening an input stream and reading from an input stream. Retry reading from an input stream internally can also retry re-opening the input stream. All these retries are counted under the retries for reading since the higher API usage is a read instead of open. The list of new metrics are: * `es.repositories.s3.input_stream.retry.event.total` - Number of times a retry cycle has been triggered. * `es.repositories.s3.input_stream.retry.success.total` - Number of a times a retry cycle has been successfully completed. This should match the above metric in numbers. Otherwise it indicates there are threads stuck in infinite retries. * `es.repositories.s3.input_stream.retry.attempts.histogram` - Number of attempts to complete a retry cycle successfully. Relates: https://github.com/elastic/elasticsearch/pull/103300#discussion_r1444125047 Relates: ES-7666 --- .../repositories/azure/AzureRepository.java | 4 +- .../gcs/GoogleCloudStorageRepository.java | 4 +- .../s3/S3BlobStoreRepositoryTests.java | 5 +- .../s3/S3RepositoryThirdPartyTests.java | 3 +- .../repositories/s3/S3BlobStore.java | 31 ++++-- .../s3/S3RepositoriesMetrics.java | 37 +++++++ .../repositories/s3/S3Repository.java | 11 +- .../repositories/s3/S3RepositoryPlugin.java | 9 +- .../s3/S3RetryingInputStream.java | 39 ++++++- .../s3/RepositoryCredentialsTests.java | 5 +- .../s3/S3BlobContainerRetriesTests.java | 103 +++++++++++++++++- .../repositories/s3/S3RepositoryTests.java | 3 +- .../repositories/RepositoriesMetrics.java | 2 + .../blobstore/MeteredBlobStoreRepository.java | 6 +- .../RepositoriesServiceTests.java | 6 +- .../telemetry/RecordingMeterRegistry.java | 2 +- 16 files changed, 216 insertions(+), 54 deletions(-) create mode 100644 modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoriesMetrics.java diff --git a/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java b/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java index f58611cb0567a..388474acc75ea 100644 --- a/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java +++ b/modules/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java @@ -21,7 +21,6 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.indices.recovery.RecoverySettings; -import org.elasticsearch.repositories.RepositoriesMetrics; import org.elasticsearch.repositories.blobstore.MeteredBlobStoreRepository; import org.elasticsearch.xcontent.NamedXContentRegistry; @@ -108,8 +107,7 @@ public AzureRepository( bigArrays, recoverySettings, buildBasePath(metadata), - buildLocation(metadata), - RepositoriesMetrics.NOOP + buildLocation(metadata) ); this.chunkSize = Repository.CHUNK_SIZE_SETTING.get(metadata.settings()); this.storageService = storageService; diff --git a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRepository.java b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRepository.java index 94d0abe17909f..e2338371cf837 100644 --- a/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRepository.java +++ b/modules/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRepository.java @@ -19,7 +19,6 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.indices.recovery.RecoverySettings; -import org.elasticsearch.repositories.RepositoriesMetrics; import org.elasticsearch.repositories.RepositoryException; import org.elasticsearch.repositories.blobstore.MeteredBlobStoreRepository; import org.elasticsearch.xcontent.NamedXContentRegistry; @@ -77,8 +76,7 @@ class GoogleCloudStorageRepository extends MeteredBlobStoreRepository { bigArrays, recoverySettings, buildBasePath(metadata), - buildLocation(metadata), - RepositoriesMetrics.NOOP + buildLocation(metadata) ); this.storageService = storageService; diff --git a/modules/repository-s3/src/internalClusterTest/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java b/modules/repository-s3/src/internalClusterTest/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java index 248ccc119794e..4080a47c7dabe 100644 --- a/modules/repository-s3/src/internalClusterTest/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java +++ b/modules/repository-s3/src/internalClusterTest/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java @@ -39,7 +39,6 @@ import org.elasticsearch.indices.recovery.RecoverySettings; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.PluginsService; -import org.elasticsearch.repositories.RepositoriesMetrics; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.repositories.Repository; import org.elasticsearch.repositories.RepositoryData; @@ -460,9 +459,9 @@ protected S3Repository createRepository( ClusterService clusterService, BigArrays bigArrays, RecoverySettings recoverySettings, - RepositoriesMetrics repositoriesMetrics + S3RepositoriesMetrics s3RepositoriesMetrics ) { - return new S3Repository(metadata, registry, getService(), clusterService, bigArrays, recoverySettings, repositoriesMetrics) { + return new S3Repository(metadata, registry, getService(), clusterService, bigArrays, recoverySettings, s3RepositoriesMetrics) { @Override public BlobStore blobStore() { diff --git a/modules/repository-s3/src/internalClusterTest/java/org/elasticsearch/repositories/s3/S3RepositoryThirdPartyTests.java b/modules/repository-s3/src/internalClusterTest/java/org/elasticsearch/repositories/s3/S3RepositoryThirdPartyTests.java index f182b54b0c696..b8fea485c6276 100644 --- a/modules/repository-s3/src/internalClusterTest/java/org/elasticsearch/repositories/s3/S3RepositoryThirdPartyTests.java +++ b/modules/repository-s3/src/internalClusterTest/java/org/elasticsearch/repositories/s3/S3RepositoryThirdPartyTests.java @@ -30,7 +30,6 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.repositories.AbstractThirdPartyRepositoryTestCase; -import org.elasticsearch.repositories.RepositoriesMetrics; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.fixtures.minio.MinioTestContainer; @@ -145,7 +144,7 @@ public long absoluteTimeInMillis() { ClusterServiceUtils.createClusterService(threadpool), BigArrays.NON_RECYCLING_INSTANCE, new RecoverySettings(node().settings(), node().injector().getInstance(ClusterService.class).getClusterSettings()), - RepositoriesMetrics.NOOP + S3RepositoriesMetrics.NOOP ) ) { repository.start(); diff --git a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobStore.java b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobStore.java index 78b1e2dba98b3..6b9937b01a433 100644 --- a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobStore.java +++ b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobStore.java @@ -33,7 +33,6 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.core.TimeValue; -import org.elasticsearch.repositories.RepositoriesMetrics; import org.elasticsearch.threadpool.ThreadPool; import java.io.IOException; @@ -84,7 +83,7 @@ class S3BlobStore implements BlobStore { private final ThreadPool threadPool; private final Executor snapshotExecutor; - private final RepositoriesMetrics repositoriesMetrics; + private final S3RepositoriesMetrics s3RepositoriesMetrics; private final StatsCollectors statsCollectors = new StatsCollectors(); @@ -98,7 +97,7 @@ class S3BlobStore implements BlobStore { RepositoryMetadata repositoryMetadata, BigArrays bigArrays, ThreadPool threadPool, - RepositoriesMetrics repositoriesMetrics + S3RepositoriesMetrics s3RepositoriesMetrics ) { this.service = service; this.bigArrays = bigArrays; @@ -110,7 +109,7 @@ class S3BlobStore implements BlobStore { this.repositoryMetadata = repositoryMetadata; this.threadPool = threadPool; this.snapshotExecutor = threadPool.executor(ThreadPool.Names.SNAPSHOT); - this.repositoriesMetrics = repositoriesMetrics; + this.s3RepositoriesMetrics = s3RepositoriesMetrics; } RequestMetricCollector getMetricCollector(Operation operation, OperationPurpose purpose) { @@ -174,19 +173,19 @@ public final void collectMetrics(Request request, Response response) { .map(List::size) .orElse(0); - repositoriesMetrics.operationCounter().incrementBy(1, attributes); + s3RepositoriesMetrics.common().operationCounter().incrementBy(1, attributes); if (numberOfAwsErrors == requestCount) { - repositoriesMetrics.unsuccessfulOperationCounter().incrementBy(1, attributes); + s3RepositoriesMetrics.common().unsuccessfulOperationCounter().incrementBy(1, attributes); } - repositoriesMetrics.requestCounter().incrementBy(requestCount, attributes); + s3RepositoriesMetrics.common().requestCounter().incrementBy(requestCount, attributes); if (exceptionCount > 0) { - repositoriesMetrics.exceptionCounter().incrementBy(exceptionCount, attributes); - repositoriesMetrics.exceptionHistogram().record(exceptionCount, attributes); + s3RepositoriesMetrics.common().exceptionCounter().incrementBy(exceptionCount, attributes); + s3RepositoriesMetrics.common().exceptionHistogram().record(exceptionCount, attributes); } if (throttleCount > 0) { - repositoriesMetrics.throttleCounter().incrementBy(throttleCount, attributes); - repositoriesMetrics.throttleHistogram().record(throttleCount, attributes); + s3RepositoriesMetrics.common().throttleCounter().incrementBy(throttleCount, attributes); + s3RepositoriesMetrics.common().throttleHistogram().record(throttleCount, attributes); } maybeRecordHttpRequestTime(request); } @@ -207,7 +206,7 @@ private void maybeRecordHttpRequestTime(Request request) { if (totalTimeInMicros == 0) { logger.warn("Expected HttpRequestTime to be tracked for request [{}] but found no count.", request); } else { - repositoriesMetrics.httpRequestTimeInMicroHistogram().record(totalTimeInMicros, attributes); + s3RepositoriesMetrics.common().httpRequestTimeInMicroHistogram().record(totalTimeInMicros, attributes); } } @@ -293,6 +292,14 @@ public long bufferSizeInBytes() { return bufferSize.getBytes(); } + public RepositoryMetadata getRepositoryMetadata() { + return repositoryMetadata; + } + + public S3RepositoriesMetrics getS3RepositoriesMetrics() { + return s3RepositoriesMetrics; + } + @Override public BlobContainer blobContainer(BlobPath path) { return new S3BlobContainer(path, this); diff --git a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoriesMetrics.java b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoriesMetrics.java new file mode 100644 index 0000000000000..e025214998d5b --- /dev/null +++ b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoriesMetrics.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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.repositories.s3; + +import org.elasticsearch.repositories.RepositoriesMetrics; +import org.elasticsearch.telemetry.metric.LongCounter; +import org.elasticsearch.telemetry.metric.LongHistogram; + +public record S3RepositoriesMetrics( + RepositoriesMetrics common, + LongCounter retryStartedCounter, + LongCounter retryCompletedCounter, + LongHistogram retryHistogram +) { + + public static S3RepositoriesMetrics NOOP = new S3RepositoriesMetrics(RepositoriesMetrics.NOOP); + + public static final String METRIC_RETRY_EVENT_TOTAL = "es.repositories.s3.input_stream.retry.event.total"; + public static final String METRIC_RETRY_SUCCESS_TOTAL = "es.repositories.s3.input_stream.retry.success.total"; + public static final String METRIC_RETRY_ATTEMPTS_HISTOGRAM = "es.repositories.s3.input_stream.retry.attempts.histogram"; + + public S3RepositoriesMetrics(RepositoriesMetrics common) { + this( + common, + common.meterRegistry().registerLongCounter(METRIC_RETRY_EVENT_TOTAL, "s3 input stream retry event count", "unit"), + common.meterRegistry().registerLongCounter(METRIC_RETRY_SUCCESS_TOTAL, "s3 input stream retry success count", "unit"), + common.meterRegistry() + .registerLongHistogram(METRIC_RETRY_ATTEMPTS_HISTOGRAM, "s3 input stream retry attempts histogram", "unit") + ); + } +} diff --git a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java index 624867a2f0c41..26b1b1158dea0 100644 --- a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java +++ b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java @@ -31,7 +31,6 @@ import org.elasticsearch.indices.recovery.RecoverySettings; import org.elasticsearch.monitor.jvm.JvmInfo; import org.elasticsearch.repositories.FinalizeSnapshotContext; -import org.elasticsearch.repositories.RepositoriesMetrics; import org.elasticsearch.repositories.RepositoryData; import org.elasticsearch.repositories.RepositoryException; import org.elasticsearch.repositories.blobstore.MeteredBlobStoreRepository; @@ -195,6 +194,8 @@ class S3Repository extends MeteredBlobStoreRepository { private final Executor snapshotExecutor; + private final S3RepositoriesMetrics s3RepositoriesMetrics; + /** * Constructs an s3 backed repository */ @@ -205,7 +206,7 @@ class S3Repository extends MeteredBlobStoreRepository { final ClusterService clusterService, final BigArrays bigArrays, final RecoverySettings recoverySettings, - final RepositoriesMetrics repositoriesMetrics + final S3RepositoriesMetrics s3RepositoriesMetrics ) { super( metadata, @@ -214,10 +215,10 @@ class S3Repository extends MeteredBlobStoreRepository { bigArrays, recoverySettings, buildBasePath(metadata), - buildLocation(metadata), - repositoriesMetrics + buildLocation(metadata) ); this.service = service; + this.s3RepositoriesMetrics = s3RepositoriesMetrics; this.snapshotExecutor = threadPool().executor(ThreadPool.Names.SNAPSHOT); // Parse and validate the user's S3 Storage Class setting @@ -408,7 +409,7 @@ protected S3BlobStore createBlobStore() { metadata, bigArrays, threadPool, - repositoriesMetrics + s3RepositoriesMetrics ); } diff --git a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoryPlugin.java b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoryPlugin.java index 83668cc271922..26047c3b416a7 100644 --- a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoryPlugin.java +++ b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RepositoryPlugin.java @@ -78,9 +78,9 @@ protected S3Repository createRepository( final ClusterService clusterService, final BigArrays bigArrays, final RecoverySettings recoverySettings, - final RepositoriesMetrics repositoriesMetrics + final S3RepositoriesMetrics s3RepositoriesMetrics ) { - return new S3Repository(metadata, registry, service.get(), clusterService, bigArrays, recoverySettings, repositoriesMetrics); + return new S3Repository(metadata, registry, service.get(), clusterService, bigArrays, recoverySettings, s3RepositoriesMetrics); } @Override @@ -101,11 +101,12 @@ public Map getRepositories( final ClusterService clusterService, final BigArrays bigArrays, final RecoverySettings recoverySettings, - RepositoriesMetrics repositoriesMetrics + final RepositoriesMetrics repositoriesMetrics ) { + final S3RepositoriesMetrics s3RepositoriesMetrics = new S3RepositoriesMetrics(repositoriesMetrics); return Collections.singletonMap( S3Repository.TYPE, - metadata -> createRepository(metadata, registry, clusterService, bigArrays, recoverySettings, repositoriesMetrics) + metadata -> createRepository(metadata, registry, clusterService, bigArrays, recoverySettings, s3RepositoriesMetrics) ); } diff --git a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RetryingInputStream.java b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RetryingInputStream.java index c457b9d51e8b9..f7a99a399f59f 100644 --- a/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RetryingInputStream.java +++ b/modules/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RetryingInputStream.java @@ -27,6 +27,7 @@ import java.nio.file.NoSuchFileException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import static org.elasticsearch.core.Strings.format; import static org.elasticsearch.repositories.s3.S3BlobStore.configureRequestForMetrics; @@ -80,7 +81,7 @@ class S3RetryingInputStream extends InputStream { this.end = end; final int initialAttempt = attempt; openStreamWithRetry(); - maybeLogForSuccessAfterRetries(initialAttempt, "opened"); + maybeLogAndRecordMetricsForSuccess(initialAttempt, "open"); } private void openStreamWithRetry() throws IOException { @@ -105,6 +106,9 @@ private void openStreamWithRetry() throws IOException { ); } + if (attempt == 1) { + blobStore.getS3RepositoriesMetrics().retryStartedCounter().incrementBy(1, metricAttributes("open")); + } final long delayInMillis = maybeLogAndComputeRetryDelay("opening", e); delayBeforeRetry(delayInMillis); } @@ -142,9 +146,12 @@ public int read() throws IOException { } else { currentOffset += 1; } - maybeLogForSuccessAfterRetries(initialAttempt, "read"); + maybeLogAndRecordMetricsForSuccess(initialAttempt, "read"); return result; } catch (IOException e) { + if (attempt == initialAttempt) { + blobStore.getS3RepositoriesMetrics().retryStartedCounter().incrementBy(1, metricAttributes("read")); + } reopenStreamOrFail(e); } } @@ -162,9 +169,12 @@ public int read(byte[] b, int off, int len) throws IOException { } else { currentOffset += bytesRead; } - maybeLogForSuccessAfterRetries(initialAttempt, "read"); + maybeLogAndRecordMetricsForSuccess(initialAttempt, "read"); return bytesRead; } catch (IOException e) { + if (attempt == initialAttempt) { + blobStore.getS3RepositoriesMetrics().retryStartedCounter().incrementBy(1, metricAttributes("read")); + } reopenStreamOrFail(e); } } @@ -246,16 +256,20 @@ private void logForRetry(Level level, String action, Exception e) { ); } - private void maybeLogForSuccessAfterRetries(int initialAttempt, String action) { + private void maybeLogAndRecordMetricsForSuccess(int initialAttempt, String action) { if (attempt > initialAttempt) { + final int numberOfRetries = attempt - initialAttempt; logger.info( "successfully {} input stream for [{}/{}] with purpose [{}] after [{}] retries", action, blobStore.bucket(), blobKey, purpose.getKey(), - attempt - initialAttempt + numberOfRetries ); + final Map attributes = metricAttributes(action); + blobStore.getS3RepositoriesMetrics().retryCompletedCounter().incrementBy(1, attributes); + blobStore.getS3RepositoriesMetrics().retryHistogram().record(numberOfRetries, attributes); } } @@ -294,6 +308,21 @@ protected long getRetryDelayInMillis() { return 10L << (Math.min(attempt - 1, 10)); } + private Map metricAttributes(String action) { + return Map.of( + "repo_type", + S3Repository.TYPE, + "repo_name", + blobStore.getRepositoryMetadata().name(), + "operation", + Operation.GET_OBJECT.getKey(), + "purpose", + purpose.getKey(), + "action", + action + ); + } + @Override public void close() throws IOException { maybeAbort(currentStream); diff --git a/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java b/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java index 28a48c2968f59..cf3bc21526bf6 100644 --- a/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java +++ b/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/RepositoryCredentialsTests.java @@ -26,7 +26,6 @@ import org.elasticsearch.indices.recovery.RecoverySettings; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.PluginsService; -import org.elasticsearch.repositories.RepositoriesMetrics; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.rest.AbstractRestChannel; import org.elasticsearch.rest.RestRequest; @@ -264,9 +263,9 @@ protected S3Repository createRepository( ClusterService clusterService, BigArrays bigArrays, RecoverySettings recoverySettings, - RepositoriesMetrics repositoriesMetrics + S3RepositoriesMetrics s3RepositoriesMetrics ) { - return new S3Repository(metadata, registry, getService(), clusterService, bigArrays, recoverySettings, repositoriesMetrics) { + return new S3Repository(metadata, registry, getService(), clusterService, bigArrays, recoverySettings, s3RepositoriesMetrics) { @Override protected void assertSnapshotOrGenericThread() { // eliminate thread name check as we create repo manually on test/main threads diff --git a/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobContainerRetriesTests.java b/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobContainerRetriesTests.java index 0ddd29171b3bd..05268d750637c 100644 --- a/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobContainerRetriesTests.java +++ b/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobContainerRetriesTests.java @@ -43,6 +43,9 @@ import org.elasticsearch.repositories.RepositoriesMetrics; import org.elasticsearch.repositories.blobstore.AbstractBlobContainerRetriesTestCase; import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; +import org.elasticsearch.telemetry.InstrumentType; +import org.elasticsearch.telemetry.Measurement; +import org.elasticsearch.telemetry.RecordingMeterRegistry; import org.elasticsearch.watcher.ResourceWatcherService; import org.hamcrest.Matcher; import org.junit.After; @@ -59,7 +62,9 @@ import java.nio.charset.StandardCharsets; import java.nio.file.NoSuchFileException; import java.util.Arrays; +import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.OptionalInt; import java.util.concurrent.atomic.AtomicBoolean; @@ -74,10 +79,13 @@ import static org.elasticsearch.repositories.s3.S3ClientSettings.READ_TIMEOUT_SETTING; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThan; @@ -91,6 +99,7 @@ public class S3BlobContainerRetriesTests extends AbstractBlobContainerRetriesTes private S3Service service; private AtomicBoolean shouldErrorOnDns; + private RecordingMeterRegistry recordingMeterRegistry; @Before public void setUp() throws Exception { @@ -109,6 +118,7 @@ protected AmazonS3ClientBuilder buildClientBuilder(S3ClientSettings clientSettin return builder; } }; + recordingMeterRegistry = new RecordingMeterRegistry(); super.setUp(); } @@ -185,7 +195,7 @@ protected BlobContainer createBlobContainer( repositoryMetadata, BigArrays.NON_RECYCLING_INSTANCE, new DeterministicTaskQueue().getThreadPool(), - RepositoriesMetrics.NOOP + new S3RepositoriesMetrics(new RepositoriesMetrics(recordingMeterRegistry)) ); return new S3BlobContainer(randomBoolean() ? BlobPath.EMPTY : BlobPath.EMPTY.add("foo"), s3BlobStore) { @Override @@ -669,8 +679,8 @@ public void handle(HttpExchange exchange) throws IOException { } exchange.getResponseBody().write(bytes, rangeStart, length); } else { - failures.incrementAndGet(); if (randomBoolean()) { + failures.incrementAndGet(); exchange.sendResponseHeaders( randomFrom( HttpStatus.SC_INTERNAL_SERVER_ERROR, @@ -686,6 +696,8 @@ public void handle(HttpExchange exchange) throws IOException { if (bytesSent >= meaningfulProgressBytes) { exchange.getResponseBody().flush(); } + } else { + failures.incrementAndGet(); } } } @@ -700,16 +712,28 @@ public void handle(HttpExchange exchange) throws IOException { final int length = between(0, randomBoolean() ? bytes.length : Integer.MAX_VALUE); logger.info("--> position={}, length={}", position, length); try (InputStream inputStream = blobContainer.readBlob(OperationPurpose.INDICES, "read_blob_retries_forever", position, length)) { + assertMetricsForOpeningStream(); + recordingMeterRegistry.getRecorder().resetCalls(); + failures.set(0); + final byte[] bytesRead = BytesReference.toBytes(Streams.readFully(inputStream)); assertArrayEquals(Arrays.copyOfRange(bytes, position, Math.min(bytes.length, position + length)), bytesRead); + assertMetricsForReadingStream(); } assertThat(failures.get(), greaterThan(totalFailures)); // Read the whole blob failures.set(0); + recordingMeterRegistry.getRecorder().resetCalls(); try (InputStream inputStream = blobContainer.readBlob(OperationPurpose.INDICES, "read_blob_retries_forever")) { + assertMetricsForOpeningStream(); + recordingMeterRegistry.getRecorder().resetCalls(); + failures.set(0); + final byte[] bytesRead = BytesReference.toBytes(Streams.readFully(inputStream)); assertArrayEquals(bytes, bytesRead); + + assertMetricsForReadingStream(); } assertThat(failures.get(), greaterThan(totalFailures)); } @@ -737,9 +761,13 @@ public void handle(HttpExchange exchange) throws IOException { : blobContainer.readBlob(randomRetryingPurpose(), "read_blob_not_found", between(0, 100), between(1, 100)) ) { Streams.readFully(inputStream); + } }); assertThat(numberOfReads.get(), equalTo(1)); + assertThat(getRetryStartedMeasurements(), empty()); + assertThat(getRetryCompletedMeasurements(), empty()); + assertThat(getRetryHistogramMeasurements(), empty()); } @Override @@ -761,6 +789,77 @@ protected OperationPurpose randomFiniteRetryingPurpose() { ); } + private void assertMetricsForOpeningStream() { + final long numberOfOperations = getOperationMeasurements(); + // S3 client sdk internally also retries within the configured maxRetries for retryable errors. + // The retries in S3RetryingInputStream are triggered when the client internal retries are unsuccessful + if (numberOfOperations > 1) { + // For opening the stream, there should be exactly one pair of started and completed records. + // There should be one histogram record, the number of retries must be greater than 0 + final Map attributes = metricAttributes("open"); + assertThat(getRetryStartedMeasurements(), contains(new Measurement(1L, attributes, false))); + assertThat(getRetryCompletedMeasurements(), contains(new Measurement(1L, attributes, false))); + final List retryHistogramMeasurements = getRetryHistogramMeasurements(); + assertThat(retryHistogramMeasurements, hasSize(1)); + assertThat(retryHistogramMeasurements.get(0).getLong(), equalTo(numberOfOperations - 1)); + assertThat(retryHistogramMeasurements.get(0).attributes(), equalTo(attributes)); + } else { + assertThat(getRetryStartedMeasurements(), empty()); + assertThat(getRetryCompletedMeasurements(), empty()); + assertThat(getRetryHistogramMeasurements(), empty()); + } + } + + private void assertMetricsForReadingStream() { + // For reading the stream, there could be multiple pairs of started and completed records. + // It is important that they always come in pairs and the number of pairs match the number + // of histogram records. + final Map attributes = metricAttributes("read"); + final List retryHistogramMeasurements = getRetryHistogramMeasurements(); + final int numberOfReads = retryHistogramMeasurements.size(); + retryHistogramMeasurements.forEach(measurement -> { + assertThat(measurement.getLong(), greaterThan(0L)); + assertThat(measurement.attributes(), equalTo(attributes)); + }); + + final List retryStartedMeasurements = getRetryStartedMeasurements(); + assertThat(retryStartedMeasurements, hasSize(1)); + assertThat(retryStartedMeasurements.get(0).getLong(), equalTo((long) numberOfReads)); + assertThat(retryStartedMeasurements.get(0).attributes(), equalTo(attributes)); + assertThat(retryStartedMeasurements, equalTo(getRetryCompletedMeasurements())); + } + + private long getOperationMeasurements() { + final List operationMeasurements = Measurement.combine( + recordingMeterRegistry.getRecorder().getMeasurements(InstrumentType.LONG_COUNTER, RepositoriesMetrics.METRIC_OPERATIONS_TOTAL) + ); + assertThat(operationMeasurements, hasSize(1)); + return operationMeasurements.get(0).getLong(); + } + + private List getRetryStartedMeasurements() { + return Measurement.combine( + recordingMeterRegistry.getRecorder() + .getMeasurements(InstrumentType.LONG_COUNTER, S3RepositoriesMetrics.METRIC_RETRY_EVENT_TOTAL) + ); + } + + private List getRetryCompletedMeasurements() { + return Measurement.combine( + recordingMeterRegistry.getRecorder() + .getMeasurements(InstrumentType.LONG_COUNTER, S3RepositoriesMetrics.METRIC_RETRY_SUCCESS_TOTAL) + ); + } + + private List getRetryHistogramMeasurements() { + return recordingMeterRegistry.getRecorder() + .getMeasurements(InstrumentType.LONG_HISTOGRAM, S3RepositoriesMetrics.METRIC_RETRY_ATTEMPTS_HISTOGRAM); + } + + private Map metricAttributes(String action) { + return Map.of("repo_type", "s3", "repo_name", "repository", "operation", "GetObject", "purpose", "Indices", "action", action); + } + /** * Asserts that an InputStream is fully consumed, or aborted, when it is closed */ diff --git a/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java b/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java index 0a92ed0a28973..50470ec499ef6 100644 --- a/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java +++ b/modules/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3RepositoryTests.java @@ -18,7 +18,6 @@ import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.env.Environment; import org.elasticsearch.indices.recovery.RecoverySettings; -import org.elasticsearch.repositories.RepositoriesMetrics; import org.elasticsearch.repositories.RepositoryException; import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; import org.elasticsearch.test.ESTestCase; @@ -130,7 +129,7 @@ private S3Repository createS3Repo(RepositoryMetadata metadata) { BlobStoreTestUtil.mockClusterService(), MockBigArrays.NON_RECYCLING_INSTANCE, new RecoverySettings(Settings.EMPTY, new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS)), - RepositoriesMetrics.NOOP + S3RepositoriesMetrics.NOOP ) { @Override protected void assertSnapshotOrGenericThread() { diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoriesMetrics.java b/server/src/main/java/org/elasticsearch/repositories/RepositoriesMetrics.java index b4d79d89ec4c6..50aa7881cd2b6 100644 --- a/server/src/main/java/org/elasticsearch/repositories/RepositoriesMetrics.java +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoriesMetrics.java @@ -13,6 +13,7 @@ import org.elasticsearch.telemetry.metric.MeterRegistry; public record RepositoriesMetrics( + MeterRegistry meterRegistry, LongCounter requestCounter, LongCounter exceptionCounter, LongCounter throttleCounter, @@ -36,6 +37,7 @@ public record RepositoriesMetrics( public RepositoriesMetrics(MeterRegistry meterRegistry) { this( + meterRegistry, meterRegistry.registerLongCounter(METRIC_REQUESTS_TOTAL, "repository request counter", "unit"), meterRegistry.registerLongCounter(METRIC_EXCEPTIONS_TOTAL, "repository request exception counter", "unit"), meterRegistry.registerLongCounter(METRIC_THROTTLES_TOTAL, "repository request throttle counter", "unit"), diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/MeteredBlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/MeteredBlobStoreRepository.java index 6ecab2f8c77f2..c5ea99b0e5c14 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/MeteredBlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/MeteredBlobStoreRepository.java @@ -14,7 +14,6 @@ import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.indices.recovery.RecoverySettings; -import org.elasticsearch.repositories.RepositoriesMetrics; import org.elasticsearch.repositories.RepositoryInfo; import org.elasticsearch.repositories.RepositoryStatsSnapshot; import org.elasticsearch.threadpool.ThreadPool; @@ -24,7 +23,6 @@ public abstract class MeteredBlobStoreRepository extends BlobStoreRepository { private final RepositoryInfo repositoryInfo; - protected final RepositoriesMetrics repositoriesMetrics; public MeteredBlobStoreRepository( RepositoryMetadata metadata, @@ -33,11 +31,9 @@ public MeteredBlobStoreRepository( BigArrays bigArrays, RecoverySettings recoverySettings, BlobPath basePath, - Map location, - RepositoriesMetrics repositoriesMetrics + Map location ) { super(metadata, namedXContentRegistry, clusterService, bigArrays, recoverySettings, basePath); - this.repositoriesMetrics = repositoriesMetrics; ThreadPool threadPool = clusterService.getClusterApplierService().threadPool(); this.repositoryInfo = new RepositoryInfo( UUIDs.randomBase64UUID(), diff --git a/server/src/test/java/org/elasticsearch/repositories/RepositoriesServiceTests.java b/server/src/test/java/org/elasticsearch/repositories/RepositoriesServiceTests.java index 4f7001f00e6a7..45e4bb09c1616 100644 --- a/server/src/test/java/org/elasticsearch/repositories/RepositoriesServiceTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/RepositoriesServiceTests.java @@ -482,8 +482,7 @@ private MeteredRepositoryTypeA(RepositoryMetadata metadata, ClusterService clust MockBigArrays.NON_RECYCLING_INSTANCE, mock(RecoverySettings.class), BlobPath.EMPTY, - Map.of("bucket", "bucket-a"), - RepositoriesMetrics.NOOP + Map.of("bucket", "bucket-a") ); } @@ -510,8 +509,7 @@ private MeteredRepositoryTypeB(RepositoryMetadata metadata, ClusterService clust MockBigArrays.NON_RECYCLING_INSTANCE, mock(RecoverySettings.class), BlobPath.EMPTY, - Map.of("bucket", "bucket-b"), - RepositoriesMetrics.NOOP + Map.of("bucket", "bucket-b") ); } diff --git a/test/framework/src/main/java/org/elasticsearch/telemetry/RecordingMeterRegistry.java b/test/framework/src/main/java/org/elasticsearch/telemetry/RecordingMeterRegistry.java index 86bfd9bf38c26..33693c297f166 100644 --- a/test/framework/src/main/java/org/elasticsearch/telemetry/RecordingMeterRegistry.java +++ b/test/framework/src/main/java/org/elasticsearch/telemetry/RecordingMeterRegistry.java @@ -33,7 +33,7 @@ public class RecordingMeterRegistry implements MeterRegistry { protected final MetricRecorder recorder = new MetricRecorder<>(); - MetricRecorder getRecorder() { + public MetricRecorder getRecorder() { return recorder; } From d67af191c0565d44e4e5571d7ea6b575bebf1812 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 23 Feb 2024 12:53:03 +0100 Subject: [PATCH 169/250] Enhance LeakTracker to record thread + test that caused a leak (#105758) Add recording of the current thread name to ever leak record. Also add functionality to set a global hint for the context of a leak and use it to record the current test name. This massively eases the task of correlating an observed leak with the test that caused it. --- .../elasticsearch/transport/LeakTracker.java | 19 ++++++++++++++++++- .../org/elasticsearch/test/ESTestCase.java | 2 ++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/transport/LeakTracker.java b/server/src/main/java/org/elasticsearch/transport/LeakTracker.java index 3be22f6fae53a..77a41cff15fd7 100644 --- a/server/src/main/java/org/elasticsearch/transport/LeakTracker.java +++ b/server/src/main/java/org/elasticsearch/transport/LeakTracker.java @@ -41,6 +41,8 @@ public final class LeakTracker { public static final LeakTracker INSTANCE = new LeakTracker(); + private static volatile String contextHint = ""; + private LeakTracker() {} /** @@ -72,6 +74,15 @@ public void reportLeak() { } } + /** + * Set a hint string that will be recorded with every leak that is recorded. Used by unit tests to allow identifying the exact test + * that caused a leak by setting the test name here. + * @param hint hint value + */ + public static void setContextHint(String hint) { + contextHint = hint; + } + public static Releasable wrap(Releasable releasable) { if (Assertions.ENABLED == false) { return releasable; @@ -299,19 +310,25 @@ private static final class Record extends Throwable { private final Record next; private final int pos; + private final String threadName; + + private final String contextHint = LeakTracker.contextHint; + Record(Record next) { this.next = next; this.pos = next.pos + 1; + threadName = Thread.currentThread().getName(); } private Record() { next = null; pos = -1; + threadName = Thread.currentThread().getName(); } @Override public String toString() { - StringBuilder buf = new StringBuilder(); + StringBuilder buf = new StringBuilder("\tin [").append(threadName).append("][").append(contextHint).append("]\n"); StackTraceElement[] array = getStackTrace(); // Skip the first three elements since those are just related to the leak tracker. for (int i = 3; i < array.length; i++) { 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 7b4032cc56cef..67919756e16a9 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -499,6 +499,7 @@ public void removeHeaderWarningAppender() { @Before public final void before() { + LeakTracker.setContextHint(getTestName()); logger.info("{}before test", getTestParamsForLogging()); assertNull("Thread context initialized twice", threadContext); if (enableWarningsCheck()) { @@ -530,6 +531,7 @@ public final void after() throws Exception { ensureAllSearchContextsReleased(); ensureCheckIndexPassed(); logger.info("{}after test", getTestParamsForLogging()); + LeakTracker.setContextHint(""); } private String getTestParamsForLogging() { From 39a4ddb3f6bfcb2db98b14ed47f105003357556e Mon Sep 17 00:00:00 2001 From: Alessandro Stoltenberg Date: Fri, 23 Feb 2024 13:01:08 +0100 Subject: [PATCH 170/250] email-reporting-attachment-docs: Correct auth and proxy fields. (#105730) --- docs/reference/watcher/actions/email.asciidoc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/reference/watcher/actions/email.asciidoc b/docs/reference/watcher/actions/email.asciidoc index 71fdd95148d24..16b9cc4be0628 100644 --- a/docs/reference/watcher/actions/email.asciidoc +++ b/docs/reference/watcher/actions/email.asciidoc @@ -149,8 +149,10 @@ killed by firewalls or load balancers in-between. means, by default watcher tries to download a dashboard for 10 minutes, forty times fifteen seconds). The setting `xpack.notification.reporting.interval` can be configured globally to change the default. -| `request.auth` | Additional auth configuration for the request -| `request.proxy` | Additional proxy configuration for the request +| `auth` | Additional auth configuration for the request, see + {kibana-ref}/automating-report-generation.html#use-watcher[use watcher] for details +| `proxy` | Additional proxy configuration for the request. See <> + on how to configure the values. |====== From 1dd2712bad106084b0d706ebe170860d11862808 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Fri, 23 Feb 2024 08:22:28 -0500 Subject: [PATCH 171/250] Add new shard_seed parameter for random_sampler agg (#104830) While it is important to ensure IID via shard hashes, it can become a barrier and a complexity when testing out random_sampler. So, this commit adds a new optional parameter called `shardSeed`, which, when combined with `seed` ensures 100% consistent sampling over shards where data is exactly the same. --- docs/changelog/104830.yaml | 5 +++ .../aggregations/bucket/RandomSamplerIT.java | 43 +++++++++++++++++++ .../org/elasticsearch/TransportVersions.java | 1 + .../sampler/random/InternalRandomSampler.java | 19 +++++++- .../RandomSamplerAggregationBuilder.java | 24 +++++++++-- .../random/RandomSamplerAggregator.java | 6 ++- .../RandomSamplerAggregatorFactory.java | 24 +++++++++-- .../aggregations/support/SamplingContext.java | 15 ++++--- .../RandomSamplerAggregationBuilderTests.java | 3 ++ .../support/SamplingContextTests.java | 5 +-- .../aggregations/AggregatorTestCase.java | 6 ++- .../test/InternalAggregationTestCase.java | 6 ++- 12 files changed, 137 insertions(+), 20 deletions(-) create mode 100644 docs/changelog/104830.yaml diff --git a/docs/changelog/104830.yaml b/docs/changelog/104830.yaml new file mode 100644 index 0000000000000..c056f3d618b75 --- /dev/null +++ b/docs/changelog/104830.yaml @@ -0,0 +1,5 @@ +pr: 104830 +summary: All new `shard_seed` parameter for `random_sampler` agg +area: Aggregations +type: enhancement +issues: [] diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/RandomSamplerIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/RandomSamplerIT.java index 28c186c559dff..53075e31cd6f9 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/RandomSamplerIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/RandomSamplerIT.java @@ -24,6 +24,7 @@ import static org.elasticsearch.search.aggregations.AggregationBuilders.histogram; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertResponse; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.lessThan; @ESIntegTestCase.SuiteScopeTestCase @@ -84,6 +85,48 @@ public void setupSuiteScopeCluster() throws Exception { ensureSearchable(); } + public void testRandomSamplerConsistentSeed() { + double[] sampleMonotonicValue = new double[1]; + double[] sampleNumericValue = new double[1]; + long[] sampledDocCount = new long[1]; + // initialize the values + assertResponse( + prepareSearch("idx").setPreference("shard:0") + .addAggregation( + new RandomSamplerAggregationBuilder("sampler").setProbability(PROBABILITY) + .setSeed(0) + .subAggregation(avg("mean_monotonic").field(MONOTONIC_VALUE)) + .subAggregation(avg("mean_numeric").field(NUMERIC_VALUE)) + .setShardSeed(42) + ), + response -> { + InternalRandomSampler sampler = response.getAggregations().get("sampler"); + sampleMonotonicValue[0] = ((Avg) sampler.getAggregations().get("mean_monotonic")).getValue(); + sampleNumericValue[0] = ((Avg) sampler.getAggregations().get("mean_numeric")).getValue(); + sampledDocCount[0] = sampler.getDocCount(); + } + ); + + for (int i = 0; i < NUM_SAMPLE_RUNS; i++) { + assertResponse( + prepareSearch("idx").setPreference("shard:0") + .addAggregation( + new RandomSamplerAggregationBuilder("sampler").setProbability(PROBABILITY) + .setSeed(0) + .subAggregation(avg("mean_monotonic").field(MONOTONIC_VALUE)) + .subAggregation(avg("mean_numeric").field(NUMERIC_VALUE)) + .setShardSeed(42) + ), + response -> { + InternalRandomSampler sampler = response.getAggregations().get("sampler"); + assertThat(((Avg) sampler.getAggregations().get("mean_monotonic")).getValue(), equalTo(sampleMonotonicValue[0])); + assertThat(((Avg) sampler.getAggregations().get("mean_numeric")).getValue(), equalTo(sampleNumericValue[0])); + assertThat(sampler.getDocCount(), equalTo(sampledDocCount[0])); + } + ); + } + } + public void testRandomSampler() { double[] sampleMonotonicValue = new double[1]; double[] sampleNumericValue = new double[1]; diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index a6fa7a9ea8e99..c88b56ba25022 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -133,6 +133,7 @@ static TransportVersion def(int id) { public static final TransportVersion INDEX_REQUEST_NORMALIZED_BYTES_PARSED = def(8_593_00_0); public static final TransportVersion INGEST_GRAPH_STRUCTURE_EXCEPTION = def(8_594_00_0); public static final TransportVersion ML_MODEL_IN_SERVICE_SETTINGS = def(8_595_00_0); + public static final TransportVersion RANDOM_AGG_SHARD_SEED = def(8_596_00_0); /* * STOP! READ THIS FIRST! No, really, diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/InternalRandomSampler.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/InternalRandomSampler.java index 4dde9cc67b975..68a1a22369d2a 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/InternalRandomSampler.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/InternalRandomSampler.java @@ -8,6 +8,7 @@ package org.elasticsearch.search.aggregations.bucket.sampler.random; +import org.elasticsearch.TransportVersions; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.core.Releasables; @@ -29,18 +30,21 @@ public class InternalRandomSampler extends InternalSingleBucketAggregation imple public static final String PARSER_NAME = "random_sampler"; private final int seed; + private final Integer shardSeed; private final double probability; InternalRandomSampler( String name, long docCount, int seed, + Integer shardSeed, double probability, InternalAggregations subAggregations, Map metadata ) { super(name, docCount, subAggregations, metadata); this.seed = seed; + this.shardSeed = shardSeed; this.probability = probability; } @@ -51,6 +55,11 @@ public InternalRandomSampler(StreamInput in) throws IOException { super(in); this.seed = in.readInt(); this.probability = in.readDouble(); + if (in.getTransportVersion().onOrAfter(TransportVersions.RANDOM_AGG_SHARD_SEED)) { + this.shardSeed = in.readOptionalInt(); + } else { + this.shardSeed = null; + } } @Override @@ -58,6 +67,9 @@ protected void doWriteTo(StreamOutput out) throws IOException { super.doWriteTo(out); out.writeInt(seed); out.writeDouble(probability); + if (out.getTransportVersion().onOrAfter(TransportVersions.RANDOM_AGG_SHARD_SEED)) { + out.writeOptionalInt(shardSeed); + } } @Override @@ -72,7 +84,7 @@ public String getType() { @Override protected InternalSingleBucketAggregation newAggregation(String name, long docCount, InternalAggregations subAggregations) { - return new InternalRandomSampler(name, docCount, seed, probability, subAggregations, metadata); + return new InternalRandomSampler(name, docCount, seed, shardSeed, probability, subAggregations, metadata); } @Override @@ -105,12 +117,15 @@ public void close() { } public SamplingContext buildContext() { - return new SamplingContext(probability, seed); + return new SamplingContext(probability, seed, shardSeed); } @Override public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { builder.field(RandomSamplerAggregationBuilder.SEED.getPreferredName(), seed); + if (shardSeed != null) { + builder.field(RandomSamplerAggregationBuilder.SHARD_SEED.getPreferredName(), shardSeed); + } builder.field(RandomSamplerAggregationBuilder.PROBABILITY.getPreferredName(), probability); builder.field(CommonFields.DOC_COUNT.getPreferredName(), getDocCount()); getAggregations().toXContentInternal(builder, params); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregationBuilder.java index 240f016c66954..9bd9ab45b633a 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregationBuilder.java @@ -34,6 +34,7 @@ public class RandomSamplerAggregationBuilder extends AbstractAggregationBuilder< static final ParseField PROBABILITY = new ParseField("probability"); static final ParseField SEED = new ParseField("seed"); + static final ParseField SHARD_SEED = new ParseField("shard_seed"); public static final ObjectParser PARSER = ObjectParser.fromBuilder( RandomSamplerAggregationBuilder.NAME, @@ -41,10 +42,12 @@ public class RandomSamplerAggregationBuilder extends AbstractAggregationBuilder< ); static { PARSER.declareInt(RandomSamplerAggregationBuilder::setSeed, SEED); + PARSER.declareInt(RandomSamplerAggregationBuilder::setShardSeed, SHARD_SEED); PARSER.declareDouble(RandomSamplerAggregationBuilder::setProbability, PROBABILITY); } private int seed = Randomness.get().nextInt(); + private Integer shardSeed; private double p; public RandomSamplerAggregationBuilder(String name) { @@ -67,10 +70,18 @@ public RandomSamplerAggregationBuilder setSeed(int seed) { return this; } + public RandomSamplerAggregationBuilder setShardSeed(int shardSeed) { + this.shardSeed = shardSeed; + return this; + } + public RandomSamplerAggregationBuilder(StreamInput in) throws IOException { super(in); this.p = in.readDouble(); this.seed = in.readInt(); + if (in.getTransportVersion().onOrAfter(TransportVersions.RANDOM_AGG_SHARD_SEED)) { + this.shardSeed = in.readOptionalInt(); + } } protected RandomSamplerAggregationBuilder( @@ -81,12 +92,16 @@ protected RandomSamplerAggregationBuilder( super(clone, factoriesBuilder, metadata); this.p = clone.p; this.seed = clone.seed; + this.shardSeed = clone.shardSeed; } @Override protected void doWriteTo(StreamOutput out) throws IOException { out.writeDouble(p); out.writeInt(seed); + if (out.getTransportVersion().onOrAfter(TransportVersions.RANDOM_AGG_SHARD_SEED)) { + out.writeOptionalInt(shardSeed); + } } static void recursivelyCheckSubAggs(Collection builders, Consumer aggregationCheck) { @@ -128,7 +143,7 @@ protected AggregatorFactory doBuild( ); } }); - return new RandomSamplerAggregatorFactory(name, seed, p, context, parent, subfactoriesBuilder, metadata); + return new RandomSamplerAggregatorFactory(name, seed, shardSeed, p, context, parent, subfactoriesBuilder, metadata); } @Override @@ -136,6 +151,9 @@ protected XContentBuilder internalXContent(XContentBuilder builder, Params param builder.startObject(); builder.field(PROBABILITY.getPreferredName(), p); builder.field(SEED.getPreferredName(), seed); + if (shardSeed != null) { + builder.field(SHARD_SEED.getPreferredName(), shardSeed); + } builder.endObject(); return null; } @@ -162,7 +180,7 @@ public TransportVersion getMinimalSupportedVersion() { @Override public int hashCode() { - return Objects.hash(super.hashCode(), p, seed); + return Objects.hash(super.hashCode(), p, seed, shardSeed); } @Override @@ -171,6 +189,6 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) return false; if (super.equals(obj) == false) return false; RandomSamplerAggregationBuilder other = (RandomSamplerAggregationBuilder) obj; - return Objects.equals(p, other.p) && Objects.equals(seed, other.seed); + return Objects.equals(p, other.p) && Objects.equals(seed, other.seed) && Objects.equals(shardSeed, other.shardSeed); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregator.java index 8853733b9a158..a279b8270cd57 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregator.java @@ -30,12 +30,14 @@ public class RandomSamplerAggregator extends BucketsAggregator implements SingleBucketAggregator { private final int seed; + private final Integer shardSeed; private final double probability; private final CheckedSupplier weightSupplier; RandomSamplerAggregator( String name, int seed, + Integer shardSeed, double probability, CheckedSupplier weightSupplier, AggregatorFactories factories, @@ -53,6 +55,7 @@ public class RandomSamplerAggregator extends BucketsAggregator implements Single ); } this.weightSupplier = weightSupplier; + this.shardSeed = shardSeed; } @Override @@ -63,6 +66,7 @@ public InternalAggregation[] buildAggregations(long[] owningBucketOrds) throws I name, bucketDocCount(owningBucketOrd), seed, + shardSeed, probability, subAggregationResults, metadata() @@ -72,7 +76,7 @@ public InternalAggregation[] buildAggregations(long[] owningBucketOrds) throws I @Override public InternalAggregation buildEmptyAggregation() { - return new InternalRandomSampler(name, 0, seed, probability, buildEmptySubAggregations(), metadata()); + return new InternalRandomSampler(name, 0, seed, shardSeed, probability, buildEmptySubAggregations(), metadata()); } /** diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregatorFactory.java index d63f574b4d8bd..4be2e932179fe 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregatorFactory.java @@ -26,6 +26,7 @@ public class RandomSamplerAggregatorFactory extends AggregatorFactory { private final int seed; + private final Integer shardSeed; private final double probability; private final SamplingContext samplingContext; private Weight weight; @@ -33,6 +34,7 @@ public class RandomSamplerAggregatorFactory extends AggregatorFactory { RandomSamplerAggregatorFactory( String name, int seed, + Integer shardSeed, double probability, AggregationContext context, AggregatorFactory parent, @@ -42,7 +44,8 @@ public class RandomSamplerAggregatorFactory extends AggregatorFactory { super(name, context, parent, subFactories, metadata); this.probability = probability; this.seed = seed; - this.samplingContext = new SamplingContext(probability, seed); + this.samplingContext = new SamplingContext(probability, seed, shardSeed); + this.shardSeed = shardSeed; } @Override @@ -53,7 +56,18 @@ public Optional getSamplingContext() { @Override public Aggregator createInternal(Aggregator parent, CardinalityUpperBound cardinality, Map metadata) throws IOException { - return new RandomSamplerAggregator(name, seed, probability, this::getWeight, factories, context, parent, cardinality, metadata); + return new RandomSamplerAggregator( + name, + seed, + shardSeed, + probability, + this::getWeight, + factories, + context, + parent, + cardinality, + metadata + ); } /** @@ -66,7 +80,11 @@ public Aggregator createInternal(Aggregator parent, CardinalityUpperBound cardin */ private Weight getWeight() throws IOException { if (weight == null) { - RandomSamplingQuery query = new RandomSamplingQuery(probability, seed, context.shardRandomSeed()); + RandomSamplingQuery query = new RandomSamplingQuery( + probability, + seed, + shardSeed == null ? context.shardRandomSeed() : shardSeed + ); BooleanQuery booleanQuery = new BooleanQuery.Builder().add(query, BooleanClause.Occur.FILTER) .add(context.query(), BooleanClause.Occur.FILTER) .build(); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/support/SamplingContext.java b/server/src/main/java/org/elasticsearch/search/aggregations/support/SamplingContext.java index 57ea138f63268..d8f34bfcf9973 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/support/SamplingContext.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/support/SamplingContext.java @@ -20,8 +20,9 @@ /** * This provides information around the current sampling context for aggregations */ -public record SamplingContext(double probability, int seed) { - public static final SamplingContext NONE = new SamplingContext(1.0, 0); +public record SamplingContext(double probability, int seed, Integer shardSeed) { + + public static final SamplingContext NONE = new SamplingContext(1.0, 0, null); public boolean isSampled() { return probability < 1.0; @@ -97,20 +98,22 @@ public Query buildQueryWithSampler(QueryBuilder builder, AggregationContext cont } BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder(); queryBuilder.add(rewritten, BooleanClause.Occur.FILTER); - queryBuilder.add(new RandomSamplingQuery(probability(), seed(), context.shardRandomSeed()), BooleanClause.Occur.FILTER); + queryBuilder.add( + new RandomSamplingQuery(probability(), seed(), shardSeed == null ? context.shardRandomSeed() : shardSeed), + BooleanClause.Occur.FILTER + ); return queryBuilder.build(); } /** * @param context The current aggregation context * @return the sampling query if the sampling context indicates that sampling is required - * @throws IOException thrown on query build failure */ - public Optional buildSamplingQueryIfNecessary(AggregationContext context) throws IOException { + public Optional buildSamplingQueryIfNecessary(AggregationContext context) { if (isSampled() == false) { return Optional.empty(); } - return Optional.of(new RandomSamplingQuery(probability(), seed(), context.shardRandomSeed())); + return Optional.of(new RandomSamplingQuery(probability(), seed(), shardSeed == null ? context.shardRandomSeed() : shardSeed)); } } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregationBuilderTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregationBuilderTests.java index 5514cb441b54c..18808f9b2aa87 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregationBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregationBuilderTests.java @@ -19,6 +19,9 @@ protected RandomSamplerAggregationBuilder createTestAggregatorBuilder() { if (randomBoolean()) { builder.setSeed(randomInt()); } + if (randomBoolean()) { + builder.setShardSeed(randomInt()); + } builder.setProbability(randomFrom(1.0, randomDoubleBetween(0.0, 0.5, false))); builder.subAggregation(AggregationBuilders.max(randomAlphaOfLength(10)).field(randomAlphaOfLength(10))); return builder; diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/support/SamplingContextTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/support/SamplingContextTests.java index d9e19cf60e481..ffb56f17c7f8f 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/support/SamplingContextTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/support/SamplingContextTests.java @@ -14,10 +14,9 @@ import static org.hamcrest.Matchers.equalTo; public class SamplingContextTests extends ESTestCase { - protected static final int NUMBER_OF_TEST_RUNS = 20; private static SamplingContext randomContext() { - return new SamplingContext(randomDoubleBetween(1e-6, 0.1, false), randomInt()); + return new SamplingContext(randomDoubleBetween(1e-6, 0.1, false), randomInt(), randomBoolean() ? null : randomInt()); } public void testScaling() { @@ -41,7 +40,7 @@ public void testScaling() { } public void testNoScaling() { - SamplingContext samplingContext = new SamplingContext(1.0, randomInt()); + SamplingContext samplingContext = new SamplingContext(1.0, randomInt(), randomBoolean() ? null : randomInt()); long randomLong = randomLong(); double randomDouble = randomDouble(); assertThat(randomLong, equalTo(samplingContext.scaleDown(randomLong))); diff --git a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java index 99734e5e224aa..1787638f9fdf3 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java @@ -1114,7 +1114,11 @@ public void testSupportedFieldTypes() throws IOException { // We should make sure if the builder says it supports sampling, that the internal aggregations returned override // finalizeSampling if (aggregationBuilder.supportsSampling()) { - SamplingContext randomSamplingContext = new SamplingContext(randomDoubleBetween(1e-8, 0.1, false), randomInt()); + SamplingContext randomSamplingContext = new SamplingContext( + randomDoubleBetween(1e-8, 0.1, false), + randomInt(), + randomBoolean() ? null : randomInt() + ); InternalAggregation sampledResult = internalAggregation.finalizeSampling(randomSamplingContext); assertThat(sampledResult.getClass(), equalTo(internalAggregation.getClass())); } diff --git a/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java index f1b147eefe723..12c5085cbcd73 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalAggregationTestCase.java @@ -283,7 +283,11 @@ public void testReduceRandom() throws IOException { doAssertReducedMultiBucketConsumer(reduced, bucketConsumer); assertReduced(reduced, inputs.toReduce()); if (supportsSampling()) { - SamplingContext randomContext = new SamplingContext(randomDoubleBetween(1e-8, 0.1, false), randomInt()); + SamplingContext randomContext = new SamplingContext( + randomDoubleBetween(1e-8, 0.1, false), + randomInt(), + randomBoolean() ? null : randomInt() + ); @SuppressWarnings("unchecked") T sampled = (T) reduced.finalizeSampling(randomContext); assertSampled(sampled, reduced, randomContext); From d37d93ac36a04ca757a8818f4ed870f7e53b11f9 Mon Sep 17 00:00:00 2001 From: florent-leborgne Date: Fri, 23 Feb 2024 15:13:51 +0100 Subject: [PATCH 172/250] [Docs] [Remote Clusters] Note about certificates in ESS for Remote Cluster Security (#105771) * note about ess certificates * Update docs/reference/modules/cluster/remote-clusters-api-key.asciidoc Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> --------- Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> --- .../reference/modules/cluster/remote-clusters-api-key.asciidoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference/modules/cluster/remote-clusters-api-key.asciidoc b/docs/reference/modules/cluster/remote-clusters-api-key.asciidoc index b95ebdf143a57..0cac52deaae4b 100644 --- a/docs/reference/modules/cluster/remote-clusters-api-key.asciidoc +++ b/docs/reference/modules/cluster/remote-clusters-api-key.asciidoc @@ -62,6 +62,9 @@ information, refer to https://www.elastic.co/subscriptions. [[remote-clusters-security-api-key]] ==== Establish trust with a remote cluster +NOTE: If a remote cluster is part of an {ess} deployment, it has a valid certificate by default. +You can therefore skip steps related to certificates in these instructions. + ===== On the remote cluster // tag::remote-cluster-steps[] From f86532b552b7c7645b562cf643199702c600f7f6 Mon Sep 17 00:00:00 2001 From: Pat Whelan Date: Fri, 23 Feb 2024 09:28:22 -0500 Subject: [PATCH 173/250] [Transform] Retry destination index creation (#105759) For Unattended Transforms, if we fail to create the destination index on the first run, we will retry the transformation iteration, but we will not retry the destination index creation on that next iteration. This change stops the Unattended Transform from progressing beyond the 0th checkpoint, so all retries will include the destination index creation. Fix #105683 Relate #104146 --- .../TransformInsufficientPermissionsIT.java | 10 +++++----- .../integration/TransformRestTestCase.java | 17 +++++++++-------- .../transform/transforms/TransformIndexer.java | 9 +++++---- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformInsufficientPermissionsIT.java b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformInsufficientPermissionsIT.java index dc48ceb7b309b..59a673790723e 100644 --- a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformInsufficientPermissionsIT.java +++ b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformInsufficientPermissionsIT.java @@ -418,16 +418,14 @@ public void testTransformPermissionsDeferUnattendedNoDest() throws Exception { ); assertRed(transformId, authIssue); - startTransform(config.getId(), RequestOptions.DEFAULT); - - // Give the transform indexer enough time to try creating destination index - Thread.sleep(5_000); + startTransform(transformId, RequestOptions.DEFAULT); String destIndexIssue = Strings.format("Could not create destination index [%s] for transform [%s]", destIndexName, transformId); // transform's auth state status is still RED due to: // - lacking permissions // - and the inability to create destination index in the indexer (which is also a consequence of lacking permissions) - assertRed(transformId, authIssue, destIndexIssue); + // wait for 10 seconds to give the transform indexer enough time to try creating destination index + assertBusy(() -> { assertRed(transformId, authIssue, destIndexIssue); }); // update transform's credentials so that the transform has permission to access source/dest indices updateConfig(transformId, "{}", RequestOptions.DEFAULT.toBuilder().addHeader(AUTH_KEY, Users.SENIOR.header).build()); @@ -593,5 +591,7 @@ private void assertRed(String transformId, String... expectedHealthIssueDetails) .map(issue -> (String) extractValue((Map) issue, "details")) .collect(toSet()); assertThat("Stats were: " + stats, actualHealthIssueDetailsSet, containsInAnyOrder(expectedHealthIssueDetails)); + // We should not progress beyond the 0th checkpoint until we correctly configure the Transform. + assertThat("Stats were: " + stats, getCheckpoint(stats), equalTo(0L)); } } diff --git a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformRestTestCase.java b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformRestTestCase.java index eed849d35ea44..897de6c120a8b 100644 --- a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformRestTestCase.java +++ b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformRestTestCase.java @@ -294,14 +294,15 @@ protected void waitUntilCheckpoint(String id, long checkpoint) throws Exception } protected void waitUntilCheckpoint(String id, long checkpoint, TimeValue waitTime) throws Exception { - assertBusy( - () -> assertEquals( - checkpoint, - ((Integer) XContentMapValues.extractValue("checkpointing.last.checkpoint", getBasicTransformStats(id))).longValue() - ), - waitTime.getMillis(), - TimeUnit.MILLISECONDS - ); + assertBusy(() -> assertEquals(checkpoint, getCheckpoint(id)), waitTime.getMillis(), TimeUnit.MILLISECONDS); + } + + protected long getCheckpoint(String id) throws IOException { + return getCheckpoint(getBasicTransformStats(id)); + } + + protected long getCheckpoint(Map stats) { + return ((Integer) XContentMapValues.extractValue("checkpointing.last.checkpoint", stats)).longValue(); } protected DateHistogramGroupSource createDateHistogramGroupSourceWithFixedInterval( diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformIndexer.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformIndexer.java index 4b2da731351d7..ff52f5e267655 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformIndexer.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformIndexer.java @@ -333,6 +333,9 @@ protected void onStart(long now, ActionListener listener) { } }, listener::onFailure); + var shouldMaybeCreateDestIndexForUnattended = context.getCheckpoint() == 0 + && Boolean.TRUE.equals(transformConfig.getSettings().getUnattended()); + ActionListener> fieldMappingsListener = ActionListener.wrap(destIndexMappings -> { if (destIndexMappings.isEmpty() == false) { // If we managed to fetch destination index mappings, we use them from now on ... @@ -344,9 +347,7 @@ protected void onStart(long now, ActionListener listener) { // Since the unattended transform could not have created the destination index yet, we do it here. // This is important to create the destination index explicitly before indexing first documents. Otherwise, the destination // index aliases may be missing. - if (destIndexMappings.isEmpty() - && context.getCheckpoint() == 0 - && Boolean.TRUE.equals(transformConfig.getSettings().getUnattended())) { + if (destIndexMappings.isEmpty() && shouldMaybeCreateDestIndexForUnattended) { doMaybeCreateDestIndex(deducedDestIndexMappings.get(), configurationReadyListener); } else { configurationReadyListener.onResponse(null); @@ -364,7 +365,7 @@ protected void onStart(long now, ActionListener listener) { deducedDestIndexMappings.set(validationResponse.getDestIndexMappings()); if (isContinuous()) { transformsConfigManager.getTransformConfiguration(getJobId(), ActionListener.wrap(config -> { - if (transformConfig.equals(config) && fieldMappings != null) { + if (transformConfig.equals(config) && fieldMappings != null && shouldMaybeCreateDestIndexForUnattended == false) { logger.trace("[{}] transform config has not changed.", getJobId()); configurationReadyListener.onResponse(null); } else { From e568f7038daa8791f6f5db92e514cf9d8408418b Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 23 Feb 2024 10:28:00 -0500 Subject: [PATCH 174/250] [ci] Attach correct build artifact link to build scan when multiple are uploaded (#105530) --- .../ElasticsearchBuildCompletePlugin.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchBuildCompletePlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchBuildCompletePlugin.java index f0604ab33ceec..e0588ed440c57 100644 --- a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchBuildCompletePlugin.java +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/ElasticsearchBuildCompletePlugin.java @@ -32,7 +32,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Optional; import javax.inject.Inject; @@ -142,6 +144,8 @@ public void execute(BuildFinishedFlowAction.Parameters parameters) throws FileNo System.out.println("Generating buildscan link for artifact..."); + // Output should be in the format: "\n" + // and multiple artifacts could be returned Process process = new ProcessBuilder( "buildkite-agent", "artifact", @@ -150,7 +154,7 @@ public void execute(BuildFinishedFlowAction.Parameters parameters) throws FileNo "--step", System.getenv("BUILDKITE_JOB_ID"), "--format", - "%i" + "%i %c" ).start(); process.waitFor(); String processOutput; @@ -159,7 +163,17 @@ public void execute(BuildFinishedFlowAction.Parameters parameters) throws FileNo } catch (IOException e) { processOutput = ""; } - String artifactUuid = processOutput.trim(); + + // Sort them by timestamp, and grab the most recent one + Optional artifact = Arrays.stream(processOutput.trim().split("\n")).map(String::trim).min((a, b) -> { + String[] partsA = a.split(" "); + String[] partsB = b.split(" "); + // ISO-8601 timestamps can be sorted lexicographically + return partsB[1].compareTo(partsA[1]); + }); + + // Grab just the UUID from the artifact + String artifactUuid = artifact.orElse("").split(" ")[0]; System.out.println("Artifact UUID: " + artifactUuid); if (artifactUuid.isEmpty() == false) { From e0d2616c3b522009b891403ca5e298cbd1d1b426 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Fri, 23 Feb 2024 11:17:42 -0500 Subject: [PATCH 175/250] CCS with minimize_roundtrips performs incremental merges of each SearchResponse (#105781) This restores the functionality that was removed from 8.13 (waiting for a change on the Kibana side). The work for this feature was added in #103134 but we had to remove the yaml changelog when we turned off the functionality in #105455. So this PR restores the changelog yaml as well. --- docs/changelog/105781.yaml | 5 +++++ .../xpack/search/MutableSearchResponse.java | 10 ++++------ 2 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 docs/changelog/105781.yaml diff --git a/docs/changelog/105781.yaml b/docs/changelog/105781.yaml new file mode 100644 index 0000000000000..c3ae7f0035904 --- /dev/null +++ b/docs/changelog/105781.yaml @@ -0,0 +1,5 @@ +pr: 105781 +summary: CCS with `minimize_roundtrips` performs incremental merges of each `SearchResponse` +area: Search +type: enhancement +issues: [] diff --git a/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/MutableSearchResponse.java b/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/MutableSearchResponse.java index 7f3099917e9ec..2d0e2295eb859 100644 --- a/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/MutableSearchResponse.java +++ b/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/MutableSearchResponse.java @@ -306,12 +306,10 @@ synchronized AsyncSearchResponse toAsyncSearchResponse(AsyncSearchTask task, lon * (for local-only/CCS minimize_roundtrips=false) */ private SearchResponseMerger createSearchResponseMerger(AsyncSearchTask task) { - return null; - // TODO uncomment this code once Kibana moves to polling the _async_search/status endpoint to determine if a search is done - // if (task.getSearchResponseMergerSupplier() == null) { - // return null; // local search and CCS minimize_roundtrips=false - // } - // return task.getSearchResponseMergerSupplier().get(); + if (task.getSearchResponseMergerSupplier() == null) { + return null; // local search and CCS minimize_roundtrips=false + } + return task.getSearchResponseMergerSupplier().get(); } private SearchResponse getMergedResponse(SearchResponseMerger merger) { From 3b2b9b35db8ec6e728c2ffe26243df0934bf1bb2 Mon Sep 17 00:00:00 2001 From: Tanguy Leroux Date: Fri, 23 Feb 2024 17:29:26 +0100 Subject: [PATCH 176/250] Assert that CacheFile does not read beyond file length (#105749) In #105570 we allowed classes extending SharedBlobCacheService to override the method to compute the size of cached regions that can be written. This allows implementations to use the full region size whatever the length of the file/blob to cache is, and ultimately it allows to append more bytes to the last cached region of an already cached file. While the underlying SparseFileTracker already makes assertions about accessing bytes within the size of the cached region, we are lacking assertions about CacheFile not accessing bytes beyond the end of file. This pull request adds those assertions, in a way that classes extending SharedBlobCacheService can disable the assertion when needed. --- .../shared/SharedBlobCacheService.java | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/shared/SharedBlobCacheService.java b/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/shared/SharedBlobCacheService.java index f2ebe61906258..1f6f075a2b2af 100644 --- a/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/shared/SharedBlobCacheService.java +++ b/x-pack/plugin/blob-cache/src/main/java/org/elasticsearch/blobcache/shared/SharedBlobCacheService.java @@ -398,23 +398,23 @@ public int getRecoveryRangeSize() { return recoveryRangeSize; } - private int getRegion(long position) { + protected int getRegion(long position) { return (int) (position / regionSize); } - private int getRegionRelativePosition(long position) { + protected int getRegionRelativePosition(long position) { return (int) (position % regionSize); } - private long getRegionStart(int region) { + protected long getRegionStart(int region) { return (long) region * regionSize; } - private long getRegionEnd(int region) { + protected long getRegionEnd(int region) { return (long) (region + 1) * regionSize; } - private int getEndingRegion(long position) { + protected int getEndingRegion(long position) { return getRegion(position - (position % regionSize == 0 ? 1 : 0)); } @@ -683,6 +683,23 @@ public final boolean isEvicted() { } } + protected boolean assertOffsetsWithinFileLength(long offset, long length, long fileLength) { + assert offset >= 0L; + assert length > 0L; + assert fileLength > 0L; + assert offset + length <= fileLength + : "accessing [" + + length + + "] bytes at offset [" + + offset + + "] in cache file [" + + this + + "] would be beyond file length [" + + fileLength + + ']'; + return true; + } + /** * While this class has incRef and tryIncRef methods, incRefEnsureOpen and tryIncrefEnsureOpen should * always be used, ensuring the right ordering between incRef/tryIncRef and ensureOpen @@ -955,6 +972,7 @@ public KeyType getCacheKey() { } public boolean tryRead(ByteBuffer buf, long offset) throws IOException { + assert assertOffsetsWithinFileLength(offset, buf.remaining(), length); final int startRegion = getRegion(offset); final long end = offset + buf.remaining(); final int endRegion = getEndingRegion(end); @@ -984,6 +1002,8 @@ public int populateAndRead( final RangeAvailableHandler reader, final RangeMissingHandler writer ) throws Exception { + assert assertOffsetsWithinFileLength(rangeToWrite.start(), rangeToWrite.length(), length); + assert assertOffsetsWithinFileLength(rangeToRead.start(), rangeToRead.length(), length); // We are interested in the total time that the system spends when fetching a result (including time spent queuing), so we start // our measurement here. final long startTime = threadPool.relativeTimeInNanos(); From bc47d185992d07c596469600b2592684885dbffe Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Fri, 23 Feb 2024 16:43:26 +0000 Subject: [PATCH 177/250] Convert uses of map/set creation using a subclass to static creation methods (#105767) --- .../NetworkDirectionProcessorTests.java | 19 +-- .../RegisteredDomainProcessorTests.java | 7 +- .../action/bulk/SimulateBulkRequestTests.java | 35 +---- .../cluster/ClusterStateTests.java | 69 +++------- .../coordination/CoordinatorTests.java | 32 ++--- .../metadata/ToAndFromJsonMetadataTests.java | 124 +++++++---------- .../ingest/SimulateIngestServiceTests.java | 82 +++++------ .../elasticsearch/rest/RestRequestTests.java | 6 +- .../core/ml/datafeed/AggProviderTests.java | 120 ++++++---------- .../analyses/ClassificationTests.java | 26 ++-- .../inference/InferenceDefinitionTests.java | 27 +--- .../transforms/SourceConfigTests.java | 28 ++-- .../ml/integration/InferenceIngestIT.java | 21 +-- .../xpack/sql/qa/jdbc/ResultSetTestCase.java | 26 ++-- .../xpack/sql/qa/rest/RestSqlTestCase.java | 29 ++-- .../sql/optimizer/OptimizerRunTests.java | 28 ++-- .../TransformUsingSearchRuntimeFieldsIT.java | 128 ++++++++---------- .../action/TransformConfigLinterTests.java | 32 ++--- .../persistence/TransformIndexTests.java | 99 ++++++-------- .../common/DocumentConversionUtilsTests.java | 26 ++-- .../transforms/pivot/SchemaUtilTests.java | 69 ++++------ 21 files changed, 400 insertions(+), 633 deletions(-) diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/NetworkDirectionProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/NetworkDirectionProcessorTests.java index 7c53df0ca3f45..72e10e5ba3711 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/NetworkDirectionProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/NetworkDirectionProcessorTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,20 +34,10 @@ private Map buildEvent(String source) { } private Map buildEvent(String source, String destination) { - return new HashMap<>() { - { - put("source", new HashMap() { - { - put("ip", source); - } - }); - put("destination", new HashMap() { - { - put("ip", destination); - } - }); - } - }; + Map event = new HashMap<>(); + event.put("source", Collections.singletonMap("ip", source)); + event.put("destination", Collections.singletonMap("ip", destination)); + return event; } public void testNoInternalNetworks() throws Exception { diff --git a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RegisteredDomainProcessorTests.java b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RegisteredDomainProcessorTests.java index 3f706a8925810..7d6fa99f81580 100644 --- a/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RegisteredDomainProcessorTests.java +++ b/modules/ingest-common/src/test/java/org/elasticsearch/ingest/common/RegisteredDomainProcessorTests.java @@ -12,7 +12,6 @@ import org.elasticsearch.ingest.TestIngestDocument; import org.elasticsearch.test.ESTestCase; -import java.util.HashMap; import java.util.Map; import static org.hamcrest.Matchers.containsString; @@ -27,11 +26,7 @@ */ public class RegisteredDomainProcessorTests extends ESTestCase { private Map buildEvent(String domain) { - return new HashMap<>() { - { - put("domain", domain); - } - }; + return Map.of("domain", domain); } public void testBasic() throws Exception { diff --git a/server/src/test/java/org/elasticsearch/action/bulk/SimulateBulkRequestTests.java b/server/src/test/java/org/elasticsearch/action/bulk/SimulateBulkRequestTests.java index 7fe036f97596e..e7e922c47acbe 100644 --- a/server/src/test/java/org/elasticsearch/action/bulk/SimulateBulkRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/bulk/SimulateBulkRequestTests.java @@ -11,7 +11,6 @@ import org.elasticsearch.test.ESTestCase; import java.io.IOException; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,32 +34,12 @@ private void testSerialization(Map> pipelineSubstitu assertThat(copy.getPipelineSubstitutions(), equalTo(simulateBulkRequest.getPipelineSubstitutions())); } - private Map> getTestPipelineSubstitutions() { - return new HashMap<>() { - { - put("pipeline1", new HashMap<>() { - { - put("processors", List.of(new HashMap<>() { - { - put("processor2", new HashMap<>()); - } - }, new HashMap<>() { - { - put("processor3", new HashMap<>()); - } - })); - } - }); - put("pipeline2", new HashMap<>() { - { - put("processors", List.of(new HashMap<>() { - { - put("processor3", new HashMap<>()); - } - })); - } - }); - } - }; + private static Map> getTestPipelineSubstitutions() { + return Map.of( + "pipeline1", + Map.of("processors", List.of(Map.of("processor2", Map.of()), Map.of("processor3", Map.of()))), + "pipeline2", + Map.of("processors", List.of(Map.of("processor3", Map.of()))) + ); } } diff --git a/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java b/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java index e0538603573f7..ee98f40a6cb29 100644 --- a/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java @@ -61,7 +61,6 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -396,13 +395,14 @@ public void testToXContent() throws IOException { } public void testToXContent_FlatSettingTrue_ReduceMappingFalse() throws IOException { - Map mapParams = new HashMap<>() { - { - put("flat_settings", "true"); - put("reduce_mappings", "false"); - put(Metadata.CONTEXT_MODE_PARAM, Metadata.CONTEXT_MODE_API); - } - }; + Map mapParams = Map.of( + "flat_settings", + "true", + "reduce_mappings", + "false", + Metadata.CONTEXT_MODE_PARAM, + Metadata.CONTEXT_MODE_API + ); final ClusterState clusterState = buildClusterState(); IndexRoutingTable index = clusterState.getRoutingTable().getIndicesRouting().get("index"); @@ -661,13 +661,14 @@ public void testToXContent_FlatSettingTrue_ReduceMappingFalse() throws IOExcepti } public void testToXContent_FlatSettingFalse_ReduceMappingTrue() throws IOException { - Map mapParams = new HashMap<>() { - { - put("flat_settings", "false"); - put("reduce_mappings", "true"); - put(Metadata.CONTEXT_MODE_PARAM, Metadata.CONTEXT_MODE_API); - } - }; + Map mapParams = Map.of( + "flat_settings", + "false", + "reduce_mappings", + "true", + Metadata.CONTEXT_MODE_PARAM, + Metadata.CONTEXT_MODE_API + ); final ClusterState clusterState = buildClusterState(); @@ -948,15 +949,7 @@ public void testToXContentSameTypeName() throws IOException { "type", // the type name is the root value, // the original logic in ClusterState.toXContent will reduce - new HashMap<>() { - { - put("type", new HashMap() { - { - put("key", "value"); - } - }); - } - } + Map.of("type", Map.of("key", "value")) ) ) .numberOfShards(1) @@ -1086,23 +1079,11 @@ private ClusterState buildClusterState() throws IOException { IndexMetadata indexMetadata = IndexMetadata.builder("index") .state(IndexMetadata.State.OPEN) .settings(Settings.builder().put(SETTING_VERSION_CREATED, IndexVersion.current())) - .putMapping(new MappingMetadata("type", new HashMap<>() { - { - put("type1", new HashMap() { - { - put("key", "value"); - } - }); - } - })) + .putMapping(new MappingMetadata("type", Map.of("type1", Map.of("key", "value")))) .putAlias(AliasMetadata.builder("alias").indexRouting("indexRouting").build()) .numberOfShards(1) .primaryTerm(0, 1L) - .putInSyncAllocationIds(0, new HashSet<>() { - { - add("allocationId"); - } - }) + .putInSyncAllocationIds(0, Set.of("allocationId")) .numberOfReplicas(2) .putRolloverInfo(new RolloverInfo("rolloveAlias", new ArrayList<>(), 1L)) .stats(new IndexMetadataStats(IndexWriteLoad.builder(1).build(), 120, 1)) @@ -1150,16 +1131,8 @@ private ClusterState buildClusterState() throws IOException { .coordinationMetadata( CoordinationMetadata.builder() .term(1) - .lastCommittedConfiguration(new CoordinationMetadata.VotingConfiguration(new HashSet<>() { - { - add("commitedConfigurationNodeId"); - } - })) - .lastAcceptedConfiguration(new CoordinationMetadata.VotingConfiguration(new HashSet<>() { - { - add("acceptedConfigurationNodeId"); - } - })) + .lastCommittedConfiguration(new CoordinationMetadata.VotingConfiguration(Set.of("commitedConfigurationNodeId"))) + .lastAcceptedConfiguration(new CoordinationMetadata.VotingConfiguration(Set.of("acceptedConfigurationNodeId"))) .addVotingConfigExclusion(new CoordinationMetadata.VotingConfigExclusion("exlucdedNodeId", "excludedNodeName")) .build() ) diff --git a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java index 3fc62981b75ba..2985cd33aaa64 100644 --- a/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/coordination/CoordinatorTests.java @@ -1885,31 +1885,23 @@ public void testImproveConfigurationPerformsVotingConfigExclusionStateCheck() { final Coordinator coordinator = cluster.getAnyLeader().coordinator; final ClusterState currentState = coordinator.getLastAcceptedState(); - Set newVotingConfigExclusion1 = new HashSet<>() { - { - add( - new CoordinationMetadata.VotingConfigExclusion( - "resolvableNodeId", - CoordinationMetadata.VotingConfigExclusion.MISSING_VALUE_MARKER - ) - ); - } - }; + Set newVotingConfigExclusion1 = Set.of( + new CoordinationMetadata.VotingConfigExclusion( + "resolvableNodeId", + CoordinationMetadata.VotingConfigExclusion.MISSING_VALUE_MARKER + ) + ); ClusterState newState1 = buildNewClusterStateWithVotingConfigExclusion(currentState, newVotingConfigExclusion1); assertFalse(Coordinator.validVotingConfigExclusionState(newState1)); - Set newVotingConfigExclusion2 = new HashSet<>() { - { - add( - new CoordinationMetadata.VotingConfigExclusion( - CoordinationMetadata.VotingConfigExclusion.MISSING_VALUE_MARKER, - "resolvableNodeName" - ) - ); - } - }; + Set newVotingConfigExclusion2 = Set.of( + new CoordinationMetadata.VotingConfigExclusion( + CoordinationMetadata.VotingConfigExclusion.MISSING_VALUE_MARKER, + "resolvableNodeName" + ) + ); ClusterState newState2 = buildNewClusterStateWithVotingConfigExclusion(currentState, newVotingConfigExclusion2); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java index e7f49bc773404..aa9d0b9368fa6 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java @@ -23,12 +23,9 @@ import org.elasticsearch.xcontent.json.JsonXContent; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -219,13 +216,14 @@ public void testSimpleJsonFromAndTo() throws IOException { private static final String ALIAS_FILTER2 = "{\"field2\":\"value2\"}"; public void testToXContentGateway_FlatSettingTrue_ReduceMappingFalse() throws IOException { - Map mapParams = new HashMap<>() { - { - put(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_GATEWAY); - put("flat_settings", "true"); - put("reduce_mappings", "false"); - } - }; + Map mapParams = Map.of( + Metadata.CONTEXT_MODE_PARAM, + CONTEXT_MODE_GATEWAY, + "flat_settings", + "true", + "reduce_mappings", + "false" + ); Metadata metadata = buildMetadata(); XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); @@ -282,11 +280,7 @@ public void testToXContentGateway_FlatSettingTrue_ReduceMappingFalse() throws IO } public void testToXContentAPI_SameTypeName() throws IOException { - Map mapParams = new HashMap<>() { - { - put(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_API); - } - }; + Map mapParams = Map.of(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_API); Metadata metadata = Metadata.builder() .clusterUUID("clusterUUID") @@ -300,15 +294,7 @@ public void testToXContentAPI_SameTypeName() throws IOException { "type", // the type name is the root value, // the original logic in ClusterState.toXContent will reduce - new HashMap<>() { - { - put("type", new HashMap() { - { - put("key", "value"); - } - }); - } - } + Map.of("type", Map.of("key", "value")) ) ) .numberOfShards(1) @@ -378,13 +364,14 @@ public void testToXContentAPI_SameTypeName() throws IOException { } public void testToXContentGateway_FlatSettingFalse_ReduceMappingTrue() throws IOException { - Map mapParams = new HashMap<>() { - { - put(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_GATEWAY); - put("flat_settings", "false"); - put("reduce_mappings", "true"); - } - }; + Map mapParams = Map.of( + Metadata.CONTEXT_MODE_PARAM, + CONTEXT_MODE_GATEWAY, + "flat_settings", + "false", + "reduce_mappings", + "true" + ); Metadata metadata = buildMetadata(); XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); @@ -443,13 +430,14 @@ public void testToXContentGateway_FlatSettingFalse_ReduceMappingTrue() throws IO } public void testToXContentAPI_FlatSettingTrue_ReduceMappingFalse() throws IOException { - Map mapParams = new HashMap<>() { - { - put(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_API); - put("flat_settings", "true"); - put("reduce_mappings", "false"); - } - }; + Map mapParams = Map.of( + Metadata.CONTEXT_MODE_PARAM, + CONTEXT_MODE_API, + "flat_settings", + "true", + "reduce_mappings", + "false" + ); final Metadata metadata = buildMetadata(); @@ -546,13 +534,14 @@ public void testToXContentAPI_FlatSettingTrue_ReduceMappingFalse() throws IOExce } public void testToXContentAPI_FlatSettingFalse_ReduceMappingTrue() throws IOException { - Map mapParams = new HashMap<>() { - { - put(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_API); - put("flat_settings", "false"); - put("reduce_mappings", "true"); - } - }; + Map mapParams = Map.of( + Metadata.CONTEXT_MODE_PARAM, + CONTEXT_MODE_API, + "flat_settings", + "false", + "reduce_mappings", + "true" + ); final Metadata metadata = buildMetadata(); @@ -655,13 +644,14 @@ public void testToXContentAPI_FlatSettingFalse_ReduceMappingTrue() throws IOExce } public void testToXContentAPIReservedMetadata() throws IOException { - Map mapParams = new HashMap<>() { - { - put(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_API); - put("flat_settings", "false"); - put("reduce_mappings", "true"); - } - }; + Map mapParams = Map.of( + Metadata.CONTEXT_MODE_PARAM, + CONTEXT_MODE_API, + "flat_settings", + "false", + "reduce_mappings", + "true" + ); Metadata metadata = buildMetadata(); @@ -840,16 +830,8 @@ private Metadata buildMetadata() throws IOException { .coordinationMetadata( CoordinationMetadata.builder() .term(1) - .lastCommittedConfiguration(new CoordinationMetadata.VotingConfiguration(new HashSet<>() { - { - add("commitedConfigurationNodeId"); - } - })) - .lastAcceptedConfiguration(new CoordinationMetadata.VotingConfiguration(new HashSet<>() { - { - add("acceptedConfigurationNodeId"); - } - })) + .lastCommittedConfiguration(new CoordinationMetadata.VotingConfiguration(Set.of("commitedConfigurationNodeId"))) + .lastAcceptedConfiguration(new CoordinationMetadata.VotingConfiguration(Set.of("acceptedConfigurationNodeId"))) .addVotingConfigExclusion(new CoordinationMetadata.VotingConfigExclusion("exlucdedNodeId", "excludedNodeName")) .build() ) @@ -859,25 +841,13 @@ private Metadata buildMetadata() throws IOException { IndexMetadata.builder("index") .state(IndexMetadata.State.OPEN) .settings(Settings.builder().put(SETTING_VERSION_CREATED, IndexVersion.current())) - .putMapping(new MappingMetadata("type", new HashMap<>() { - { - put("type1", new HashMap() { - { - put("key", "value"); - } - }); - } - })) + .putMapping(new MappingMetadata("type", Map.of("type1", Map.of("key", "value")))) .putAlias(AliasMetadata.builder("alias").indexRouting("indexRouting").build()) .numberOfShards(1) .primaryTerm(0, 1L) - .putInSyncAllocationIds(0, new HashSet<>() { - { - add("allocationId"); - } - }) + .putInSyncAllocationIds(0, Set.of("allocationId")) .numberOfReplicas(2) - .putRolloverInfo(new RolloverInfo("rolloveAlias", new ArrayList<>(), 1L)) + .putRolloverInfo(new RolloverInfo("rolloveAlias", List.of(), 1L)) ) .put( IndexTemplateMetadata.builder("template") diff --git a/server/src/test/java/org/elasticsearch/ingest/SimulateIngestServiceTests.java b/server/src/test/java/org/elasticsearch/ingest/SimulateIngestServiceTests.java index 3ef1b1983df8b..30145ab37c322 100644 --- a/server/src/test/java/org/elasticsearch/ingest/SimulateIngestServiceTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/SimulateIngestServiceTests.java @@ -19,10 +19,13 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xcontent.XContentType; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.elasticsearch.test.LambdaMatchers.transformedMatch; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -30,6 +33,12 @@ public class SimulateIngestServiceTests extends ESTestCase { + private static Map newHashMap(K key, V value) { + Map map = new HashMap<>(); + map.put(key, value); + return map; + } + public void testGetPipeline() { PipelineConfiguration pipelineConfiguration = new PipelineConfiguration("pipeline1", new BytesArray(""" {"processors": [{"processor1" : {}}]}"""), XContentType.JSON); @@ -57,74 +66,47 @@ public void testGetPipeline() { SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest((Map>) null); SimulateIngestService simulateIngestService = new SimulateIngestService(ingestService, simulateBulkRequest); Pipeline pipeline = simulateIngestService.getPipeline("pipeline1"); - assertThat(pipeline.getProcessors().size(), equalTo(1)); - assertThat(pipeline.getProcessors().get(0).getType(), equalTo("processor1")); + assertThat(pipeline.getProcessors(), contains(transformedMatch(Processor::getType, equalTo("processor1")))); assertNull(simulateIngestService.getPipeline("pipeline2")); } { // Here we make sure that if we have a substitution with the same name as the original pipeline that we get the new one back - Map> pipelineSubstitutions = new HashMap<>() { - { - put("pipeline1", new HashMap<>() { - { - put("processors", List.of(new HashMap<>() { - { - put("processor2", new HashMap<>()); - } - }, new HashMap<>() { - { - put("processor3", new HashMap<>()); - } - })); - } - }); - put("pipeline2", new HashMap<>() { - { - put("processors", List.of(new HashMap<>() { - { - put("processor3", new HashMap<>()); - } - })); - } - }); - } - }; + Map> pipelineSubstitutions = new HashMap<>(); + pipelineSubstitutions.put( + "pipeline1", + newHashMap( + "processors", + List.of(newHashMap("processor2", Collections.emptyMap()), newHashMap("processor3", Collections.emptyMap())) + ) + ); + pipelineSubstitutions.put("pipeline2", newHashMap("processors", List.of(newHashMap("processor3", Collections.emptyMap())))); + SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(pipelineSubstitutions); SimulateIngestService simulateIngestService = new SimulateIngestService(ingestService, simulateBulkRequest); Pipeline pipeline1 = simulateIngestService.getPipeline("pipeline1"); - assertThat(pipeline1.getProcessors().size(), equalTo(2)); - assertThat(pipeline1.getProcessors().get(0).getType(), equalTo("processor2")); - assertThat(pipeline1.getProcessors().get(1).getType(), equalTo("processor3")); + assertThat( + pipeline1.getProcessors(), + contains( + transformedMatch(Processor::getType, equalTo("processor2")), + transformedMatch(Processor::getType, equalTo("processor3")) + ) + ); Pipeline pipeline2 = simulateIngestService.getPipeline("pipeline2"); - assertThat(pipeline2.getProcessors().size(), equalTo(1)); - assertThat(pipeline2.getProcessors().get(0).getType(), equalTo("processor3")); + assertThat(pipeline2.getProcessors(), contains(transformedMatch(Processor::getType, equalTo("processor3")))); } { /* * Here we make sure that if we have a substitution for a new pipeline we still get the original one back (as well as the new * one). */ - Map> pipelineSubstitutions = new HashMap<>() { - { - put("pipeline2", new HashMap<>() { - { - put("processors", List.of(new HashMap<>() { - { - put("processor3", new HashMap<>()); - } - })); - } - }); - } - }; + Map> pipelineSubstitutions = new HashMap<>(); + pipelineSubstitutions.put("pipeline2", newHashMap("processors", List.of(newHashMap("processor3", Collections.emptyMap())))); SimulateBulkRequest simulateBulkRequest = new SimulateBulkRequest(pipelineSubstitutions); SimulateIngestService simulateIngestService = new SimulateIngestService(ingestService, simulateBulkRequest); Pipeline pipeline1 = simulateIngestService.getPipeline("pipeline1"); - assertThat(pipeline1.getProcessors().size(), equalTo(1)); - assertThat(pipeline1.getProcessors().get(0).getType(), equalTo("processor1")); + assertThat(pipeline1.getProcessors(), contains(transformedMatch(Processor::getType, equalTo("processor1")))); Pipeline pipeline2 = simulateIngestService.getPipeline("pipeline2"); - assertThat(pipeline2.getProcessors().size(), equalTo(1)); - assertThat(pipeline2.getProcessors().get(0).getType(), equalTo("processor3")); + assertThat(pipeline2.getProcessors(), contains(transformedMatch(Processor::getType, equalTo("processor3")))); } } diff --git a/server/src/test/java/org/elasticsearch/rest/RestRequestTests.java b/server/src/test/java/org/elasticsearch/rest/RestRequestTests.java index 809bf528ba194..bb06dbe5d09aa 100644 --- a/server/src/test/java/org/elasticsearch/rest/RestRequestTests.java +++ b/server/src/test/java/org/elasticsearch/rest/RestRequestTests.java @@ -256,11 +256,7 @@ public void testMarkPathRestricted() { IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> request1.markPathRestricted("foo")); assertThat(exception.getMessage(), is("The parameter [" + PATH_RESTRICTED + "] is already defined.")); - RestRequest request2 = contentRestRequest("content", new HashMap<>() { - { - put(PATH_RESTRICTED, "foo"); - } - }); + RestRequest request2 = contentRestRequest("content", Map.of(PATH_RESTRICTED, "foo")); exception = expectThrows(IllegalArgumentException.class, () -> request2.markPathRestricted("bar")); assertThat(exception.getMessage(), is("The parameter [" + PATH_RESTRICTED + "] is already defined.")); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/AggProviderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/AggProviderTests.java index 40c6a74f4aaa0..19caa6a96d515 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/AggProviderTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/datafeed/AggProviderTests.java @@ -67,7 +67,7 @@ public static AggProvider createRandomValidAggProvider() { } public static AggProvider createRandomValidAggProvider(String name, String field) { - Map agg = Collections.singletonMap(name, Collections.singletonMap("avg", Collections.singletonMap("field", field))); + Map agg = Map.of(name, Map.of("avg", Map.of("field", field))); try { SearchModule searchModule = new SearchModule(Settings.EMPTY, Collections.emptyList()); AggregatorFactories.Builder aggs = XContentObjectTransformer.aggregatorTransformer( @@ -87,95 +87,65 @@ public void testEmptyAggMap() throws IOException { assertThat(e.getMessage(), equalTo("Datafeed aggregations are not parsable")); } + private static HashMap hashMapOf(K key, V value) { + HashMap map = new HashMap<>(); + map.put(key, value); + return map; + } + + private static HashMap hashMapOf(K k1, V v1, K k2, V v2, K k3, V v3) { + HashMap map = new HashMap<>(); + map.put(k1, v1); + map.put(k2, v2); + map.put(k3, v3); + return map; + } + public void testRewriteBadNumericInterval() { long numericInterval = randomNonNegativeLong(); - Map maxTime = Collections.singletonMap("max", Collections.singletonMap("field", "time")); - Map numericDeprecated = new HashMap<>() { - { - put("interval", numericInterval); - put("field", "foo"); - put("aggs", Collections.singletonMap("time", maxTime)); - } - }; - Map expected = new HashMap<>() { - { - put("fixed_interval", numericInterval + "ms"); - put("field", "foo"); - put("aggs", Collections.singletonMap("time", maxTime)); - } - }; - Map deprecated = Collections.singletonMap("buckets", Collections.singletonMap("date_histogram", numericDeprecated)); + Map maxTime = Map.of("max", Map.of("field", "time")); + Map numericDeprecated = hashMapOf("interval", numericInterval, "field", "foo", "aggs", Map.of("time", maxTime)); + Map expected = Map.of("fixed_interval", numericInterval + "ms", "field", "foo", "aggs", Map.of("time", maxTime)); + Map deprecated = hashMapOf("buckets", hashMapOf("date_histogram", numericDeprecated)); assertTrue(AggProvider.rewriteDateHistogramInterval(deprecated, false)); - assertThat(deprecated, equalTo(Collections.singletonMap("buckets", Collections.singletonMap("date_histogram", expected)))); - - numericDeprecated = new HashMap<>() { - { - put("interval", numericInterval + "ms"); - put("field", "foo"); - put("aggs", Collections.singletonMap("time", maxTime)); - } - }; - deprecated = Collections.singletonMap("date_histogram", Collections.singletonMap("date_histogram", numericDeprecated)); + assertThat(deprecated, equalTo(Map.of("buckets", Map.of("date_histogram", expected)))); + + numericDeprecated = hashMapOf("interval", numericInterval + "ms", "field", "foo", "aggs", Map.of("time", maxTime)); + deprecated = hashMapOf("date_histogram", hashMapOf("date_histogram", numericDeprecated)); assertTrue(AggProvider.rewriteDateHistogramInterval(deprecated, false)); - assertThat(deprecated, equalTo(Collections.singletonMap("date_histogram", Collections.singletonMap("date_histogram", expected)))); + assertThat(deprecated, equalTo(Map.of("date_histogram", Map.of("date_histogram", expected)))); } public void testRewriteBadCalendarInterval() { String calendarInterval = "1w"; - Map maxTime = Collections.singletonMap("max", Collections.singletonMap("field", "time")); - Map calendarDeprecated = new HashMap<>() { - { - put("interval", calendarInterval); - put("field", "foo"); - put("aggs", Collections.singletonMap("time", maxTime)); - } - }; - Map expected = new HashMap<>() { - { - put("calendar_interval", calendarInterval); - put("field", "foo"); - put("aggs", Collections.singletonMap("time", maxTime)); - } - }; - Map deprecated = Collections.singletonMap( - "buckets", - Collections.singletonMap("date_histogram", calendarDeprecated) - ); + Map maxTime = Map.of("max", Map.of("field", "time")); + Map calendarDeprecated = hashMapOf("interval", calendarInterval, "field", "foo", "aggs", Map.of("time", maxTime)); + Map expected = Map.of("calendar_interval", calendarInterval, "field", "foo", "aggs", Map.of("time", maxTime)); + Map deprecated = hashMapOf("buckets", hashMapOf("date_histogram", calendarDeprecated)); assertTrue(AggProvider.rewriteDateHistogramInterval(deprecated, false)); - assertThat(deprecated, equalTo(Collections.singletonMap("buckets", Collections.singletonMap("date_histogram", expected)))); - - calendarDeprecated = new HashMap<>() { - { - put("interval", calendarInterval); - put("field", "foo"); - put("aggs", Collections.singletonMap("time", maxTime)); - } - }; - deprecated = Collections.singletonMap("date_histogram", Collections.singletonMap("date_histogram", calendarDeprecated)); + assertThat(deprecated, equalTo(Map.of("buckets", Map.of("date_histogram", expected)))); + + calendarDeprecated = hashMapOf("interval", calendarInterval, "field", "foo", "aggs", Map.of("time", maxTime)); + deprecated = hashMapOf("date_histogram", hashMapOf("date_histogram", calendarDeprecated)); assertTrue(AggProvider.rewriteDateHistogramInterval(deprecated, false)); - assertThat(deprecated, equalTo(Collections.singletonMap("date_histogram", Collections.singletonMap("date_histogram", expected)))); + assertThat(deprecated, equalTo(Map.of("date_histogram", Map.of("date_histogram", expected)))); } public void testRewriteWhenNoneMustOccur() { String calendarInterval = "1w"; - Map maxTime = Collections.singletonMap("max", Collections.singletonMap("field", "time")); - Map calendarDeprecated = new HashMap<>() { - { - put("calendar_interval", calendarInterval); - put("field", "foo"); - put("aggs", Collections.singletonMap("time", maxTime)); - } - }; - Map expected = new HashMap<>() { - { - put("calendar_interval", calendarInterval); - put("field", "foo"); - put("aggs", Collections.singletonMap("time", maxTime)); - } - }; - Map current = Collections.singletonMap("buckets", Collections.singletonMap("date_histogram", calendarDeprecated)); + Map maxTime = Map.of("max", Map.of("field", "time")); + Map calendarDeprecated = Map.of( + "calendar_interval", + calendarInterval, + "field", + "foo", + "aggs", + Map.of("time", maxTime) + ); + Map expected = Map.of("calendar_interval", calendarInterval, "field", "foo", "aggs", Map.of("time", maxTime)); + Map current = Map.of("buckets", Map.of("date_histogram", calendarDeprecated)); assertFalse(AggProvider.rewriteDateHistogramInterval(current, false)); - assertThat(current, equalTo(Collections.singletonMap("buckets", Collections.singletonMap("date_histogram", expected)))); + assertThat(current, equalTo(Map.of("buckets", Map.of("date_histogram", expected)))); } @Override diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java index afb5cfc4fb1bc..62f73b0f2bccd 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/analyses/ClassificationTests.java @@ -41,7 +41,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; import java.util.Set; @@ -446,18 +445,19 @@ public void testGetResultMappings_DependentVariableMappingHasNoTypes() { } public void testGetResultMappings_DependentVariableMappingIsPresent() { - Map expectedTopClassesMapping = new HashMap<>() { - { - put("type", "nested"); - put("properties", new HashMap<>() { - { - put("class_name", singletonMap("type", "dummy")); - put("class_probability", singletonMap("type", "double")); - put("class_score", singletonMap("type", "double")); - } - }); - } - }; + Map expectedTopClassesMapping = Map.of( + "type", + "nested", + "properties", + Map.of( + "class_name", + Map.of("type", "dummy"), + "class_probability", + Map.of("type", "double"), + "class_score", + Map.of("type", "double") + ) + ); FieldCapabilitiesResponse fieldCapabilitiesResponse = new FieldCapabilitiesResponse( new String[0], Collections.singletonMap("foo", Collections.singletonMap("dummy", createFieldCapabilities("foo", "dummy"))) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/inference/InferenceDefinitionTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/inference/InferenceDefinitionTests.java index 14ec7a2053a1c..9f2326d022eab 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/inference/InferenceDefinitionTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/inference/trainedmodel/inference/InferenceDefinitionTests.java @@ -94,41 +94,20 @@ public void testMultiClassIrisInference() throws IOException, ParseException { xContentRegistry() ); - Map fields = new HashMap<>() { - { - put("sepal_length", 5.1); - put("sepal_width", 3.5); - put("petal_length", 1.4); - put("petal_width", 0.2); - } - }; + Map fields = Map.of("sepal_length", 5.1, "sepal_width", 3.5, "petal_length", 1.4, "petal_width", 0.2); assertThat( ((ClassificationInferenceResults) definition.infer(fields, ClassificationConfig.EMPTY_PARAMS)).getClassificationLabel(), equalTo("Iris-setosa") ); - fields = new HashMap<>() { - { - put("sepal_length", 7.0); - put("sepal_width", 3.2); - put("petal_length", 4.7); - put("petal_width", 1.4); - } - }; + fields = Map.of("sepal_length", 7.0, "sepal_width", 3.2, "petal_length", 4.7, "petal_width", 1.4); assertThat( ((ClassificationInferenceResults) definition.infer(fields, ClassificationConfig.EMPTY_PARAMS)).getClassificationLabel(), equalTo("Iris-versicolor") ); - fields = new HashMap<>() { - { - put("sepal_length", 6.5); - put("sepal_width", 3.0); - put("petal_length", 5.2); - put("petal_width", 2.0); - } - }; + fields = Map.of("sepal_length", 6.5, "sepal_width", 3.0, "petal_length", 5.2, "petal_width", 2.0); assertThat( ((ClassificationInferenceResults) definition.infer(fields, ClassificationConfig.EMPTY_PARAMS)).getClassificationLabel(), equalTo("Iris-virginica") diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/SourceConfigTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/SourceConfigTests.java index a88530904b3d2..048f6b8b12090 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/SourceConfigTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/transform/transforms/SourceConfigTests.java @@ -13,7 +13,6 @@ import org.junit.Before; import java.io.IOException; -import java.util.HashMap; import java.util.Map; import java.util.function.Predicate; @@ -146,19 +145,20 @@ public void testGetRuntimeMappings_EmptyRuntimeMappings() { } public void testGetRuntimeMappings_NonEmptyRuntimeMappings() { - Map runtimeMappings = new HashMap<>() { - { - put("field-A", singletonMap("type", "keyword")); - put("field-B", singletonMap("script", "some script")); - put("field-C", singletonMap("script", "some other script")); - } - }; - Map scriptBasedRuntimeMappings = new HashMap<>() { - { - put("field-B", singletonMap("script", "some script")); - put("field-C", singletonMap("script", "some other script")); - } - }; + Map runtimeMappings = Map.of( + "field-A", + Map.of("type", "keyword"), + "field-B", + Map.of("script", "some script"), + "field-C", + Map.of("script", "some other script") + ); + Map scriptBasedRuntimeMappings = Map.of( + "field-B", + Map.of("script", "some script"), + "field-C", + Map.of("script", "some other script") + ); SourceConfig sourceConfig = new SourceConfig(generateRandomStringArray(10, 10, false, false), randomQueryConfig(), runtimeMappings); assertThat(sourceConfig.getRuntimeMappings(), is(equalTo(runtimeMappings))); assertThat(sourceConfig.getScriptBasedRuntimeMappings(), is(equalTo(scriptBasedRuntimeMappings))); diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/InferenceIngestIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/InferenceIngestIT.java index 84c5ed9f934bb..0544534501ab2 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/InferenceIngestIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/InferenceIngestIT.java @@ -33,7 +33,6 @@ import org.junit.Before; import java.io.IOException; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -539,15 +538,17 @@ private static Request searchRequest(String index, QueryBuilder queryBuilder) th return request; } - private Map generateSourceDoc() { - return new HashMap<>() { - { - put("col1", randomFrom("female", "male")); - put("col2", randomFrom("S", "M", "L", "XL")); - put("col3", randomFrom("true", "false", "none", "other")); - put("col4", randomIntBetween(0, 10)); - } - }; + private static Map generateSourceDoc() { + return Map.of( + "col1", + randomFrom("female", "male"), + "col2", + randomFrom("S", "M", "L", "XL"), + "col3", + randomFrom("true", "false", "none", "other"), + "col4", + randomIntBetween(0, 10) + ); } private static final String REGRESSION_DEFINITION = """ diff --git a/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java b/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java index 1793369c14905..d99fb9674818c 100644 --- a/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java +++ b/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java @@ -1019,17 +1019,21 @@ public void testGettingInvalidFloat() throws Exception { // // BigDecimal fetching testing // - static final Map, Integer> JAVA_TO_SQL_NUMERIC_TYPES_MAP = new HashMap<>() { - { - put(Byte.class, Types.TINYINT); - put(Short.class, Types.SMALLINT); - put(Integer.class, Types.INTEGER); - put(Long.class, Types.BIGINT); - put(Float.class, Types.REAL); - put(Double.class, Types.DOUBLE); - // TODO: no half & scaled float testing - } - }; + static final Map, Integer> JAVA_TO_SQL_NUMERIC_TYPES_MAP = Map.of( + Byte.class, + Types.TINYINT, + Short.class, + Types.SMALLINT, + Integer.class, + Types.INTEGER, + Long.class, + Types.BIGINT, + Float.class, + Types.REAL, + Double.class, + Types.DOUBLE + // TODO: no half & scaled float testing + ); private static void validateBigDecimalWithoutCasting(ResultSet results, List testValues) throws SQLException { diff --git a/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/rest/RestSqlTestCase.java b/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/rest/RestSqlTestCase.java index fb92ac096fc36..ca9532d8dc7d0 100644 --- a/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/rest/RestSqlTestCase.java +++ b/x-pack/plugin/sql/qa/server/src/main/java/org/elasticsearch/xpack/sql/qa/rest/RestSqlTestCase.java @@ -1511,18 +1511,19 @@ public void testBasicAsyncWait() throws IOException { public void testAsyncTextWait() throws IOException { RequestObjectBuilder builder = query("SELECT 1").waitForCompletionTimeout("1d").keepOnCompletion(false); - Map contentMap = new HashMap<>() { - { - put("txt", " 1 \n---------------\n1 \n"); - put("csv", "1\r\n1\r\n"); - put("tsv", "1\n1\n"); - } - }; + Map contentMap = Map.of( + "txt", + " 1 \n---------------\n1 \n", + "csv", + "1\r\n1\r\n", + "tsv", + "1\n1\n" + ); - for (String format : contentMap.keySet()) { - Response response = runSqlAsTextWithFormat(builder, format); + for (var format : contentMap.entrySet()) { + Response response = runSqlAsTextWithFormat(builder, format.getKey()); - assertEquals(contentMap.get(format), responseBody(response)); + assertEquals(format.getValue(), responseBody(response)); assertTrue(hasText(response.getHeader(HEADER_NAME_ASYNC_ID))); assertEquals("false", response.getHeader(HEADER_NAME_ASYNC_PARTIAL)); @@ -1532,13 +1533,7 @@ public void testAsyncTextWait() throws IOException { @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/80089") public void testAsyncTextPaginated() throws IOException, InterruptedException { - final Map acceptMap = new HashMap<>() { - { - put("txt", "text/plain"); - put("csv", "text/csv"); - put("tsv", "text/tab-separated-values"); - } - }; + final Map acceptMap = Map.of("txt", "text/plain", "csv", "text/csv", "tsv", "text/tab-separated-values"); final int fetchSize = randomIntBetween(1, 10); final int fetchCount = randomIntBetween(1, 9); bulkLoadTestData(fetchSize * fetchCount); // NB: product needs to stay below 100, for txt format tests diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerRunTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerRunTests.java index 9ce49721ba2ae..81a1e3b0741f4 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerRunTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/optimizer/OptimizerRunTests.java @@ -32,7 +32,6 @@ import org.elasticsearch.xpack.sql.types.SqlTypesTests; import java.time.ZonedDateTime; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -52,17 +51,22 @@ public class OptimizerRunTests extends ESTestCase { private final IndexResolution getIndexResult; private final Analyzer analyzer; private final Optimizer optimizer; - private static final Map> COMPARISONS = new HashMap<>() { - { - put(EQ.symbol(), Equals.class); - put(NULLEQ.symbol(), NullEquals.class); - put(NEQ.symbol(), NotEquals.class); - put(GT.symbol(), GreaterThan.class); - put(GTE.symbol(), GreaterThanOrEqual.class); - put(LT.symbol(), LessThan.class); - put(LTE.symbol(), LessThanOrEqual.class); - } - }; + private static final Map> COMPARISONS = Map.of( + EQ.symbol(), + Equals.class, + NULLEQ.symbol(), + NullEquals.class, + NEQ.symbol(), + NotEquals.class, + GT.symbol(), + GreaterThan.class, + GTE.symbol(), + GreaterThanOrEqual.class, + LT.symbol(), + LessThan.class, + LTE.symbol(), + LessThanOrEqual.class + ); private static final LiteralsOnTheRight LITERALS_ON_THE_RIGHT = new LiteralsOnTheRight(); public OptimizerRunTests() { diff --git a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformUsingSearchRuntimeFieldsIT.java b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformUsingSearchRuntimeFieldsIT.java index 9f4a15029f05f..2e509bedbce39 100644 --- a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformUsingSearchRuntimeFieldsIT.java +++ b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformUsingSearchRuntimeFieldsIT.java @@ -28,12 +28,12 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static java.util.Collections.singletonMap; +import static java.util.Map.entry; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; @@ -49,39 +49,31 @@ public class TransformUsingSearchRuntimeFieldsIT extends TransformRestTestCase { private static final int NUM_USERS = 28; private static Map createRuntimeMappings() { - return new HashMap<>() { - { - put("user-upper", new HashMap<>() { - { - put("type", "keyword"); - put( - "script", - singletonMap("source", "if (params._source.user_id != null) {emit(params._source.user_id.toUpperCase())}") - ); - } - }); - put("stars", new HashMap<>() { - { - put("type", "long"); - } - }); - put("stars-x2", new HashMap<>() { - { - put("type", "long"); - put("script", singletonMap("source", "if (params._source.stars != null) {emit(2 * params._source.stars)}")); - } - }); - put("timestamp-5m", new HashMap<>() { - { - put("type", "date"); - put( - "script", - singletonMap("source", "emit(doc['timestamp'].value.toInstant().minus(5, ChronoUnit.MINUTES).toEpochMilli())") - ); - } - }); - } - }; + return Map.ofEntries( + entry( + "user-upper", + Map.of( + "type", + "keyword", + "script", + Map.of("source", "if (params._source.user_id != null) {emit(params._source.user_id.toUpperCase())}") + ) + ), + entry("stars", Map.of("type", "long")), + entry( + "stars-x2", + Map.of("type", "long", "script", Map.of("source", "if (params._source.stars != null) {emit(2 * params._source.stars)}")) + ), + entry( + "timestamp-5m", + Map.of( + "type", + "date", + "script", + Map.of("source", "emit(doc['timestamp'].value.toInstant().minus(5, ChronoUnit.MINUTES).toEpochMilli())") + ) + ) + ); } @Before @@ -114,17 +106,15 @@ public void testPivotTransform() throws Exception { var previewResponse = previewTransform(Strings.toString(config), RequestOptions.DEFAULT); // Verify preview mappings - Map expectedMappingProperties = new HashMap<>() { - { - put("by-user", singletonMap("type", "keyword")); - put("review_score", singletonMap("type", "double")); - put("review_score_max", singletonMap("type", "long")); - put("review_score_rt_avg", singletonMap("type", "double")); - put("review_score_rt_max", singletonMap("type", "long")); - put("timestamp", singletonMap("type", "date")); - put("timestamp_rt", singletonMap("type", "date")); - } - }; + Map expectedMappingProperties = Map.ofEntries( + entry("by-user", Map.of("type", "keyword")), + entry("review_score", Map.of("type", "double")), + entry("review_score_max", Map.of("type", "long")), + entry("review_score_rt_avg", Map.of("type", "double")), + entry("review_score_rt_max", Map.of("type", "long")), + entry("timestamp", Map.of("type", "date")), + entry("timestamp_rt", Map.of("type", "date")) + ); var generatedMappings = (Map) XContentMapValues.extractValue("generated_dest_index.mappings", previewResponse); assertThat(generatedMappings, allOf(hasKey("_meta"), hasEntry("properties", expectedMappingProperties))); // Verify preview contents @@ -167,20 +157,16 @@ public void testPivotTransform() throws Exception { public void testPivotTransform_BadRuntimeFieldScript() throws Exception { String destIndexName = "reviews-by-user-pivot"; String transformId = "transform-with-st-rt-fields-pivot"; - Map runtimeMappings = new HashMap<>() { - { - put("user-upper", new HashMap<>() { - { - put("type", "keyword"); - // Method name used in the script is misspelled, i.e.: "toUperCase" instead of "toUpperCase" - put( - "script", - singletonMap("source", "if (params._source.user_id != null) {emit(params._source.user_id.toUperCase())}") - ); - } - }); - } - }; + Map runtimeMappings = Map.of( + "user-upper", + Map.of( + "type", + "keyword", + // Method name used in the script is misspelled, i.e.: "toUperCase" instead of "toUpperCase" + "script", + Map.of("source", "if (params._source.user_id != null) {emit(params._source.user_id.toUperCase())}") + ) + ); Map groups = singletonMap("by-user", new TermsGroupSource("user-upper", null, false)); AggregatorFactories.Builder aggs = AggregatorFactories.builder() @@ -273,20 +259,16 @@ public void testLatestTransform() throws Exception { public void testLatestTransform_BadRuntimeFieldScript() throws Exception { String destIndexName = "reviews-by-user-latest"; String transformId = "transform-with-st-rt-fields-latest"; - Map runtimeMappings = new HashMap<>() { - { - put("user-upper", new HashMap<>() { - { - put("type", "keyword"); - // Method name used in the script is misspelled, i.e.: "toUperCase" instead of "toUpperCase" - put( - "script", - singletonMap("source", "if (params._source.user_id != null) {emit(params._source.user_id.toUperCase())}") - ); - } - }); - } - }; + Map runtimeMappings = Map.of( + "user-upper", + Map.of( + "type", + "keyword", + // Method name used in the script is misspelled, i.e.: "toUperCase" instead of "toUpperCase" + "script", + Map.of("source", "if (params._source.user_id != null) {emit(params._source.user_id.toUperCase())}") + ) + ); SourceConfig sourceConfig = new SourceConfig(new String[] { REVIEWS_INDEX_NAME }, QueryConfig.matchAll(), runtimeMappings); TransformConfig configWithRuntimeFields = createTransformConfigBuilder(transformId, destIndexName, QueryConfig.matchAll(), "dummy") diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/action/TransformConfigLinterTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/action/TransformConfigLinterTests.java index 30b86c71f473b..3006717bd843b 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/action/TransformConfigLinterTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/action/TransformConfigLinterTests.java @@ -27,11 +27,9 @@ import org.elasticsearch.xpack.transform.transforms.pivot.Pivot; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; @@ -52,13 +50,14 @@ public void testGetWarnings_Pivot_WithScriptBasedRuntimeFields() { assertThat(TransformConfigLinter.getWarnings(function, sourceConfig, syncConfig), is(empty())); - Map runtimeMappings = new HashMap<>() { - { - put("rt-field-A", singletonMap("type", "keyword")); - put("rt-field-B", singletonMap("script", "some script")); - put("rt-field-C", singletonMap("script", "some other script")); - } - }; + Map runtimeMappings = Map.of( + "rt-field-A", + Map.of("type", "keyword"), + "rt-field-B", + Map.of("script", "some script"), + "rt-field-C", + Map.of("script", "some other script") + ); sourceConfig = new SourceConfig( generateRandomStringArray(10, 10, false, false), QueryConfigTests.randomQueryConfig(), @@ -81,13 +80,14 @@ public void testGetWarnings_Latest_WithScriptBasedRuntimeFields() { SyncConfig syncConfig = new TimeSyncConfig("rt-field-C", null); - Map runtimeMappings = new HashMap<>() { - { - put("rt-field-A", singletonMap("type", "keyword")); - put("rt-field-B", singletonMap("script", "some script")); - put("rt-field-C", singletonMap("script", "some other script")); - } - }; + Map runtimeMappings = Map.of( + "rt-field-A", + Map.of("type", "keyword"), + "rt-field-B", + Map.of("script", "some script"), + "rt-field-C", + Map.of("script", "some other script") + ); sourceConfig = new SourceConfig( generateRandomStringArray(10, 10, false, false), QueryConfigTests.randomQueryConfig(), diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformIndexTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformIndexTests.java index 9db4ba1fc73b6..87b65978f667e 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformIndexTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/persistence/TransformIndexTests.java @@ -45,7 +45,7 @@ import java.util.concurrent.TimeUnit; import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonMap; +import static java.util.Map.entry; import static org.elasticsearch.common.xcontent.support.XContentMapValues.extractValue; import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.contains; @@ -220,68 +220,45 @@ public void testSetUpDestinationAliases() { public void testCreateMappingsFromStringMap() { assertThat(TransformIndex.createMappingsFromStringMap(emptyMap()), is(anEmptyMap())); + assertThat(TransformIndex.createMappingsFromStringMap(Map.of("a", "long")), equalTo(Map.of("a", Map.of("type", "long")))); assertThat( - TransformIndex.createMappingsFromStringMap(singletonMap("a", "long")), - is(equalTo(singletonMap("a", singletonMap("type", "long")))) + TransformIndex.createMappingsFromStringMap(Map.of("a", "long", "b", "keyword")), + equalTo(Map.of("a", Map.of("type", "long"), "b", Map.of("type", "keyword"))) + ); + assertThat( + TransformIndex.createMappingsFromStringMap(Map.of("a", "long", "a.b", "keyword")), + equalTo(Map.of("a", Map.of("type", "long"), "a.b", Map.of("type", "keyword"))) + ); + assertThat( + TransformIndex.createMappingsFromStringMap(Map.of("a", "long", "a.b", "text", "a.b.c", "keyword")), + equalTo(Map.of("a", Map.of("type", "long"), "a.b", Map.of("type", "text"), "a.b.c", Map.of("type", "keyword"))) + ); + assertThat( + TransformIndex.createMappingsFromStringMap( + Map.ofEntries( + entry("a", "object"), + entry("a.b", "long"), + entry("c", "nested"), + entry("c.d", "boolean"), + entry("f", "object"), + entry("f.g", "object"), + entry("f.g.h", "text"), + entry("f.g.h.i", "text") + ) + ), + equalTo( + Map.ofEntries( + entry("a", Map.of("type", "object")), + entry("a.b", Map.of("type", "long")), + entry("c", Map.of("type", "nested")), + entry("c.d", Map.of("type", "boolean")), + entry("f", Map.of("type", "object")), + entry("f.g", Map.of("type", "object")), + entry("f.g.h", Map.of("type", "text")), + entry("f.g.h.i", Map.of("type", "text")) + ) + ) ); - assertThat(TransformIndex.createMappingsFromStringMap(new HashMap<>() { - { - put("a", "long"); - put("b", "keyword"); - } - }), is(equalTo(new HashMap<>() { - { - put("a", singletonMap("type", "long")); - put("b", singletonMap("type", "keyword")); - } - }))); - assertThat(TransformIndex.createMappingsFromStringMap(new HashMap<>() { - { - put("a", "long"); - put("a.b", "keyword"); - } - }), is(equalTo(new HashMap<>() { - { - put("a", singletonMap("type", "long")); - put("a.b", singletonMap("type", "keyword")); - } - }))); - assertThat(TransformIndex.createMappingsFromStringMap(new HashMap<>() { - { - put("a", "long"); - put("a.b", "text"); - put("a.b.c", "keyword"); - } - }), is(equalTo(new HashMap<>() { - { - put("a", singletonMap("type", "long")); - put("a.b", singletonMap("type", "text")); - put("a.b.c", singletonMap("type", "keyword")); - } - }))); - assertThat(TransformIndex.createMappingsFromStringMap(new HashMap<>() { - { - put("a", "object"); - put("a.b", "long"); - put("c", "nested"); - put("c.d", "boolean"); - put("f", "object"); - put("f.g", "object"); - put("f.g.h", "text"); - put("f.g.h.i", "text"); - } - }), is(equalTo(new HashMap<>() { - { - put("a", singletonMap("type", "object")); - put("a.b", singletonMap("type", "long")); - put("c", singletonMap("type", "nested")); - put("c.d", singletonMap("type", "boolean")); - put("f", singletonMap("type", "object")); - put("f.g", singletonMap("type", "object")); - put("f.g.h", singletonMap("type", "text")); - put("f.g.h.i", singletonMap("type", "text")); - } - }))); } @SuppressWarnings("unchecked") diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/common/DocumentConversionUtilsTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/common/DocumentConversionUtilsTests.java index c6f2a33240471..b4d38ab517bb7 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/common/DocumentConversionUtilsTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/common/DocumentConversionUtilsTests.java @@ -14,7 +14,6 @@ import org.elasticsearch.test.ESTestCase; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import static java.util.Map.entry; @@ -87,21 +86,16 @@ public void testRemoveInternalFields() { } public void testExtractFieldMappings() { - FieldCapabilitiesResponse response = new FieldCapabilitiesResponse(new String[] { "some-index" }, new HashMap<>() { - { - put("field-1", new HashMap<>() { - { - put("keyword", createFieldCapabilities("field-1", "keyword")); - } - }); - put("field-2", new HashMap<>() { - { - put("long", createFieldCapabilities("field-2", "long")); - put("keyword", createFieldCapabilities("field-2", "keyword")); - } - }); - } - }); + FieldCapabilitiesResponse response = new FieldCapabilitiesResponse( + new String[] { "some-index" }, + Map.ofEntries( + entry("field-1", Map.of("keyword", createFieldCapabilities("field-1", "keyword"))), + entry( + "field-2", + Map.of("long", createFieldCapabilities("field-2", "long"), "keyword", createFieldCapabilities("field-2", "keyword")) + ) + ) + ); assertThat( DocumentConversionUtils.extractFieldMappings(response), diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/pivot/SchemaUtilTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/pivot/SchemaUtilTests.java index f6846bc065976..212942a09e40e 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/pivot/SchemaUtilTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/pivot/SchemaUtilTests.java @@ -46,9 +46,11 @@ import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; import static java.util.Collections.singletonMap; +import static org.elasticsearch.test.MapMatcher.matchesMap; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; @@ -56,29 +58,26 @@ public class SchemaUtilTests extends ESTestCase { public void testInsertNestedObjectMappings() { - Map fieldMappings = new HashMap<>() { - { - // creates: a.b, a - put("a.b.c", "long"); - put("a.b.d", "double"); - // creates: c.b, c - put("c.b.a", "double"); - // creates: c.d - put("c.d.e", "object"); - put("d", "long"); - put("e.f.g", "long"); - // cc: already there - put("e.f", "object"); - // cc: already there but different type (should not be possible) - put("e", "long"); - // cc: start with . (should not be possible) - put(".x", "long"); - // cc: start and ends with . (should not be possible), creates: .y - put(".y.", "long"); - // cc: ends with . (should not be possible), creates: .z - put(".z.", "long"); - } - }; + Map fieldMappings = new HashMap<>(); + // creates: a.b, a + fieldMappings.put("a.b.c", "long"); + fieldMappings.put("a.b.d", "double"); + // creates: c.b, c + fieldMappings.put("c.b.a", "double"); + // creates: c.d + fieldMappings.put("c.d.e", "object"); + fieldMappings.put("d", "long"); + fieldMappings.put("e.f.g", "long"); + // cc: already there + fieldMappings.put("e.f", "object"); + // cc: already there but different type (should not be possible) + fieldMappings.put("e", "long"); + // cc: start with . (should not be possible) + fieldMappings.put(".x", "long"); + // cc: start and ends with . (should not be possible), creates: .y + fieldMappings.put(".y.", "long"); + // cc: ends with . (should not be possible), creates: .z + fieldMappings.put(".z.", "long"); SchemaUtil.insertNestedObjectMappings(fieldMappings); @@ -122,10 +121,7 @@ public void testGetSourceFieldMappings() throws InterruptedException { null, listener ), - mappings -> { - assertNotNull(mappings); - assertTrue(mappings.isEmpty()); - } + mappings -> assertThat(mappings, anEmptyMap()) ); // fields is empty @@ -137,10 +133,7 @@ public void testGetSourceFieldMappings() throws InterruptedException { new String[] {}, listener ), - mappings -> { - assertNotNull(mappings); - assertTrue(mappings.isEmpty()); - } + mappings -> assertThat(mappings, anEmptyMap()) ); // good use @@ -152,23 +145,13 @@ public void testGetSourceFieldMappings() throws InterruptedException { new String[] { "field-1", "field-2" }, listener ), - mappings -> { - assertNotNull(mappings); - assertEquals(2, mappings.size()); - assertEquals("long", mappings.get("field-1")); - assertEquals("long", mappings.get("field-2")); - } + mappings -> assertThat(mappings, matchesMap(Map.of("field-1", "long", "field-2", "long"))) ); } } public void testGetSourceFieldMappingsWithRuntimeMappings() throws InterruptedException { - Map runtimeMappings = new HashMap<>() { - { - put("field-2", singletonMap("type", "keyword")); - put("field-3", singletonMap("type", "boolean")); - } - }; + Map runtimeMappings = Map.of("field-2", Map.of("type", "keyword"), "field-3", Map.of("type", "boolean")); try (var threadPool = createThreadPool()) { final var client = new FieldCapsMockClient(threadPool, emptySet()); this.>assertAsync( From 2e9e8f869b682a63696750faf830ec555cbd17f7 Mon Sep 17 00:00:00 2001 From: Saikat Sarkar <132922331+saikatsarkar056@users.noreply.github.com> Date: Fri, 23 Feb 2024 10:10:11 -0700 Subject: [PATCH 178/250] Display error for text_expansion if the queried field does not have the right type (#105581) * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Display error for text_expansion if the queried field does not have the right type * Clean up the code * Optimize the code for pruning config * Optimize the code for pruning config * Write findBestWeightFor for clear code * Run Spotless * Remove findBestWeightFor method --- .../ml/queries/TextExpansionQueryBuilder.java | 57 ++++++++++++------- .../queries/WeightedTokensQueryBuilder.java | 45 ++++++++++++--- .../TextExpansionQueryBuilderTests.java | 7 +-- .../test/ml/text_expansion_search.yml | 17 ++++++ 4 files changed, 92 insertions(+), 34 deletions(-) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilder.java index 7d5197b9e9ba0..675d062fdb3af 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilder.java @@ -18,7 +18,6 @@ import org.elasticsearch.core.Nullable; import org.elasticsearch.index.query.AbstractQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.xcontent.ParseField; @@ -32,8 +31,10 @@ import org.elasticsearch.xpack.core.ml.inference.trainedmodel.TextExpansionConfigUpdate; import java.io.IOException; +import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import static org.elasticsearch.xpack.core.ClientHelper.ML_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; @@ -51,6 +52,34 @@ public class TextExpansionQueryBuilder extends AbstractQueryBuilder weightedTokensSupplier; private final TokenPruningConfig tokenPruningConfig; + public enum AllowedFieldType { + RANK_FEATURES("rank_features"), + SPARSE_VECTOR("sparse_vector"); + + private final String typeName; + + AllowedFieldType(String typeName) { + this.typeName = typeName; + } + + public String getTypeName() { + return typeName; + } + + public static boolean isFieldTypeAllowed(String typeName) { + for (AllowedFieldType fieldType : values()) { + if (fieldType.getTypeName().equals(typeName)) { + return true; + } + } + return false; + } + + public static String getAllowedFieldTypesAsString() { + return Arrays.stream(values()).map(value -> value.typeName).collect(Collectors.joining(", ")); + } + } + public TextExpansionQueryBuilder(String fieldName, String modelText, String modelId) { this(fieldName, modelText, modelId, null); } @@ -198,24 +227,14 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws } private QueryBuilder weightedTokensToQuery(String fieldName, TextExpansionResults textExpansionResults) { - if (tokenPruningConfig != null) { - WeightedTokensQueryBuilder weightedTokensQueryBuilder = new WeightedTokensQueryBuilder( - fieldName, - textExpansionResults.getWeightedTokens(), - tokenPruningConfig - ); - weightedTokensQueryBuilder.queryName(queryName); - weightedTokensQueryBuilder.boost(boost); - return weightedTokensQueryBuilder; - } - var boolQuery = QueryBuilders.boolQuery(); - for (var weightedToken : textExpansionResults.getWeightedTokens()) { - boolQuery.should(QueryBuilders.termQuery(fieldName, weightedToken.token()).boost(weightedToken.weight())); - } - boolQuery.minimumShouldMatch(1); - boolQuery.boost(this.boost); - boolQuery.queryName(this.queryName); - return boolQuery; + WeightedTokensQueryBuilder weightedTokensQueryBuilder = new WeightedTokensQueryBuilder( + fieldName, + textExpansionResults.getWeightedTokens(), + tokenPruningConfig + ); + weightedTokensQueryBuilder.queryName(queryName); + weightedTokensQueryBuilder.boost(boost); + return weightedTokensQueryBuilder; } @Override diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/queries/WeightedTokensQueryBuilder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/queries/WeightedTokensQueryBuilder.java index a09bcadaacfc0..51139881fc2e4 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/queries/WeightedTokensQueryBuilder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/queries/WeightedTokensQueryBuilder.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Objects; +import static org.elasticsearch.xpack.ml.queries.TextExpansionQueryBuilder.AllowedFieldType; import static org.elasticsearch.xpack.ml.queries.TextExpansionQueryBuilder.PRUNING_CONFIG; public class WeightedTokensQueryBuilder extends AbstractQueryBuilder { @@ -152,27 +153,53 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { if (ft == null) { return new MatchNoDocsQuery("The \"" + getName() + "\" query is against a field that does not exist"); } + + final String fieldTypeName = ft.typeName(); + if (AllowedFieldType.isFieldTypeAllowed(fieldTypeName) == false) { + throw new ElasticsearchParseException( + "[" + + fieldTypeName + + "]" + + " is not an appropriate field type for this query. " + + "Allowed field types are [" + + AllowedFieldType.getAllowedFieldTypesAsString() + + "]." + ); + } + + return (this.tokenPruningConfig == null) + ? queryBuilderWithAllTokens(tokens, ft, context) + : queryBuilderWithPrunedTokens(tokens, ft, context); + } + + private Query queryBuilderWithAllTokens(List tokens, MappedFieldType ft, SearchExecutionContext context) { var qb = new BooleanQuery.Builder(); - int fieldDocCount = context.getIndexReader().getDocCount(fieldName); - float bestWeight = 0f; - for (var t : tokens) { - bestWeight = Math.max(t.weight(), bestWeight); + + for (var token : tokens) { + qb.add(new BoostQuery(ft.termQuery(token.token(), context), token.weight()), BooleanClause.Occur.SHOULD); } + return qb.setMinimumNumberShouldMatch(1).build(); + } + + private Query queryBuilderWithPrunedTokens(List tokens, MappedFieldType ft, SearchExecutionContext context) + throws IOException { + var qb = new BooleanQuery.Builder(); + int fieldDocCount = context.getIndexReader().getDocCount(fieldName); + float bestWeight = tokens.stream().map(WeightedToken::weight).reduce(0f, Math::max); float averageTokenFreqRatio = getAverageTokenFreqRatio(context.getIndexReader(), fieldDocCount); if (averageTokenFreqRatio == 0) { return new MatchNoDocsQuery("The \"" + getName() + "\" query is against an empty field"); } + for (var token : tokens) { boolean keep = shouldKeepToken(context.getIndexReader(), token, fieldDocCount, averageTokenFreqRatio, bestWeight); - if (this.tokenPruningConfig != null) { - keep ^= this.tokenPruningConfig.isOnlyScorePrunedTokens(); - } + keep ^= this.tokenPruningConfig.isOnlyScorePrunedTokens(); if (keep) { qb.add(new BoostQuery(ft.termQuery(token.token(), context), token.weight()), BooleanClause.Occur.SHOULD); } } - qb.setMinimumNumberShouldMatch(1); - return qb.build(); + + return qb.setMinimumNumberShouldMatch(1).build(); } @Override diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilderTests.java index 13f12f3cdc1e1..50561d92f5d37 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilderTests.java @@ -25,7 +25,6 @@ import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.extras.MapperExtrasPlugin; -import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.plugins.Plugin; @@ -260,10 +259,6 @@ public void testThatTokensAreCorrectlyPruned() { SearchExecutionContext searchExecutionContext = createSearchExecutionContext(); TextExpansionQueryBuilder queryBuilder = createTestQueryBuilder(); QueryBuilder rewrittenQueryBuilder = rewriteAndFetch(queryBuilder, searchExecutionContext); - if (queryBuilder.getTokenPruningConfig() == null) { - assertTrue(rewrittenQueryBuilder instanceof BoolQueryBuilder); - } else { - assertTrue(rewrittenQueryBuilder instanceof WeightedTokensQueryBuilder); - } + assertTrue(rewrittenQueryBuilder instanceof WeightedTokensQueryBuilder); } } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/text_expansion_search.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/text_expansion_search.yml index 5e29d3cdf2ae6..dc4e1751ccdee 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/text_expansion_search.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/text_expansion_search.yml @@ -287,3 +287,20 @@ setup: tokens_weight_threshold: 0.4 only_score_pruned_tokens: true - match: { hits.total.value: 0 } + +--- +"Test text-expansion that displays error for invalid queried field type": + - skip: + version: " - 8.13.99" + reason: "validation for invalid field type introduced in 8.14.0" + + - do: + catch: /\[keyword\] is not an appropriate field type for this query/ + search: + index: index-with-rank-features + body: + query: + text_expansion: + source_text: + model_id: text_expansion_model + model_text: "octopus comforter smells" From 229dba3b6b17e4f8632465dfd8ae055882403345 Mon Sep 17 00:00:00 2001 From: Tomonori Soejima <25199092+TomonoriSoejima@users.noreply.github.com> Date: Sat, 24 Feb 2024 02:16:16 +0900 Subject: [PATCH 179/250] [Transform] Clarify transform error message about needing the remote cluster client role (#96310) Clarify transform error message about needing the remote cluster client role. --- .../integration/TransformNoRemoteClusterClientNodeIT.java | 6 +++--- .../xpack/transform/transforms/TransformNodes.java | 5 ++++- .../transforms/TransformPersistentTasksExecutorTests.java | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/transform/src/internalClusterTest/java/org/elasticsearch/xpack/transform/integration/TransformNoRemoteClusterClientNodeIT.java b/x-pack/plugin/transform/src/internalClusterTest/java/org/elasticsearch/xpack/transform/integration/TransformNoRemoteClusterClientNodeIT.java index 21ff1dded6ae5..5090a00211ff4 100644 --- a/x-pack/plugin/transform/src/internalClusterTest/java/org/elasticsearch/xpack/transform/integration/TransformNoRemoteClusterClientNodeIT.java +++ b/x-pack/plugin/transform/src/internalClusterTest/java/org/elasticsearch/xpack/transform/integration/TransformNoRemoteClusterClientNodeIT.java @@ -50,7 +50,7 @@ public void testPreviewTransformWithRemoteIndex() { e.getMessage(), allOf( containsString("No appropriate node to run on"), - containsString("transform requires a remote connection but remote is disabled") + containsString("transform requires a remote connection but the node does not have the remote_cluster_client role") ) ); } @@ -74,7 +74,7 @@ public void testPutTransformWithRemoteIndex_NoDeferValidation() { e.getMessage(), allOf( containsString("No appropriate node to run on"), - containsString("transform requires a remote connection but remote is disabled") + containsString("transform requires a remote connection but the node does not have the remote_cluster_client role") ) ); } @@ -140,7 +140,7 @@ public void testUpdateTransformWithRemoteIndex_NoDeferValidation() { e.getMessage(), allOf( containsString("No appropriate node to run on"), - containsString("transform requires a remote connection but remote is disabled") + containsString("transform requires a remote connection but the node does not have the remote_cluster_client role") ) ); } diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformNodes.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformNodes.java index d282239099d6b..56e5fd5900cfb 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformNodes.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformNodes.java @@ -241,7 +241,10 @@ public static boolean nodeCanRunThisTransform( // does the transform require a remote and remote is enabled? if (requiresRemote && node.isRemoteClusterClient() == false) { if (explain != null) { - explain.put(node.getId(), "transform requires a remote connection but remote is disabled"); + explain.put( + node.getId(), + "transform requires a remote connection but the node does not have the remote_cluster_client role" + ); } return false; } diff --git a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformPersistentTasksExecutorTests.java b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformPersistentTasksExecutorTests.java index b32ec235fcc6f..b927a248faf31 100644 --- a/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformPersistentTasksExecutorTests.java +++ b/x-pack/plugin/transform/src/test/java/org/elasticsearch/xpack/transform/transforms/TransformPersistentTasksExecutorTests.java @@ -166,7 +166,7 @@ public void testNodeAssignmentProblems() { equalTo( "Not starting transform [new-task-id], reasons [" + "current-data-node-with-0-tasks-transform-remote-disabled:" - + "transform requires a remote connection but remote is disabled" + + "transform requires a remote connection but the node does not have the remote_cluster_client role" + "]" ) ); @@ -195,7 +195,7 @@ public void testNodeAssignmentProblems() { equalTo( "Not starting transform [new-task-id], reasons [" + "current-data-node-with-0-tasks-transform-remote-disabled:" - + "transform requires a remote connection but remote is disabled" + + "transform requires a remote connection but the node does not have the remote_cluster_client role" + "|" + "current-data-node-with-transform-disabled:not a transform node" + "]" From bc2a77ff9b1be59bd3090e437db8d8e7c2410c09 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Fri, 23 Feb 2024 17:21:52 +0000 Subject: [PATCH 180/250] [TEST] Issue another cluster state change to make sure ILM notices an index is assigned (#105725) ILM transitions to `wait-for-index-color` (a step that needs a cluster state changed event to evaluate against) but misses the cluster state event that notifies that `partial-index` is now `GREEN`. And then the cluster is quiet and no more state changes occur and we timeout. Note that the test is unblocked by the teardown of the IT that triggers some cluster state changes. This fixes the test by issueing some empty `reroute` request to cause some cluster state traffic in the cluster and ILM notices an index is assigned. Note that a production cluster is busy and ILM would eventually notice the new state and make progress. ``` 2024-02-22T06:33:01,388][INFO ][o.e.x.i.IndexLifecycleTransition] [node_t0] moving index [index] from [{"phase":"frozen","action":"searchable_snapshot","name":"mount-snapshot"}] to [{"phase":"froz en","action":"searchable_snapshot","name":"wait-for-index-color"}] in policy [policy] [2024-02-22T06:33:01,490][INFO ][o.e.c.r.a.AllocationService] [node_t0] current.health="GREEN" message="Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[partial-index][0]]])." previous.health="YELLOW" reason="shards started [[partial-index][0]]" ``` Fixes #102405 --- .../xpack/autoscaling/existence/FrozenExistenceDeciderIT.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/existence/FrozenExistenceDeciderIT.java b/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/existence/FrozenExistenceDeciderIT.java index 7e9e83e616f61..c72d5e83d2bd3 100644 --- a/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/existence/FrozenExistenceDeciderIT.java +++ b/x-pack/plugin/autoscaling/src/internalClusterTest/java/org/elasticsearch/xpack/autoscaling/existence/FrozenExistenceDeciderIT.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.autoscaling.existence; +import org.elasticsearch.action.admin.cluster.reroute.ClusterRerouteRequest; import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; import org.elasticsearch.blobcache.BlobCachePlugin; import org.elasticsearch.cluster.health.ClusterHealthStatus; @@ -138,6 +139,9 @@ public void testZeroToOne() throws Exception { // we've seen a case where bootstrapping a node took just over 60 seconds in the test environment, so using an (excessive) 90 // seconds max wait time to avoid flakiness assertBusy(() -> { + // cause a bit of cluster activity using an empty reroute call in case the `wait-for-index-colour` ILM step missed the + // notification that partial-index is now GREEN. + client().admin().cluster().reroute(new ClusterRerouteRequest()).actionGet(); String[] indices = indices(); assertThat(indices, arrayContaining(PARTIAL_INDEX_NAME)); assertThat(indices, not(arrayContaining(INDEX_NAME))); From 35b2dbee2ae6e63f89fe994ebc2b51af35c08595 Mon Sep 17 00:00:00 2001 From: Matteo Piergiovanni <134913285+piergm@users.noreply.github.com> Date: Fri, 23 Feb 2024 19:05:09 +0100 Subject: [PATCH 181/250] Field-caps field has value lookup use map instead of looping array (#105770) * use map instead of loop * Update docs/changelog/105770.yaml --- docs/changelog/105770.yaml | 5 +++++ .../index/mapper/extras/RankFeatureFieldMapper.java | 8 +------- .../index/mapper/extras/RankFeatureMetaFieldMapper.java | 8 +------- .../org/elasticsearch/index/mapper/MappedFieldType.java | 8 +------- 4 files changed, 8 insertions(+), 21 deletions(-) create mode 100644 docs/changelog/105770.yaml diff --git a/docs/changelog/105770.yaml b/docs/changelog/105770.yaml new file mode 100644 index 0000000000000..ec8ae4f380e2f --- /dev/null +++ b/docs/changelog/105770.yaml @@ -0,0 +1,5 @@ +pr: 105770 +summary: Field-caps field has value lookup use map instead of looping array +area: Search +type: enhancement +issues: [] diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureFieldMapper.java index f63f290bf58fc..c058dddd8f875 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureFieldMapper.java @@ -9,7 +9,6 @@ package org.elasticsearch.index.mapper.extras; import org.apache.lucene.document.FeatureField; -import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.FieldInfos; import org.apache.lucene.index.Term; import org.apache.lucene.search.Query; @@ -136,12 +135,7 @@ public Query existsQuery(SearchExecutionContext context) { @Override public boolean fieldHasValue(FieldInfos fieldInfos) { - for (FieldInfo fieldInfo : fieldInfos) { - if (fieldInfo.getName().equals(NAME)) { - return true; - } - } - return false; + return fieldInfos.fieldInfo(NAME) != null; } @Override diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureMetaFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureMetaFieldMapper.java index 07fe64c7466bd..c45065037b5a8 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureMetaFieldMapper.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/RankFeatureMetaFieldMapper.java @@ -8,7 +8,6 @@ package org.elasticsearch.index.mapper.extras; -import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.FieldInfos; import org.apache.lucene.search.Query; import org.elasticsearch.index.mapper.MappedFieldType; @@ -59,12 +58,7 @@ public Query existsQuery(SearchExecutionContext context) { @Override public boolean fieldHasValue(FieldInfos fieldInfos) { - for (FieldInfo fieldInfo : fieldInfos) { - if (fieldInfo.getName().equals(NAME)) { - return true; - } - } - return false; + return fieldInfos.fieldInfo(NAME) != null; } @Override diff --git a/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java b/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java index 8ee9665f60362..265374a687312 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/MappedFieldType.java @@ -9,7 +9,6 @@ package org.elasticsearch.index.mapper; import org.apache.lucene.analysis.TokenStream; -import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.FieldInfos; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.PrefixCodedTerms; @@ -644,12 +643,7 @@ public void validateMatchedRoutingPath(String routingPath) { * @return {@code true} if field is present in fieldInfos {@code false} otherwise */ public boolean fieldHasValue(FieldInfos fieldInfos) { - for (FieldInfo fieldInfo : fieldInfos) { - if (fieldInfo.getName().equals(name())) { - return true; - } - } - return false; + return fieldInfos.fieldInfo(name()) != null; } /** From 138bc6ad2c5fc77d48107ec20fe8c71b313c6a6a Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 23 Feb 2024 19:14:32 +0100 Subject: [PATCH 182/250] Speedup FieldCapabilitiesFetcher a little more (#105777) Make the code a little more predictable here. Save a redundant string builder that was allocating lots of bytes unnecessarily as well as stop building a redundant capturing lambda in the fetcher. --- .../fieldcaps/FieldCapabilitiesFetcher.java | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesFetcher.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesFetcher.java index 363f50542c4dc..8025923dbdd33 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesFetcher.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesFetcher.java @@ -120,11 +120,8 @@ private FieldCapabilitiesIndexResponse doFetch( // even if the mapping is the same if we return only fields with values we need // to make sure that we consider all the shard-mappings pair, that is why we // calculate a different hash for this particular case. - StringBuilder sb = new StringBuilder(indexService.getShard(shardId.getId()).getShardUuid()); - if (mapping != null) { - sb.append(mapping.getSha256()); - } - indexMappingHash = sb.toString(); + final String shardUuid = indexService.getShard(shardId.getId()).getShardUuid(); + indexMappingHash = mapping == null ? shardUuid : shardUuid + mapping.getSha256(); } if (indexMappingHash != null) { final Map existing = indexMappingHashToResponses.get(indexMappingHash); @@ -160,16 +157,19 @@ static Map retrieveFieldCaps( ) { boolean includeParentObjects = checkIncludeParents(filters); - Predicate filter = buildFilter(indexFieldfilter, filters, types, context); + Predicate filter = buildFilter(filters, types, context); boolean isTimeSeriesIndex = context.getIndexSettings().getTimestampBounds() != null; + var fieldInfos = indexShard.getFieldInfos(); + includeEmptyFields = includeEmptyFields || enableFieldHasValue == false; Map responseMap = new HashMap<>(); for (String field : context.getAllFieldNames()) { if (fieldNameFilter.test(field) == false) { continue; } MappedFieldType ft = context.getFieldType(field); - boolean includeField = includeEmptyFields || enableFieldHasValue == false || ft.fieldHasValue(indexShard.getFieldInfos()); - if (includeField && filter.test(ft)) { + if ((includeEmptyFields || ft.fieldHasValue(fieldInfos)) + && (indexFieldfilter.test(ft.name()) || context.isMetadataField(ft.name())) + && (filter == null || filter.test(ft))) { IndexFieldCapabilities fieldCap = new IndexFieldCapabilities( field, ft.familyTypeName(), @@ -245,17 +245,12 @@ private static boolean alwaysMatches(QueryBuilder indexFilter) { return indexFilter == null || indexFilter instanceof MatchAllQueryBuilder; } - private static Predicate buildFilter( - Predicate fieldFilter, - String[] filters, - String[] fieldTypes, - SearchExecutionContext context - ) { + private static Predicate buildFilter(String[] filters, String[] fieldTypes, SearchExecutionContext context) { // security filters don't exclude metadata fields - Predicate fcf = ft -> fieldFilter.test(ft.name()) || context.isMetadataField(ft.name()); + Predicate fcf = null; if (fieldTypes.length > 0) { Set acceptedTypes = Set.of(fieldTypes); - fcf = fcf.and(ft -> acceptedTypes.contains(ft.familyTypeName())); + fcf = ft -> acceptedTypes.contains(ft.familyTypeName()); } for (String filter : filters) { if ("parent".equals(filter) || "-parent".equals(filter)) { @@ -268,7 +263,7 @@ private static Predicate buildFilter( case "-multifield" -> ft -> context.isMultiField(ft.name()) == false; default -> throw new IllegalArgumentException("Unknown field caps filter [" + filter + "]"); }; - fcf = fcf.and(next); + fcf = fcf == null ? next : fcf.and(next); } return fcf; } From 4b8b09f0058e92ba26c1b890995cca7e0f2c969c Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Fri, 23 Feb 2024 13:40:11 -0500 Subject: [PATCH 183/250] Muting test for issue #105794 (#105795) related to: https://github.com/elastic/elasticsearch/issues/105794 --- .../integration/TransformInsufficientPermissionsIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformInsufficientPermissionsIT.java b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformInsufficientPermissionsIT.java index 59a673790723e..105633c7340e5 100644 --- a/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformInsufficientPermissionsIT.java +++ b/x-pack/plugin/transform/qa/multi-node-tests/src/javaRestTest/java/org/elasticsearch/xpack/transform/integration/TransformInsufficientPermissionsIT.java @@ -440,6 +440,7 @@ public void testTransformPermissionsDeferUnattendedNoDest() throws Exception { * unattended = true * pre-existing dest index = true */ + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105794") public void testTransformPermissionsDeferUnattendedDest() throws Exception { String transformId = "transform-permissions-defer-unattended-dest-exists"; String sourceIndexName = transformId + "-index"; From 5f3e4aeab6481201a238e4f4a24cbccfab8d14ea Mon Sep 17 00:00:00 2001 From: Andrei Stefan Date: Sat, 24 Feb 2024 12:14:56 +0200 Subject: [PATCH 184/250] ESQL: push down "[text_field] is not null" and "[text_field] is null"(#105593) --- docs/changelog/105593.yaml | 5 + .../src/main/resources/stats.csv-spec | 22 ++++ .../optimizer/LocalPhysicalPlanOptimizer.java | 5 + .../LocalPhysicalPlanOptimizerTests.java | 111 ++++++++++++++++++ .../test/esql/81_text_exact_subfields.yml | 23 ++++ 5 files changed, 166 insertions(+) create mode 100644 docs/changelog/105593.yaml diff --git a/docs/changelog/105593.yaml b/docs/changelog/105593.yaml new file mode 100644 index 0000000000000..4eef0d9404f42 --- /dev/null +++ b/docs/changelog/105593.yaml @@ -0,0 +1,5 @@ +pr: 105593 +summary: "ESQL: push down \"[text_field] is not null\"" +area: ES|QL +type: enhancement +issues: [] diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec index 97b36859c1419..4aff4c689c077 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/stats.csv-spec @@ -1195,3 +1195,25 @@ ROW a = 1 | STATS couNt(*) | SORT `couNt(*)` couNt(*):l 1 ; + +isNullWithStatsCount_On_TextField +FROM airports +| EVAL s = name, x = name +| WHERE s IS NULL +| STATS c = COUNT(x) +; + +c:l +0 +; + +isNotNullWithStatsCount_On_TextField +FROM airports +| EVAL s = name, x = name +| WHERE s IS NOT NULL +| STATS c = COUNT(x) +; + +c:l +891 +; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizer.java index 279ce3185d4aa..7ae8e029fd761 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizer.java @@ -249,6 +249,11 @@ public static boolean canPushToSource(Expression exp, Predicate return canPushToSource(not.field(), hasIdenticalDelegate); } else if (exp instanceof UnaryScalarFunction usf) { if (usf instanceof RegexMatch || usf instanceof IsNull || usf instanceof IsNotNull) { + if (usf instanceof IsNull || usf instanceof IsNotNull) { + if (usf.field() instanceof FieldAttribute fa && fa.dataType().equals(DataTypes.TEXT)) { + return true; + } + } return isAttributePushable(usf.field(), usf, hasIdenticalDelegate); } } else if (exp instanceof CIDRMatch cidrMatch) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java index 55320cfbeca32..cf387245a5968 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java @@ -57,6 +57,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -399,6 +400,7 @@ public void testIsNotNullPushdownFilter() { /** * Expects + * * LimitExec[1000[INTEGER]] * \_ExchangeExec[[],false] * \_ProjectExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, job{f}#10, job.raw{f}#11, languages{f}#6, last_n @@ -420,6 +422,115 @@ public void testIsNullPushdownFilter() { assertThat(query.query().toString(), is(expected.toString())); } + /** + * Expects + * + * LimitExec[500[INTEGER]] + * \_AggregateExec[[],[COUNT(gender{f}#7) AS count(gender)],FINAL,null] + * \_ExchangeExec[[count{r}#15, seen{r}#16],true] + * \_AggregateExec[[],[COUNT(gender{f}#7) AS count(gender)],PARTIAL,8] + * \_FieldExtractExec[gender{f}#7] + * \_EsQueryExec[test], query[{"exists":{"field":"gender","boost":1.0}}][_doc{f}#17], limit[], sort[] estimatedRowSize[54] + */ + public void testIsNotNull_TextField_Pushdown() { + String textField = randomFrom("gender", "job"); + var plan = plan(String.format(Locale.ROOT, "from test | where %s is not null | stats count(%s)", textField, textField)); + + var limit = as(plan, LimitExec.class); + var finalAgg = as(limit.child(), AggregateExec.class); + var exchange = as(finalAgg.child(), ExchangeExec.class); + var partialAgg = as(exchange.child(), AggregateExec.class); + var fieldExtract = as(partialAgg.child(), FieldExtractExec.class); + var query = as(fieldExtract.child(), EsQueryExec.class); + var expected = QueryBuilders.existsQuery(textField); + assertThat(query.query().toString(), is(expected.toString())); + } + + /** + * Expects + * LimitExec[1000[INTEGER]] + * \_ExchangeExec[[],false] + * \_ProjectExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, job{f}#10, job.raw{f}#11, languages{f}#6, last_n + * ame{f}#7, long_noidx{f}#12, salary{f}#8]] + * \_FieldExtractExec[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gen..] + * \_EsQueryExec[test], query[{"bool":{"must_not":[{"exists":{"field":"gender","boost":1.0}}],"boost":1.0}}] + * [_doc{f}#13], limit[1000], sort[] estimatedRowSize[324] + */ + public void testIsNull_TextField_Pushdown() { + String textField = randomFrom("gender", "job"); + var plan = plan(String.format(Locale.ROOT, "from test | where %s is null", textField, textField)); + + var limit = as(plan, LimitExec.class); + var exchange = as(limit.child(), ExchangeExec.class); + var project = as(exchange.child(), ProjectExec.class); + var fieldExtract = as(project.child(), FieldExtractExec.class); + var query = as(fieldExtract.child(), EsQueryExec.class); + var expected = QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery(textField)); + assertThat(query.query().toString(), is(expected.toString())); + } + + /** + * count(x) adds an implicit "exists(x)" filter in the pushed down query + * This test checks this "exists" doesn't clash with the "is null" pushdown on the text field. + * In this particular query, "exists(x)" and "x is null" cancel each other out. + * + * Expects + * + * LimitExec[1000[INTEGER]] + * \_AggregateExec[[],[COUNT(job{f}#19) AS c],FINAL,8] + * \_ExchangeExec[[count{r}#22, seen{r}#23],true] + * \_LocalSourceExec[[count{r}#22, seen{r}#23],[LongVectorBlock[vector=ConstantLongVector[positions=1, value=0]], BooleanVectorBlock + * [vector=ConstantBooleanVector[positions=1, value=true]]]] + */ + public void testIsNull_TextField_Pushdown_WithCount() { + var plan = plan(""" + from test + | eval filtered_job = job, count_job = job + | where filtered_job IS NULL + | stats c = COUNT(count_job) + """, IS_SV_STATS); + + var limit = as(plan, LimitExec.class); + var agg = as(limit.child(), AggregateExec.class); + var exg = as(agg.child(), ExchangeExec.class); + as(exg.child(), LocalSourceExec.class); + } + + /** + * count(x) adds an implicit "exists(x)" filter in the pushed down query. + * This test checks this "exists" doesn't clash with the "is null" pushdown on the text field. + * In this particular query, "exists(x)" and "x is not null" go hand in hand and the query is pushed down to Lucene. + * + * Expects + * + * LimitExec[1000[INTEGER]] + * \_AggregateExec[[],[COUNT(job{f}#19) AS c],FINAL,8] + * \_ExchangeExec[[count{r}#22, seen{r}#23],true] + * \_EsStatsQueryExec[test], stats[Stat[name=job, type=COUNT, query={ + * "exists" : { + * "field" : "job", + * "boost" : 1.0 + * } + * }]]], query[{"exists":{"field":"job","boost":1.0}}][count{r}#25, seen{r}#26], limit[], + */ + public void testIsNotNull_TextField_Pushdown_WithCount() { + var plan = plan(""" + from test + | eval filtered_job = job, count_job = job + | where filtered_job IS NOT NULL + | stats c = COUNT(count_job) + """, IS_SV_STATS); + + var limit = as(plan, LimitExec.class); + var agg = as(limit.child(), AggregateExec.class); + var exg = as(agg.child(), ExchangeExec.class); + var esStatsQuery = as(exg.child(), EsStatsQueryExec.class); + assertThat(esStatsQuery.limit(), is(nullValue())); + assertThat(Expressions.names(esStatsQuery.output()), contains("count", "seen")); + var stat = as(esStatsQuery.stats().get(0), Stat.class); + assertThat(stat.query(), is(QueryBuilders.existsQuery("job"))); + } + /** * Expects * LimitExec[1000[INTEGER]] diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/81_text_exact_subfields.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/81_text_exact_subfields.yml index 64d4665e3cfe7..3b58ee01edfa0 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/81_text_exact_subfields.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/81_text_exact_subfields.yml @@ -147,6 +147,29 @@ setup: - length: { values: 0 } + - do: + allowed_warnings_regex: + - "No limit defined, adding default limit of \\[.*\\]" + esql.query: + body: + query: 'from test | where text_ignore_above is not null | keep text_ignore_above, text_ignore_above.raw, text_normalizer, text_normalizer.raw, non_indexed, non_indexed.raw' + + - match: { columns.0.name: "text_ignore_above" } + - match: { columns.0.type: "text" } + - match: { columns.1.name: "text_ignore_above.raw" } + - match: { columns.1.type: "keyword" } + - match: { columns.2.name: "text_normalizer" } + - match: { columns.2.type: "text" } + - match: { columns.3.name: "text_normalizer.raw" } + - match: { columns.3.type: "keyword" } + - match: { columns.4.name: "non_indexed" } + - match: { columns.4.type: "text" } + - match: { columns.5.name: "non_indexed.raw" } + - match: { columns.5.type: "keyword" } + + - length: { values: 2 } + + - do: allowed_warnings_regex: - "No limit defined, adding default limit of \\[.*\\]" From 50090f1a9c025379a73b4743a3a9dcf2c3b1847d Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Sat, 24 Feb 2024 13:18:43 +0100 Subject: [PATCH 185/250] Simplify LeakTracker by using java.lang.ref.Cleaner (#105798) Using the cleaner here simplifies the logic quite a bit and removes the need for us to reason through reference queues and weak references. Also from my testing, this solution is actually more sensitive and a leak will be reported closer to the execution tht created it in the first place. --- .../elasticsearch/transport/LeakTracker.java | 112 +++++++----------- .../common/util/MockPageCacheRecycler.java | 4 +- .../org/elasticsearch/test/ESTestCase.java | 1 - 3 files changed, 43 insertions(+), 74 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/transport/LeakTracker.java b/server/src/main/java/org/elasticsearch/transport/LeakTracker.java index 77a41cff15fd7..75bab40a3d9a0 100644 --- a/server/src/main/java/org/elasticsearch/transport/LeakTracker.java +++ b/server/src/main/java/org/elasticsearch/transport/LeakTracker.java @@ -17,10 +17,10 @@ import org.elasticsearch.core.RefCounted; import org.elasticsearch.core.Releasable; -import java.lang.ref.ReferenceQueue; -import java.lang.ref.WeakReference; +import java.lang.ref.Cleaner; import java.util.Set; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; @@ -32,11 +32,10 @@ public final class LeakTracker { private static final Logger logger = LogManager.getLogger(LeakTracker.class); - private static final int TARGET_RECORDS = 25; + private static final Cleaner cleaner = Cleaner.create(); - private final Set> allLeaks = ConcurrentCollections.newConcurrentSet(); + private static final int TARGET_RECORDS = 25; - private final ReferenceQueue refQueue = new ReferenceQueue<>(); private final ConcurrentMap reportedLeaks = ConcurrentCollections.newConcurrentMap(); public static final LeakTracker INSTANCE = new LeakTracker(); @@ -49,29 +48,10 @@ private LeakTracker() {} * Track the given object. * * @param obj object to track - * @return leak object that must be released by a call to {@link Leak#close(Object)} before {@code obj} goes out of scope + * @return leak object that must be released by a call to {@link LeakTracker.Leak#close()} before {@code obj} goes out of scope */ - public Leak track(T obj) { - reportLeak(); - return new Leak<>(obj, refQueue, allLeaks); - } - - public void reportLeak() { - while (true) { - Leak ref = (Leak) refQueue.poll(); - if (ref == null) { - break; - } - - if (ref.dispose() == false || logger.isErrorEnabled() == false) { - continue; - } - - String records = ref.toString(); - if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) { - logger.error("LEAK: resource was not cleaned up before it was garbage-collected.{}", records); - } - } + public Leak track(Object obj) { + return new Leak(obj); } /** @@ -94,7 +74,7 @@ public void close() { try { releasable.close(); } finally { - leak.close(releasable); + leak.close(); } } @@ -135,7 +115,7 @@ public boolean tryIncRef() { @Override public boolean decRef() { if (refCounted.decRef()) { - leak.close(refCounted); + leak.close(); return true; } leak.record(); @@ -163,33 +143,43 @@ public boolean equals(Object obj) { }; } - public static final class Leak extends WeakReference { + public final class Leak implements Runnable { - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static final AtomicReferenceFieldUpdater, Record> headUpdater = - (AtomicReferenceFieldUpdater) AtomicReferenceFieldUpdater.newUpdater(Leak.class, Record.class, "head"); + private static final AtomicReferenceFieldUpdater headUpdater = AtomicReferenceFieldUpdater.newUpdater( + Leak.class, + Record.class, + "head" + ); - @SuppressWarnings({ "unchecked", "rawtypes" }) - private static final AtomicIntegerFieldUpdater> droppedRecordsUpdater = - (AtomicIntegerFieldUpdater) AtomicIntegerFieldUpdater.newUpdater(Leak.class, "droppedRecords"); + private static final AtomicIntegerFieldUpdater droppedRecordsUpdater = AtomicIntegerFieldUpdater.newUpdater( + Leak.class, + "droppedRecords" + ); @SuppressWarnings("unused") private volatile Record head; @SuppressWarnings("unused") private volatile int droppedRecords; - private final Set> allLeaks; - private final int trackedHash; + private final AtomicBoolean closed = new AtomicBoolean(false); - private Leak(Object referent, ReferenceQueue refQueue, Set> allLeaks) { - super(referent, refQueue); + private final Cleaner.Cleanable cleanable; - assert referent != null; - - trackedHash = System.identityHashCode(referent); - allLeaks.add(this); + @SuppressWarnings("this-escape") + private Leak(Object referent) { + this.cleanable = cleaner.register(referent, this); headUpdater.set(this, new Record(Record.BOTTOM)); - this.allLeaks = allLeaks; + } + + @Override + public void run() { + if (closed.compareAndSet(false, true) == false || logger.isErrorEnabled() == false) { + return; + } + String records = toString(); + if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) { + logger.error("LEAK: resource was not cleaned up before it was garbage-collected.{}", records); + } } /** @@ -221,38 +211,18 @@ public void record() { } } - private boolean dispose() { - clear(); - return allLeaks.remove(this); - } - /** * Stop tracking the object that this leak was created for. * - * @param trackedObject the object that this leak was originally created for * @return true if the leak was released by this call, false if the leak had already been released */ - public boolean close(T trackedObject) { - assert trackedHash == System.identityHashCode(trackedObject); - try { - if (allLeaks.remove(this)) { - // Call clear so the reference is not even enqueued. - clear(); - headUpdater.set(this, null); - return true; - } - return false; - } finally { - reachabilityFence0(trackedObject); - } - } - - private static void reachabilityFence0(Object ref) { - if (ref != null) { - synchronized (ref) { - // empty on purpose - } + public boolean close() { + if (closed.compareAndSet(false, true)) { + cleanable.clean(); + headUpdater.set(this, null); + return true; } + return false; } @Override diff --git a/test/framework/src/main/java/org/elasticsearch/common/util/MockPageCacheRecycler.java b/test/framework/src/main/java/org/elasticsearch/common/util/MockPageCacheRecycler.java index 56dea95b6a282..80f3db60e9432 100644 --- a/test/framework/src/main/java/org/elasticsearch/common/util/MockPageCacheRecycler.java +++ b/test/framework/src/main/java/org/elasticsearch/common/util/MockPageCacheRecycler.java @@ -31,11 +31,11 @@ public MockPageCacheRecycler(Settings settings) { private V wrap(final V v) { return new V() { - private final LeakTracker.Leak> leak = LeakTracker.INSTANCE.track(v); + private final LeakTracker.Leak leak = LeakTracker.INSTANCE.track(v); @Override public void close() { - boolean leakReleased = leak.close(v); + boolean leakReleased = leak.close(); assert leakReleased : "leak should not have been released already"; final T ref = v(); if (ref instanceof Object[]) { 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 67919756e16a9..06d09f3942a1c 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -704,7 +704,6 @@ public void log(StatusData data) { // separate method so that this can be checked again after suite scoped cluster is shut down protected static void checkStaticState() throws Exception { - LeakTracker.INSTANCE.reportLeak(); MockBigArrays.ensureAllArraysAreReleased(); // ensure no one changed the status logger level on us From 3587bc828e90116661b2c9d9491fb04f88332684 Mon Sep 17 00:00:00 2001 From: David Kyle Date: Sun, 25 Feb 2024 11:06:21 +0000 Subject: [PATCH 186/250] [ML] Rename the internal text embedding service to elasticsearch (#105753) --- .../inference/put-inference.asciidoc | 134 +++++++++--------- .../InferenceNamedWriteablesProvider.java | 8 +- .../xpack/inference/InferencePlugin.java | 4 +- .../CustomElandInternalServiceSettings.java | 6 +- .../CustomElandModel.java | 4 +- .../ElasticsearchInternalService.java} | 24 ++-- ...ElasticsearchInternalServiceSettings.java} | 12 +- .../ElasticsearchModel.java} | 12 +- ...lingualE5SmallInternalServiceSettings.java | 10 +- .../MultilingualE5SmallModel.java | 6 +- .../ElasticsearchInternalServiceTests.java} | 68 ++++----- ...alE5SmallInternalServiceSettingsTests.java | 8 +- 12 files changed, 144 insertions(+), 152 deletions(-) rename x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/{textembedding => elasticsearch}/CustomElandInternalServiceSettings.java (93%) rename x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/{textembedding => elasticsearch}/CustomElandModel.java (96%) rename x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/{textembedding/TextEmbeddingInternalService.java => elasticsearch/ElasticsearchInternalService.java} (95%) rename x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/{textembedding/TextEmbeddingInternalServiceSettings.java => elasticsearch/ElasticsearchInternalServiceSettings.java} (74%) rename x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/{textembedding/TextEmbeddingModel.java => elasticsearch/ElasticsearchModel.java} (77%) rename x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/{textembedding => elasticsearch}/MultilingualE5SmallInternalServiceSettings.java (93%) rename x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/{textembedding => elasticsearch}/MultilingualE5SmallModel.java (94%) rename x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/{textembedding/TextEmbeddingInternalServiceTests.java => elasticsearch/ElasticsearchInternalServiceTests.java} (86%) rename x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/{textembedding => elasticsearch}/MultilingualE5SmallInternalServiceSettingsTests.java (95%) diff --git a/docs/reference/inference/put-inference.asciidoc b/docs/reference/inference/put-inference.asciidoc index 5332808d2ce12..2c0d4d38548bb 100644 --- a/docs/reference/inference/put-inference.asciidoc +++ b/docs/reference/inference/put-inference.asciidoc @@ -6,11 +6,11 @@ experimental[] Creates a model to perform an {infer} task. -IMPORTANT: The {infer} APIs enable you to use certain services, such as built-in -{ml} models (ELSER, E5), models uploaded through Eland, Cohere, OpenAI, or -Hugging Face, in your cluster. For built-in models and models uploaded though -Eland, the {infer} APIs offer an alternative way to use and manage trained -models. However, if you do not plan to use the {infer} APIs to use these models +IMPORTANT: The {infer} APIs enable you to use certain services, such as built-in +{ml} models (ELSER, E5), models uploaded through Eland, Cohere, OpenAI, or +Hugging Face. For built-in models and models uploaded though +Eland, the {infer} APIs offer an alternative way to use and manage trained +models. However, if you do not plan to use the {infer} APIs to use these models or if you want to use non-NLP models, use the <>. @@ -41,7 +41,7 @@ The following services are available through the {infer} API: * ELSER * Hugging Face * OpenAI -* text embedding (for built-in models and models uploaded through Eland) +* Elasticsearch (for built-in models and models uploaded through Eland) [discrete] @@ -68,12 +68,12 @@ The type of the {infer} task that the model will perform. Available task types: (Required, string) The type of service supported for the specified task type. Available services: -* `cohere`: specify the `text_embedding` task type to use the Cohere service. +* `cohere`: specify the `text_embedding` task type to use the Cohere service. * `elser`: specify the `sparse_embedding` task type to use the ELSER service. -* `hugging_face`: specify the `text_embedding` task type to use the Hugging Face +* `hugging_face`: specify the `text_embedding` task type to use the Hugging Face service. * `openai`: specify the `text_embedding` task type to use the OpenAI service. -* `text_embedding`: specify the `text_embedding` task type to use the E5 +* `elasticsearch`: specify the `text_embedding` task type to use the E5 built-in model or text embedding models uploaded by Eland. `service_settings`:: @@ -86,14 +86,14 @@ Settings used to install the {infer} model. These settings are specific to the ===== `api_key`::: (Required, string) -A valid API key of your Cohere account. You can find your Cohere API keys or you -can create a new one +A valid API key of your Cohere account. You can find your Cohere API keys or you +can create a new one https://dashboard.cohere.com/api-keys[on the API keys settings page]. -IMPORTANT: You need to provide the API key only once, during the {infer} model -creation. The <> does not retrieve your API key. After -creating the {infer} model, you cannot change the associated API key. If you -want to use a different API key, delete the {infer} model and recreate it with +IMPORTANT: You need to provide the API key only once, during the {infer} model +creation. The <> does not retrieve your API key. After +creating the {infer} model, you cannot change the associated API key. If you +want to use a different API key, delete the {infer} model and recreate it with the same name and the updated API key. `embedding_type`:: @@ -105,9 +105,9 @@ Valid values are: `model_id`:: (Optional, string) -The name of the model to use for the {infer} task. To review the available -models, refer to the -https://docs.cohere.com/reference/embed[Cohere docs]. Defaults to +The name of the model to use for the {infer} task. To review the available +models, refer to the +https://docs.cohere.com/reference/embed[Cohere docs]. Defaults to `embed-english-v2.0`. ===== + @@ -116,13 +116,13 @@ https://docs.cohere.com/reference/embed[Cohere docs]. Defaults to ===== `num_allocations`::: (Required, integer) -The number of model allocations to create. `num_allocations` must not exceed the +The number of model allocations to create. `num_allocations` must not exceed the number of available processors per node divided by the `num_threads`. `num_threads`::: (Required, integer) -The number of threads to use by each model allocation. `num_threads` must not -exceed the number of available processors per node divided by the number of +The number of threads to use by each model allocation. `num_threads` must not +exceed the number of available processors per node divided by the number of allocations. Must be a power of 2. Max allowed value is 32. ===== + @@ -131,14 +131,14 @@ allocations. Must be a power of 2. Max allowed value is 32. ===== `api_key`::: (Required, string) -A valid access token of your Hugging Face account. You can find your Hugging -Face access tokens or you can create a new one +A valid access token of your Hugging Face account. You can find your Hugging +Face access tokens or you can create a new one https://huggingface.co/settings/tokens[on the settings page]. -IMPORTANT: You need to provide the API key only once, during the {infer} model -creation. The <> does not retrieve your API key. After -creating the {infer} model, you cannot change the associated API key. If you -want to use a different API key, delete the {infer} model and recreate it with +IMPORTANT: You need to provide the API key only once, during the {infer} model +creation. The <> does not retrieve your API key. After +creating the {infer} model, you cannot change the associated API key. If you +want to use a different API key, delete the {infer} model and recreate it with the same name and the updated API key. `url`::: @@ -151,21 +151,21 @@ The URL endpoint to use for the requests. ===== `api_key`::: (Required, string) -A valid API key of your OpenAI account. You can find your OpenAI API keys in -your OpenAI account under the +A valid API key of your OpenAI account. You can find your OpenAI API keys in +your OpenAI account under the https://platform.openai.com/api-keys[API keys section]. -IMPORTANT: You need to provide the API key only once, during the {infer} model -creation. The <> does not retrieve your API key. After -creating the {infer} model, you cannot change the associated API key. If you -want to use a different API key, delete the {infer} model and recreate it with +IMPORTANT: You need to provide the API key only once, during the {infer} model +creation. The <> does not retrieve your API key. After +creating the {infer} model, you cannot change the associated API key. If you +want to use a different API key, delete the {infer} model and recreate it with the same name and the updated API key. `organization_id`::: (Optional, string) -The unique identifier of your organization. You can find the Organization ID in -your OpenAI account under -https://platform.openai.com/account/organization[**Settings** > **Organizations**]. +The unique identifier of your organization. You can find the Organization ID in +your OpenAI account under +https://platform.openai.com/account/organization[**Settings** > **Organizations**]. `url`::: (Optional, string) @@ -173,25 +173,25 @@ The URL endpoint to use for the requests. Can be changed for testing purposes. Defaults to `https://api.openai.com/v1/embeddings`. ===== + -.`service_settings` for the `text_embedding` service +.`service_settings` for the `elasticsearch` service [%collapsible%closed] ===== `model_id`::: (Required, string) -The name of the text embedding model to use for the {infer} task. It can be the -ID of either a built-in model (for example, `.multilingual-e5-small` for E5) or +The name of the model to use for the {infer} task. It can be the +ID of either a built-in model (for example, `.multilingual-e5-small` for E5) or a text embedding model already {ml-docs}/ml-nlp-import-model.html#ml-nlp-import-script[uploaded through Eland]. `num_allocations`::: (Required, integer) -The number of model allocations to create. `num_allocations` must not exceed the +The number of model allocations to create. `num_allocations` must not exceed the number of available processors per node divided by the `num_threads`. `num_threads`::: (Required, integer) -The number of threads to use by each model allocation. `num_threads` must not -exceed the number of available processors per node divided by the number of +The number of threads to use by each model allocation. `num_threads` must not +exceed the number of available processors per node divided by the number of allocations. Must be a power of 2. Max allowed value is 32. ===== @@ -211,26 +211,26 @@ Valid values are: * `classification`: use it for embeddings passed through a text classifier. * `clusterning`: use it for the embeddings run through a clustering algorithm. * `ingest`: use it for storing document embeddings in a vector database. - * `search`: use it for storing embeddings of search queries run against a + * `search`: use it for storing embeddings of search queries run against a vector data base to find relevant documents. `model`::: (Optional, string) -For `openai` sevice only. The name of the model to use for the {infer} task. Refer -to the +For `openai` sevice only. The name of the model to use for the {infer} task. Refer +to the https://platform.openai.com/docs/guides/embeddings/what-are-embeddings[OpenAI documentation] for the list of available text embedding models. `truncate`::: (Optional, string) -For `cohere` service only. Specifies how the API handles inputs longer than the +For `cohere` service only. Specifies how the API handles inputs longer than the maximum token length. Defaults to `END`. Valid values are: - * `NONE`: when the input exceeds the maximum input token length an error is + * `NONE`: when the input exceeds the maximum input token length an error is returned. - * `START`: when the input exceeds the maximum input token length the start of + * `START`: when the input exceeds the maximum input token length the start of + the input is discarded. + * `END`: when the input exceeds the maximum input token length the end of the input is discarded. - * `END`: when the input exceeds the maximum input token length the end of - the input is discarded. ===== @@ -267,7 +267,7 @@ PUT _inference/text_embedding/cohere-embeddings [discrete] [[inference-example-e5]] -===== E5 via the text embedding service +===== E5 via the elasticsearch service The following example shows how to create an {infer} model called `my-e5-model` to perform a `text_embedding` task type. @@ -276,7 +276,7 @@ The following example shows how to create an {infer} model called ------------------------------------------------------------ PUT _inference/text_embedding/my-e5-model { - "service": "text_embedding", + "service": "elasticsearch", "service_settings": { "num_allocations": 1, "num_threads": 1, @@ -285,8 +285,8 @@ PUT _inference/text_embedding/my-e5-model } ------------------------------------------------------------ // TEST[skip:TBD] -<1> The `model_id` must be the ID of one of the built-in E5 models. Valid values -are `.multilingual-e5-small` and `.multilingual-e5-small_linux-x86_64`. For +<1> The `model_id` must be the ID of one of the built-in E5 models. Valid values +are `.multilingual-e5-small` and `.multilingual-e5-small_linux-x86_64`. For further details, refer to the {ml-docs}/ml-nlp-e5.html[E5 model documentation]. @@ -339,7 +339,7 @@ The following example shows how to create an {infer} model called [source,console] ------------------------------------------------------------ -PUT _inference/text_embedding/hugging-face-embeddings +PUT _inference/text_embedding/hugging-face-embeddings { "service": "hugging_face", "service_settings": { @@ -349,20 +349,20 @@ PUT _inference/text_embedding/hugging-face-embeddings } ------------------------------------------------------------ // TEST[skip:TBD] -<1> A valid Hugging Face access token. You can find on the +<1> A valid Hugging Face access token. You can find on the https://huggingface.co/settings/tokens[settings page of your account]. -<2> The {infer} endpoint URL you created on Hugging Face. +<2> The {infer} endpoint URL you created on Hugging Face. -Create a new {infer} endpoint on -https://ui.endpoints.huggingface.co/[the Hugging Face endpoint page] to get an -endpoint URL. Select the model you want to use on the new endpoint creation page -- for example `intfloat/e5-small-v2` - then select the `Sentence Embeddings` -task under the Advanced configuration section. Create the endpoint. Copy the URL +Create a new {infer} endpoint on +https://ui.endpoints.huggingface.co/[the Hugging Face endpoint page] to get an +endpoint URL. Select the model you want to use on the new endpoint creation page +- for example `intfloat/e5-small-v2` - then select the `Sentence Embeddings` +task under the Advanced configuration section. Create the endpoint. Copy the URL after the endpoint initialization has been finished. [discrete] [[inference-example-eland]] -===== Models uploaded by Eland via the text embedding service +===== Models uploaded by Eland via the elasticsearch service The following example shows how to create an {infer} model called `my-msmarco-minilm-model` to perform a `text_embedding` task type. @@ -371,7 +371,7 @@ The following example shows how to create an {infer} model called ------------------------------------------------------------ PUT _inference/text_embedding/my-msmarco-minilm-model { - "service": "text_embedding", + "service": "elasticsearch", "service_settings": { "num_allocations": 1, "num_threads": 1, @@ -380,8 +380,8 @@ PUT _inference/text_embedding/my-msmarco-minilm-model } ------------------------------------------------------------ // TEST[skip:TBD] -<1> The `model_id` must be the ID of a text embedding model which has already -been +<1> The `model_id` must be the ID of a text embedding model which has already +been {ml-docs}/ml-nlp-import-model.html#ml-nlp-import-script[uploaded through Eland]. @@ -405,4 +405,4 @@ PUT _inference/text_embedding/openai_embeddings } } ------------------------------------------------------------ -// TEST[skip:TBD] \ No newline at end of file +// TEST[skip:TBD] 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 55e4200a3b5ed..c38b427200744 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 @@ -23,6 +23,8 @@ import org.elasticsearch.xpack.inference.services.cohere.CohereServiceSettings; import org.elasticsearch.xpack.inference.services.cohere.embeddings.CohereEmbeddingsServiceSettings; import org.elasticsearch.xpack.inference.services.cohere.embeddings.CohereEmbeddingsTaskSettings; +import org.elasticsearch.xpack.inference.services.elasticsearch.ElasticsearchInternalServiceSettings; +import org.elasticsearch.xpack.inference.services.elasticsearch.MultilingualE5SmallInternalServiceSettings; import org.elasticsearch.xpack.inference.services.elser.ElserInternalServiceSettings; import org.elasticsearch.xpack.inference.services.elser.ElserMlNodeTaskSettings; import org.elasticsearch.xpack.inference.services.huggingface.HuggingFaceServiceSettings; @@ -31,8 +33,6 @@ import org.elasticsearch.xpack.inference.services.openai.embeddings.OpenAiEmbeddingsServiceSettings; import org.elasticsearch.xpack.inference.services.openai.embeddings.OpenAiEmbeddingsTaskSettings; import org.elasticsearch.xpack.inference.services.settings.DefaultSecretSettings; -import org.elasticsearch.xpack.inference.services.textembedding.MultilingualE5SmallInternalServiceSettings; -import org.elasticsearch.xpack.inference.services.textembedding.TextEmbeddingInternalServiceSettings; import java.util.ArrayList; import java.util.List; @@ -96,8 +96,8 @@ public static List getNamedWriteables() { namedWriteables.add( new NamedWriteableRegistry.Entry( ServiceSettings.class, - TextEmbeddingInternalServiceSettings.NAME, - TextEmbeddingInternalServiceSettings::new + ElasticsearchInternalServiceSettings.NAME, + ElasticsearchInternalServiceSettings::new ) ); namedWriteables.add( 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 eb11ebe782216..1c5e5d4e9ef94 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 @@ -60,11 +60,11 @@ import org.elasticsearch.xpack.inference.rest.RestPutInferenceModelAction; import org.elasticsearch.xpack.inference.services.ServiceComponents; import org.elasticsearch.xpack.inference.services.cohere.CohereService; +import org.elasticsearch.xpack.inference.services.elasticsearch.ElasticsearchInternalService; import org.elasticsearch.xpack.inference.services.elser.ElserInternalService; import org.elasticsearch.xpack.inference.services.huggingface.HuggingFaceService; import org.elasticsearch.xpack.inference.services.huggingface.elser.HuggingFaceElserService; import org.elasticsearch.xpack.inference.services.openai.OpenAiService; -import org.elasticsearch.xpack.inference.services.textembedding.TextEmbeddingInternalService; import java.util.ArrayList; import java.util.Collection; @@ -184,7 +184,7 @@ public List getInferenceServiceFactories() { context -> new HuggingFaceService(httpFactory.get(), serviceComponents.get()), context -> new OpenAiService(httpFactory.get(), serviceComponents.get()), context -> new CohereService(httpFactory.get(), serviceComponents.get()), - TextEmbeddingInternalService::new + ElasticsearchInternalService::new ); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/CustomElandInternalServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandInternalServiceSettings.java similarity index 93% rename from x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/CustomElandInternalServiceSettings.java rename to x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandInternalServiceSettings.java index 49cf3fdcd9e89..ee22d51914b15 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/CustomElandInternalServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandInternalServiceSettings.java @@ -3,11 +3,9 @@ * 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. - * - * this file was contributed to by a generative AI */ -package org.elasticsearch.xpack.inference.services.textembedding; +package org.elasticsearch.xpack.inference.services.elasticsearch; import org.elasticsearch.TransportVersion; import org.elasticsearch.common.ValidationException; @@ -21,7 +19,7 @@ import static org.elasticsearch.TransportVersions.ML_TEXT_EMBEDDING_INFERENCE_SERVICE_ADDED; -public class CustomElandInternalServiceSettings extends TextEmbeddingInternalServiceSettings { +public class CustomElandInternalServiceSettings extends ElasticsearchInternalServiceSettings { public static final String NAME = "custom_eland_model_internal_service_settings"; diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/CustomElandModel.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandModel.java similarity index 96% rename from x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/CustomElandModel.java rename to x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandModel.java index 5d7b63431841f..aa05af9461565 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/CustomElandModel.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/CustomElandModel.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.inference.services.textembedding; +package org.elasticsearch.xpack.inference.services.elasticsearch; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; @@ -17,7 +17,7 @@ import static org.elasticsearch.xpack.core.ml.inference.assignment.AllocationStatus.State.STARTED; -public class CustomElandModel extends TextEmbeddingModel { +public class CustomElandModel extends ElasticsearchModel { public CustomElandModel( String inferenceEntityId, diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/TextEmbeddingInternalService.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalService.java similarity index 95% rename from x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/TextEmbeddingInternalService.java rename to x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalService.java index 06d6545a381bd..1aafa340268f3 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/TextEmbeddingInternalService.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalService.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.inference.services.textembedding; +package org.elasticsearch.xpack.inference.services.elasticsearch; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -52,9 +52,9 @@ import static org.elasticsearch.xpack.inference.services.ServiceUtils.throwIfNotEmptyMap; import static org.elasticsearch.xpack.inference.services.settings.InternalServiceSettings.MODEL_ID; -public class TextEmbeddingInternalService implements InferenceService { +public class ElasticsearchInternalService implements InferenceService { - public static final String NAME = "text_embedding"; + public static final String NAME = "elasticsearch"; static final String MULTILINGUAL_E5_SMALL_MODEL_ID = ".multilingual-e5-small"; static final String MULTILINGUAL_E5_SMALL_MODEL_ID_LINUX_X86 = ".multilingual-e5-small_linux-x86_64"; @@ -65,9 +65,9 @@ public class TextEmbeddingInternalService implements InferenceService { private final OriginSettingClient client; - private static final Logger logger = LogManager.getLogger(TextEmbeddingInternalService.class); + private static final Logger logger = LogManager.getLogger(ElasticsearchInternalService.class); - public TextEmbeddingInternalService(InferenceServiceExtension.InferenceServiceFactoryContext context) { + public ElasticsearchInternalService(InferenceServiceExtension.InferenceServiceFactoryContext context) { this.client = new OriginSettingClient(context.client(), ClientHelper.INFERENCE_ORIGIN); } @@ -168,7 +168,7 @@ private static boolean modelVariantDoesNotMatchArchitecturesAndIsNotPlatformAgno } @Override - public TextEmbeddingModel parsePersistedConfigWithSecrets( + public ElasticsearchModel parsePersistedConfigWithSecrets( String inferenceEntityId, TaskType taskType, Map config, @@ -178,7 +178,7 @@ public TextEmbeddingModel parsePersistedConfigWithSecrets( } @Override - public TextEmbeddingModel parsePersistedConfig(String inferenceEntityId, TaskType taskType, Map config) { + public ElasticsearchModel parsePersistedConfig(String inferenceEntityId, TaskType taskType, Map config) { Map serviceSettingsMap = removeFromMapOrThrowIfNull(config, ModelConfigurations.SERVICE_SETTINGS); String modelId = (String) serviceSettingsMap.get(MODEL_ID); @@ -270,7 +270,7 @@ public void chunkedInfer( @Override public void start(Model model, ActionListener listener) { - if (model instanceof TextEmbeddingModel == false) { + if (model instanceof ElasticsearchModel == false) { listener.onFailure(notTextEmbeddingModelException(model)); return; } @@ -282,8 +282,8 @@ public void start(Model model, ActionListener listener) { return; } - var startRequest = ((TextEmbeddingModel) model).getStartTrainedModelDeploymentActionRequest(); - var responseListener = ((TextEmbeddingModel) model).getCreateTrainedModelAssignmentActionListener(model, listener); + var startRequest = ((ElasticsearchModel) model).getStartTrainedModelDeploymentActionRequest(); + var responseListener = ((ElasticsearchModel) model).getCreateTrainedModelAssignmentActionListener(model, listener); client.execute(StartTrainedModelDeploymentAction.INSTANCE, startRequest, responseListener); } @@ -299,7 +299,7 @@ public void stop(String inferenceEntityId, ActionListener listener) { @Override public void putModel(Model model, ActionListener listener) { - if (model instanceof TextEmbeddingModel == false) { + if (model instanceof ElasticsearchModel == false) { listener.onFailure(notTextEmbeddingModelException(model)); return; } else if (model instanceof MultilingualE5SmallModel e5Model) { @@ -347,7 +347,7 @@ public void isModelDownloaded(Model model, ActionListener listener) { } }); - if (model instanceof TextEmbeddingModel == false) { + if (model instanceof ElasticsearchModel == false) { listener.onFailure(notTextEmbeddingModelException(model)); } else if (model.getServiceSettings() instanceof InternalServiceSettings internalServiceSettings) { String modelId = internalServiceSettings.getModelId(); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/TextEmbeddingInternalServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalServiceSettings.java similarity index 74% rename from x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/TextEmbeddingInternalServiceSettings.java rename to x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalServiceSettings.java index fcc96703e221f..f6458b48f99fc 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/TextEmbeddingInternalServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchInternalServiceSettings.java @@ -3,11 +3,9 @@ * 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. - * - * This file was contributed to by a generative AI */ -package org.elasticsearch.xpack.inference.services.textembedding; +package org.elasticsearch.xpack.inference.services.elasticsearch; import org.elasticsearch.TransportVersion; import org.elasticsearch.common.io.stream.StreamInput; @@ -17,21 +15,21 @@ import static org.elasticsearch.TransportVersions.ML_TEXT_EMBEDDING_INFERENCE_SERVICE_ADDED; -public class TextEmbeddingInternalServiceSettings extends InternalServiceSettings { +public class ElasticsearchInternalServiceSettings extends InternalServiceSettings { public static final String NAME = "text_embedding_internal_service_settings"; - public TextEmbeddingInternalServiceSettings(int numAllocations, int numThreads, String modelVariant) { + public ElasticsearchInternalServiceSettings(int numAllocations, int numThreads, String modelVariant) { super(numAllocations, numThreads, modelVariant); } - public TextEmbeddingInternalServiceSettings(StreamInput in) throws IOException { + public ElasticsearchInternalServiceSettings(StreamInput in) throws IOException { super(in.readVInt(), in.readVInt(), in.readString()); } @Override public String getWriteableName() { - return TextEmbeddingInternalServiceSettings.NAME; + return ElasticsearchInternalServiceSettings.NAME; } @Override diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/TextEmbeddingModel.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchModel.java similarity index 77% rename from x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/TextEmbeddingModel.java rename to x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchModel.java index 800e2928c7afa..954469537a4cc 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/TextEmbeddingModel.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/ElasticsearchModel.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.inference.services.textembedding; +package org.elasticsearch.xpack.inference.services.elasticsearch; import org.elasticsearch.action.ActionListener; import org.elasticsearch.inference.Model; @@ -14,20 +14,20 @@ import org.elasticsearch.xpack.core.ml.action.CreateTrainedModelAssignmentAction; import org.elasticsearch.xpack.core.ml.action.StartTrainedModelDeploymentAction; -public abstract class TextEmbeddingModel extends Model { +public abstract class ElasticsearchModel extends Model { - public TextEmbeddingModel( + public ElasticsearchModel( String inferenceEntityId, TaskType taskType, String service, - TextEmbeddingInternalServiceSettings serviceSettings + ElasticsearchInternalServiceSettings serviceSettings ) { super(new ModelConfigurations(inferenceEntityId, taskType, service, serviceSettings)); } @Override - public TextEmbeddingInternalServiceSettings getServiceSettings() { - return (TextEmbeddingInternalServiceSettings) super.getServiceSettings(); + public ElasticsearchInternalServiceSettings getServiceSettings() { + return (ElasticsearchInternalServiceSettings) super.getServiceSettings(); } abstract StartTrainedModelDeploymentAction.Request getStartTrainedModelDeploymentActionRequest(); diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/MultilingualE5SmallInternalServiceSettings.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/MultilingualE5SmallInternalServiceSettings.java similarity index 93% rename from x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/MultilingualE5SmallInternalServiceSettings.java rename to x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/MultilingualE5SmallInternalServiceSettings.java index aa1de0e0beddc..5e93c1a46f796 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/textembedding/MultilingualE5SmallInternalServiceSettings.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/services/elasticsearch/MultilingualE5SmallInternalServiceSettings.java @@ -3,11 +3,9 @@ * 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. - * - * this file was contributed to by a generative AI */ -package org.elasticsearch.xpack.inference.services.textembedding; +package org.elasticsearch.xpack.inference.services.elasticsearch; import org.elasticsearch.TransportVersion; import org.elasticsearch.common.ValidationException; @@ -23,7 +21,7 @@ import static org.elasticsearch.TransportVersions.ML_TEXT_EMBEDDING_INFERENCE_SERVICE_ADDED; -public class MultilingualE5SmallInternalServiceSettings extends TextEmbeddingInternalServiceSettings { +public class MultilingualE5SmallInternalServiceSettings extends ElasticsearchInternalServiceSettings { public static final String NAME = "multilingual_e5_small_service_settings"; @@ -53,12 +51,12 @@ public static MultilingualE5SmallInternalServiceSettings.Builder fromMap(Map( - Map.of(TextEmbeddingInternalServiceSettings.NUM_ALLOCATIONS, 1, TextEmbeddingInternalServiceSettings.NUM_THREADS, 4) + Map.of(ElasticsearchInternalServiceSettings.NUM_ALLOCATIONS, 1, ElasticsearchInternalServiceSettings.NUM_THREADS, 4) ) ); @@ -79,12 +79,12 @@ public void testParseRequestConfig() { ModelConfigurations.SERVICE_SETTINGS, new HashMap<>( Map.of( - TextEmbeddingInternalServiceSettings.NUM_ALLOCATIONS, + ElasticsearchInternalServiceSettings.NUM_ALLOCATIONS, 1, - TextEmbeddingInternalServiceSettings.NUM_THREADS, + ElasticsearchInternalServiceSettings.NUM_THREADS, 4, InternalServiceSettings.MODEL_ID, - TextEmbeddingInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID + ElasticsearchInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID ) ) ); @@ -92,7 +92,7 @@ public void testParseRequestConfig() { var e5ServiceSettings = new MultilingualE5SmallInternalServiceSettings( 1, 4, - TextEmbeddingInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID + ElasticsearchInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID ); service.parseRequestConfig( @@ -111,7 +111,7 @@ public void testParseRequestConfig() { settings.put( ModelConfigurations.SERVICE_SETTINGS, new HashMap<>( - Map.of(TextEmbeddingInternalServiceSettings.NUM_ALLOCATIONS, 1, TextEmbeddingInternalServiceSettings.NUM_THREADS, 4) + Map.of(ElasticsearchInternalServiceSettings.NUM_ALLOCATIONS, 1, ElasticsearchInternalServiceSettings.NUM_THREADS, 4) ) ); settings.put("not_a_valid_config_setting", randomAlphaOfLength(10)); @@ -132,12 +132,12 @@ public void testParseRequestConfig() { ModelConfigurations.SERVICE_SETTINGS, new HashMap<>( Map.of( - TextEmbeddingInternalServiceSettings.NUM_ALLOCATIONS, + ElasticsearchInternalServiceSettings.NUM_ALLOCATIONS, 1, - TextEmbeddingInternalServiceSettings.NUM_THREADS, + ElasticsearchInternalServiceSettings.NUM_THREADS, 4, InternalServiceSettings.MODEL_ID, - TextEmbeddingInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID, // we can't directly test the eland case until we mock + ElasticsearchInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID, // we can't directly test the eland case until we mock // the threadpool within the client "not_a_valid_service_setting", randomAlphaOfLength(10) @@ -161,12 +161,12 @@ public void testParseRequestConfig() { ModelConfigurations.SERVICE_SETTINGS, new HashMap<>( Map.of( - TextEmbeddingInternalServiceSettings.NUM_ALLOCATIONS, + ElasticsearchInternalServiceSettings.NUM_ALLOCATIONS, 1, - TextEmbeddingInternalServiceSettings.NUM_THREADS, + ElasticsearchInternalServiceSettings.NUM_THREADS, 4, InternalServiceSettings.MODEL_ID, - TextEmbeddingInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID, // we can't directly test the eland case until we mock + ElasticsearchInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID, // we can't directly test the eland case until we mock // the threadpool within the client "extra_setting_that_should_not_be_here", randomAlphaOfLength(10) @@ -190,12 +190,12 @@ public void testParseRequestConfig() { ModelConfigurations.SERVICE_SETTINGS, new HashMap<>( Map.of( - TextEmbeddingInternalServiceSettings.NUM_ALLOCATIONS, + ElasticsearchInternalServiceSettings.NUM_ALLOCATIONS, 1, - TextEmbeddingInternalServiceSettings.NUM_THREADS, + ElasticsearchInternalServiceSettings.NUM_THREADS, 4, InternalServiceSettings.MODEL_ID, - TextEmbeddingInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID // we can't directly test the eland case until we mock + ElasticsearchInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID // we can't directly test the eland case until we mock // the threadpool within the client ) ) @@ -214,7 +214,7 @@ public void testParseRequestConfig() { private ActionListener getModelVerificationActionListener(MultilingualE5SmallInternalServiceSettings e5ServiceSettings) { return ActionListener.wrap(model -> { assertEquals( - new MultilingualE5SmallModel(randomInferenceEntityId, taskType, TextEmbeddingInternalService.NAME, e5ServiceSettings), + new MultilingualE5SmallModel(randomInferenceEntityId, taskType, ElasticsearchInternalService.NAME, e5ServiceSettings), model ); }, e -> { fail("Model parsing failed " + e.getMessage()); }); @@ -229,14 +229,14 @@ public void testParsePersistedConfig() { settings.put( ModelConfigurations.SERVICE_SETTINGS, new HashMap<>( - Map.of(TextEmbeddingInternalServiceSettings.NUM_ALLOCATIONS, 1, TextEmbeddingInternalServiceSettings.NUM_THREADS, 4) + Map.of(ElasticsearchInternalServiceSettings.NUM_ALLOCATIONS, 1, ElasticsearchInternalServiceSettings.NUM_THREADS, 4) ) ); var e5ServiceSettings = new MultilingualE5SmallInternalServiceSettings( 1, 4, - TextEmbeddingInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID + ElasticsearchInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID ); expectThrows(IllegalArgumentException.class, () -> service.parsePersistedConfig(randomInferenceEntityId, taskType, settings)); @@ -253,9 +253,9 @@ public void testParsePersistedConfig() { ModelConfigurations.SERVICE_SETTINGS, new HashMap<>( Map.of( - TextEmbeddingInternalServiceSettings.NUM_ALLOCATIONS, + ElasticsearchInternalServiceSettings.NUM_ALLOCATIONS, 1, - TextEmbeddingInternalServiceSettings.NUM_THREADS, + ElasticsearchInternalServiceSettings.NUM_THREADS, 4, InternalServiceSettings.MODEL_ID, "invalid" @@ -266,7 +266,7 @@ public void testParsePersistedConfig() { CustomElandModel parsedModel = (CustomElandModel) service.parsePersistedConfig(randomInferenceEntityId, taskType, settings); var elandServiceSettings = new CustomElandInternalServiceSettings(1, 4, "invalid"); assertEquals( - new CustomElandModel(randomInferenceEntityId, taskType, TextEmbeddingInternalService.NAME, elandServiceSettings), + new CustomElandModel(randomInferenceEntityId, taskType, ElasticsearchInternalService.NAME, elandServiceSettings), parsedModel ); } @@ -279,12 +279,12 @@ public void testParsePersistedConfig() { ModelConfigurations.SERVICE_SETTINGS, new HashMap<>( Map.of( - TextEmbeddingInternalServiceSettings.NUM_ALLOCATIONS, + ElasticsearchInternalServiceSettings.NUM_ALLOCATIONS, 1, - TextEmbeddingInternalServiceSettings.NUM_THREADS, + ElasticsearchInternalServiceSettings.NUM_THREADS, 4, InternalServiceSettings.MODEL_ID, - TextEmbeddingInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID + ElasticsearchInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID ) ) ); @@ -292,7 +292,7 @@ public void testParsePersistedConfig() { var e5ServiceSettings = new MultilingualE5SmallInternalServiceSettings( 1, 4, - TextEmbeddingInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID + ElasticsearchInternalService.MULTILINGUAL_E5_SMALL_MODEL_ID ); MultilingualE5SmallModel parsedModel = (MultilingualE5SmallModel) service.parsePersistedConfig( @@ -301,7 +301,7 @@ public void testParsePersistedConfig() { settings ); assertEquals( - new MultilingualE5SmallModel(randomInferenceEntityId, taskType, TextEmbeddingInternalService.NAME, e5ServiceSettings), + new MultilingualE5SmallModel(randomInferenceEntityId, taskType, ElasticsearchInternalService.NAME, e5ServiceSettings), parsedModel ); } @@ -313,7 +313,7 @@ public void testParsePersistedConfig() { settings.put( ModelConfigurations.SERVICE_SETTINGS, new HashMap<>( - Map.of(TextEmbeddingInternalServiceSettings.NUM_ALLOCATIONS, 1, TextEmbeddingInternalServiceSettings.NUM_THREADS, 4) + Map.of(ElasticsearchInternalServiceSettings.NUM_ALLOCATIONS, 1, ElasticsearchInternalServiceSettings.NUM_THREADS, 4) ) ); settings.put("not_a_valid_config_setting", randomAlphaOfLength(10)); @@ -328,9 +328,9 @@ public void testParsePersistedConfig() { ModelConfigurations.SERVICE_SETTINGS, new HashMap<>( Map.of( - TextEmbeddingInternalServiceSettings.NUM_ALLOCATIONS, + ElasticsearchInternalServiceSettings.NUM_ALLOCATIONS, 1, - TextEmbeddingInternalServiceSettings.NUM_THREADS, + ElasticsearchInternalServiceSettings.NUM_THREADS, 4, "not_a_valid_service_setting", randomAlphaOfLength(10) @@ -403,9 +403,9 @@ public void testChunkInfer() { assertTrue("Listener not called", gotResults.get()); } - private TextEmbeddingInternalService createService(Client client) { + private ElasticsearchInternalService createService(Client client) { var context = new InferenceServiceExtension.InferenceServiceFactoryContext(client); - return new TextEmbeddingInternalService(context); + return new ElasticsearchInternalService(context); } public static Model randomModelConfig(String inferenceEntityId) { @@ -417,7 +417,7 @@ public static Model randomModelConfig(String inferenceEntityId) { case "MultilingualE5SmallModel" -> new MultilingualE5SmallModel( inferenceEntityId, TaskType.TEXT_EMBEDDING, - TextEmbeddingInternalService.NAME, + ElasticsearchInternalService.NAME, MultilingualE5SmallInternalServiceSettingsTests.createRandom() ); default -> throw new IllegalArgumentException("model " + model + " is not supported for testing"); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/textembedding/MultilingualE5SmallInternalServiceSettingsTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/MultilingualE5SmallInternalServiceSettingsTests.java similarity index 95% rename from x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/textembedding/MultilingualE5SmallInternalServiceSettingsTests.java rename to x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/MultilingualE5SmallInternalServiceSettingsTests.java index 10e34a277eea3..fbff04efe6883 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/textembedding/MultilingualE5SmallInternalServiceSettingsTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/services/elasticsearch/MultilingualE5SmallInternalServiceSettingsTests.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.inference.services.textembedding; +package org.elasticsearch.xpack.inference.services.elasticsearch; import org.elasticsearch.common.ValidationException; import org.elasticsearch.common.io.stream.Writeable; @@ -24,7 +24,7 @@ public static MultilingualE5SmallInternalServiceSettings createRandom() { return new MultilingualE5SmallInternalServiceSettings( randomIntBetween(1, 4), randomIntBetween(1, 4), - randomFrom(TextEmbeddingInternalService.MULTILINGUAL_E5_SMALL_VALID_IDS) + randomFrom(ElasticsearchInternalService.MULTILINGUAL_E5_SMALL_VALID_IDS) ); } @@ -43,7 +43,7 @@ public void testFromMap_DefaultModelVersion() { } public void testFromMap() { - String randomModelVariant = randomFrom(TextEmbeddingInternalService.MULTILINGUAL_E5_SMALL_VALID_IDS); + String randomModelVariant = randomFrom(ElasticsearchInternalService.MULTILINGUAL_E5_SMALL_VALID_IDS); var serviceSettings = MultilingualE5SmallInternalServiceSettings.fromMap( new HashMap<>( Map.of( @@ -138,7 +138,7 @@ protected MultilingualE5SmallInternalServiceSettings mutateInstance(Multilingual instance.getModelId() ); case 2 -> { - var versions = new HashSet<>(TextEmbeddingInternalService.MULTILINGUAL_E5_SMALL_VALID_IDS); + var versions = new HashSet<>(ElasticsearchInternalService.MULTILINGUAL_E5_SMALL_VALID_IDS); versions.remove(instance.getModelId()); yield new MultilingualE5SmallInternalServiceSettings( instance.getNumAllocations(), From d50707203aedb6ce434475bc4fbf01a7631f0f50 Mon Sep 17 00:00:00 2001 From: Alexander Spies Date: Mon, 26 Feb 2024 12:38:02 +0100 Subject: [PATCH 187/250] ESQL: Fix wrong attribute shadowing in pushdown rules (#105650) Fix https://github.com/elastic/elasticsearch/issues/105434 Fixes accidental shadowing when pushing down `GROK`/`DISSECT`, `EVAL` or `ENRICH` past a `SORT`. Example for how this works: ``` ... | SORT x | EVAL x = y ... pushing this down just like that would be incorrect as x is used in the SORT, so we turn this essentially into ... | EVAL $$x = x | EVAL x = y | SORT $$x | DROP $$x ... ``` The same logic is applied to `GROK`/`DISSECT` and `ENRICH`. This allows to re-enable the dependency checker (after fixing a small bug in it when handling `ENRICH`). --- docs/changelog/105650.yaml | 6 + .../src/main/resources/dissect.csv-spec | 8 ++ .../resources/enrich-IT_tests_only.csv-spec | 12 +- .../src/main/resources/eval.csv-spec | 53 +++++++ .../src/main/resources/grok.csv-spec | 9 ++ .../esql/optimizer/LogicalPlanOptimizer.java | 134 ++++++++++++------ .../xpack/esql/optimizer/OptimizerRules.java | 6 +- .../optimizer/LogicalPlanOptimizerTests.java | 96 +++++++++++++ 8 files changed, 277 insertions(+), 47 deletions(-) create mode 100644 docs/changelog/105650.yaml diff --git a/docs/changelog/105650.yaml b/docs/changelog/105650.yaml new file mode 100644 index 0000000000000..f43da5b315f4c --- /dev/null +++ b/docs/changelog/105650.yaml @@ -0,0 +1,6 @@ +pr: 105650 +summary: "ESQL: Fix wrong attribute shadowing in pushdown rules" +area: ES|QL +type: bug +issues: + - 105434 diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/dissect.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/dissect.csv-spec index 1133b24cd1cf3..225ea37688689 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/dissect.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/dissect.csv-spec @@ -146,6 +146,14 @@ Bezalel Simmel | Bezalel | Simmel ; +overwriteNameAfterSort#[skip:-8.13.0] +from employees | sort emp_no ASC | dissect first_name "Ge%{emp_no}gi" | limit 1 | rename emp_no as first_name_fragment | keep first_name_fragment +; + +first_name_fragment:keyword +or +; + # for now it calculates only based on the first value multivalueInput from employees | where emp_no <= 10006 | dissect job_positions "%{a} %{b} %{c}" | sort emp_no | keep emp_no, a, b, c; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/enrich-IT_tests_only.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/enrich-IT_tests_only.csv-spec index e107fc2ffea63..2fa567996290d 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/enrich-IT_tests_only.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/enrich-IT_tests_only.csv-spec @@ -51,7 +51,6 @@ emp_no:integer | x:keyword | lang:keyword ; - withAliasSort from employees | eval x = to_string(languages) | keep emp_no, x | sort emp_no | limit 3 | enrich languages_policy on x with lang = language_name; @@ -63,6 +62,17 @@ emp_no:integer | x:keyword | lang:keyword ; +withAliasOverwriteName#[skip:-8.13.0] +from employees | sort emp_no +| eval x = to_string(languages) | enrich languages_policy on x with emp_no = language_name +| keep emp_no | limit 1 +; + +emp_no:keyword +French +; + + withAliasAndPlain from employees | sort emp_no desc | limit 3 | eval x = to_string(languages) | keep emp_no, x | enrich languages_policy on x with lang = language_name, language_name; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/eval.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/eval.csv-spec index 21ce5cf5c7fc2..a8e5a5930a06b 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/eval.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/eval.csv-spec @@ -368,4 +368,57 @@ avg_height_feet:double // end::evalUnnamedColumnStats-result[] ; +overwriteName#[skip:-8.13.0] +FROM employees +| SORT emp_no asc +| EVAL full_name = concat(first_name, " ", last_name) +| EVAL emp_no = concat(full_name, " ", to_string(emp_no)) +| KEEP full_name, emp_no +| LIMIT 3; + +full_name:keyword | emp_no:keyword +Georgi Facello | Georgi Facello 10001 +Bezalel Simmel | Bezalel Simmel 10002 +Parto Bamford | Parto Bamford 10003 +; + +overwriteNameWhere#[skip:-8.13.0] +FROM employees +| SORT emp_no ASC +| EVAL full_name = concat(first_name, " ", last_name) +| EVAL emp_no = concat(full_name, " ", to_string(emp_no)) +| WHERE emp_no == "Bezalel Simmel 10002" +| KEEP full_name, emp_no +| LIMIT 3; + +full_name:keyword | emp_no:keyword +Bezalel Simmel | Bezalel Simmel 10002 +; + +overwriteNameAfterSort#[skip:-8.13.0] +FROM employees +| SORT emp_no ASC +| EVAL emp_no = -emp_no +| LIMIT 3 +| KEEP emp_no +; + +emp_no:i +-10001 +-10002 +-10003 +; +overwriteNameAfterSortChained#[skip:-8.13.0] +FROM employees +| SORT emp_no ASC +| EVAL x = emp_no, y = -emp_no, emp_no = y +| LIMIT 3 +| KEEP emp_no +; + +emp_no:i +-10001 +-10002 +-10003 +; diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/grok.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/grok.csv-spec index f71f51d42c45f..fbe31deeb0f97 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/grok.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/grok.csv-spec @@ -122,6 +122,15 @@ Bezalel Simmel | Bezalel | Simmel ; +overwriteNameAfterSort#[skip:-8.13.0] +from employees | sort emp_no ASC | grok first_name "Ge(?[a-z]{2})gi" | limit 1 | rename emp_no as first_name_fragment | keep first_name_fragment +; + +first_name_fragment:keyword +or +; + + multivalueOutput row a = "foo bar" | grok a "%{WORD:b} %{WORD:b}"; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java index ab413bd89f0a6..db5751245c40a 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizer.java @@ -72,6 +72,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -259,7 +260,11 @@ protected LogicalPlan rule(Aggregate aggregate) { static String temporaryName(Expression inner, Expression outer, int suffix) { String in = toString(inner); String out = toString(outer); - return "$$" + in + "$" + out + "$" + suffix; + return rawTemporaryName(in, out, String.valueOf(suffix)); + } + + static String rawTemporaryName(String inner, String outer, String suffix) { + return "$$" + inner + "$" + outer + "$" + suffix; } static int TO_STRING_LIMIT = 16; @@ -839,9 +844,31 @@ private static LogicalPlan maybePushDownPastUnary(Filter filter, UnaryPlan unary } } + protected static class PushDownEval extends OptimizerRules.OptimizerRule { + @Override + protected LogicalPlan rule(Eval eval) { + return pushGeneratingPlanPastProjectAndOrderBy(eval, asAttributes(eval.fields())); + } + } + + protected static class PushDownRegexExtract extends OptimizerRules.OptimizerRule { + @Override + protected LogicalPlan rule(RegexExtract re) { + return pushGeneratingPlanPastProjectAndOrderBy(re, re.extractedFields()); + } + } + + protected static class PushDownEnrich extends OptimizerRules.OptimizerRule { + @Override + protected LogicalPlan rule(Enrich en) { + return pushGeneratingPlanPastProjectAndOrderBy(en, asAttributes(en.enrichFields())); + } + } + /** - * Pushes Evals past OrderBys. Although it seems arbitrary whether the OrderBy or the Eval is executed first, - * this transformation ensures that OrderBys only separated by an eval can be combined by PushDownAndCombineOrderBy. + * Pushes LogicalPlans which generate new attributes (Eval, Grok/Dissect, Enrich), past OrderBys and Projections. + * Although it seems arbitrary whether the OrderBy or the Eval is executed first, this transformation ensures that OrderBys only + * separated by an eval can be combined by PushDownAndCombineOrderBy. * * E.g.: * @@ -851,59 +878,82 @@ private static LogicalPlan maybePushDownPastUnary(Filter filter, UnaryPlan unary * * ... | eval x = b + 1 | sort a | sort x * - * Ordering the evals before the orderBys has the advantage that it's always possible to order the plans like this. + * Ordering the Evals before the OrderBys has the advantage that it's always possible to order the plans like this. * E.g., in the example above it would not be possible to put the eval after the two orderBys. + * + * In case one of the Eval's fields would shadow the orderBy's attributes, we rename the attribute first. + * + * E.g. + * + * ... | sort a | eval a = b + 1 | ... + * + * becomes + * + * ... | eval $$a = a | eval a = b + 1 | sort $$a | drop $$a */ - protected static class PushDownEval extends OptimizerRules.OptimizerRule { - @Override - protected LogicalPlan rule(Eval eval) { - LogicalPlan child = eval.child(); + private static LogicalPlan pushGeneratingPlanPastProjectAndOrderBy(UnaryPlan generatingPlan, List generatedAttributes) { + LogicalPlan child = generatingPlan.child(); - if (child instanceof OrderBy orderBy) { - return orderBy.replaceChild(eval.replaceChild(orderBy.child())); - } else if (child instanceof Project) { - var projectWithEvalChild = pushDownPastProject(eval); - var fieldProjections = asAttributes(eval.fields()); - return projectWithEvalChild.withProjections(mergeOutputExpressions(fieldProjections, projectWithEvalChild.projections())); - } + if (child instanceof OrderBy orderBy) { + Set evalFieldNames = new LinkedHashSet<>(Expressions.names(generatedAttributes)); - return eval; - } - } + // Look for attributes in the OrderBy's expressions and create aliases with temporary names for them. + AttributeReplacement nonShadowedOrders = renameAttributesInExpressions(evalFieldNames, orderBy.order()); - // same as for PushDownEval - protected static class PushDownRegexExtract extends OptimizerRules.OptimizerRule { - @Override - protected LogicalPlan rule(RegexExtract re) { - LogicalPlan child = re.child(); + AttributeMap aliasesForShadowedOrderByAttrs = nonShadowedOrders.replacedAttributes; + @SuppressWarnings("unchecked") + List newOrder = (List) (List) nonShadowedOrders.rewrittenExpressions; - if (child instanceof OrderBy orderBy) { - return orderBy.replaceChild(re.replaceChild(orderBy.child())); - } else if (child instanceof Project) { - var projectWithChild = pushDownPastProject(re); - return projectWithChild.withProjections(mergeOutputExpressions(re.extractedFields(), projectWithChild.projections())); + if (aliasesForShadowedOrderByAttrs.isEmpty() == false) { + List newAliases = new ArrayList<>(aliasesForShadowedOrderByAttrs.values()); + + LogicalPlan plan = new Eval(orderBy.source(), orderBy.child(), newAliases); + plan = generatingPlan.replaceChild(plan); + plan = new OrderBy(orderBy.source(), plan, newOrder); + plan = new Project(generatingPlan.source(), plan, generatingPlan.output()); + + return plan; } - return re; + return orderBy.replaceChild(generatingPlan.replaceChild(orderBy.child())); + } else if (child instanceof Project) { + var projectWithEvalChild = pushDownPastProject(generatingPlan); + return projectWithEvalChild.withProjections(mergeOutputExpressions(generatedAttributes, projectWithEvalChild.projections())); } + + return generatingPlan; } - // TODO double-check: this should be the same as EVAL and GROK/DISSECT, needed to avoid unbounded sort - protected static class PushDownEnrich extends OptimizerRules.OptimizerRule { - @Override - protected LogicalPlan rule(Enrich re) { - LogicalPlan child = re.child(); + private record AttributeReplacement(List rewrittenExpressions, AttributeMap replacedAttributes) {}; - if (child instanceof OrderBy orderBy) { - return orderBy.replaceChild(re.replaceChild(orderBy.child())); - } else if (child instanceof Project) { - var projectWithChild = pushDownPastProject(re); - var attrs = asAttributes(re.enrichFields()); - return projectWithChild.withProjections(mergeOutputExpressions(attrs, projectWithChild.projections())); - } + /** + * Replace attributes in the given expressions by assigning them temporary names. + * Returns the rewritten expressions and a map with an alias for each replaced attribute; the rewritten expressions reference + * these aliases. + */ + private static AttributeReplacement renameAttributesInExpressions( + Set attributeNamesToRename, + List expressions + ) { + AttributeMap aliasesForReplacedAttributes = new AttributeMap<>(); + List rewrittenExpressions = new ArrayList<>(); + + for (Expression expr : expressions) { + rewrittenExpressions.add(expr.transformUp(Attribute.class, attr -> { + if (attributeNamesToRename.contains(attr.name())) { + Alias renamedAttribute = aliasesForReplacedAttributes.computeIfAbsent(attr, a -> { + String tempName = SubstituteSurrogates.rawTemporaryName(a.name(), "temp_name", a.id().toString()); + // TODO: this should be synthetic + return new Alias(a.source(), tempName, null, a, null, false); + }); + return renamedAttribute.toAttribute(); + } - return re; + return attr; + })); } + + return new AttributeReplacement(rewrittenExpressions, aliasesForReplacedAttributes); } protected static class PushDownAndCombineOrderBy extends OptimizerRules.OptimizerRule { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/OptimizerRules.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/OptimizerRules.java index a1064b5b7d6bc..b9018f56e60de 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/OptimizerRules.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/OptimizerRules.java @@ -28,7 +28,7 @@ import org.elasticsearch.xpack.esql.plan.physical.RegexExtractExec; import org.elasticsearch.xpack.esql.plan.physical.RowExec; import org.elasticsearch.xpack.esql.plan.physical.ShowExec; -import org.elasticsearch.xpack.ql.common.Failure; +import org.elasticsearch.xpack.ql.common.Failures; import org.elasticsearch.xpack.ql.expression.AttributeSet; import org.elasticsearch.xpack.ql.expression.Expressions; import org.elasticsearch.xpack.ql.plan.QueryPlan; @@ -36,8 +36,6 @@ import org.elasticsearch.xpack.ql.plan.logical.EsRelation; import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; -import java.util.Collection; - import static org.elasticsearch.xpack.ql.common.Failure.fail; class OptimizerRules { @@ -46,7 +44,7 @@ private OptimizerRules() {} static class DependencyConsistency

    > { - void checkPlan(P p, Collection failures) { + void checkPlan(P p, Failures failures) { AttributeSet refs = references(p); AttributeSet input = p.inputSet(); AttributeSet generated = generates(p); 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 9dfcffbf48e6e..943d60a3882b7 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 @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.optimizer; +import org.elasticsearch.common.logging.LoggerMessageFormat; import org.elasticsearch.common.lucene.BytesRefs; import org.elasticsearch.compute.aggregation.QuantileStates; import org.elasticsearch.test.ESTestCase; @@ -42,6 +43,7 @@ import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Div; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Mod; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Mul; +import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Neg; import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Sub; import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.In; import org.elasticsearch.xpack.esql.parser.EsqlParser; @@ -57,6 +59,7 @@ import org.elasticsearch.xpack.esql.plan.logical.local.LocalSupplier; import org.elasticsearch.xpack.ql.expression.Alias; import org.elasticsearch.xpack.ql.expression.Attribute; +import org.elasticsearch.xpack.ql.expression.AttributeSet; import org.elasticsearch.xpack.ql.expression.Expression; import org.elasticsearch.xpack.ql.expression.Expressions; import org.elasticsearch.xpack.ql.expression.FieldAttribute; @@ -3266,6 +3269,99 @@ public void testPlanSanityCheck() throws Exception { assertThat(e.getMessage(), containsString(" optimized incorrectly due to missing references [salary")); } + /** + * Pushing down EVAL/GROK/DISSECT/ENRICH must not accidentally shadow attributes required by SORT. + * + * For DISSECT expects the following; the others are similar. + * + * EsqlProject[[first_name{f}#37, emp_no{r}#33, salary{r}#34]] + * \_TopN[[Order[$$emp_no$temp_name$36{r}#46 + $$salary$temp_name$41{r}#47 * 13[INTEGER],ASC,LAST], Order[NEG($$salary$t + * emp_name$41{r}#47),DESC,FIRST]],3[INTEGER]] + * \_Dissect[first_name{f}#37,Parser[pattern=%{emp_no} %{salary}, appendSeparator=, parser=org.elasticsearch.dissect.Dissect + * Parser@b6858b],[emp_no{r}#33, salary{r}#34]] + * \_Eval[[emp_no{f}#36 AS $$emp_no$temp_name$36, salary{f}#41 AS $$salary$temp_name$41]] + * \_EsRelation[test][_meta_field{f}#42, emp_no{f}#36, first_name{f}#37, ..] + */ + public void testPushdownWithOverwrittenName() { + List overwritingCommands = List.of( + "EVAL emp_no = 3*emp_no, salary = -2*emp_no-salary", + "DISSECT first_name \"%{emp_no} %{salary}\"", + "GROK first_name \"%{WORD:emp_no} %{WORD:salary}\"", + "ENRICH languages_idx ON first_name WITH emp_no = language_code, salary = language_code" + ); + + String queryTemplateKeepAfter = """ + FROM test + | SORT 13*(emp_no+salary) ASC, -salary DESC + | {} + | KEEP first_name, emp_no, salary + | LIMIT 3 + """; + // Equivalent but with KEEP first - ensures that attributes in the final projection are correct after pushdown rules were applied. + String queryTemplateKeepFirst = """ + FROM test + | KEEP emp_no, salary, first_name + | SORT 13*(emp_no+salary) ASC, -salary DESC + | {} + | LIMIT 3 + """; + + for (String overwritingCommand : overwritingCommands) { + String queryTemplate = randomBoolean() ? queryTemplateKeepFirst : queryTemplateKeepAfter; + var plan = optimizedPlan(LoggerMessageFormat.format(null, queryTemplate, overwritingCommand)); + + var project = as(plan, Project.class); + var projections = project.projections(); + assertThat(projections.size(), equalTo(3)); + assertThat(projections.get(0).name(), equalTo("first_name")); + assertThat(projections.get(1).name(), equalTo("emp_no")); + assertThat(projections.get(2).name(), equalTo("salary")); + + var topN = as(project.child(), TopN.class); + assertThat(topN.order().size(), is(2)); + + var firstOrderExpr = as(topN.order().get(0), Order.class); + var mul = as(firstOrderExpr.child(), Mul.class); + var add = as(mul.left(), Add.class); + var renamed_emp_no = as(add.left(), ReferenceAttribute.class); + var renamed_salary = as(add.right(), ReferenceAttribute.class); + assertThat(renamed_emp_no.toString(), startsWith("$$emp_no$temp_name")); + assertThat(renamed_salary.toString(), startsWith("$$salary$temp_name")); + + var secondOrderExpr = as(topN.order().get(1), Order.class); + var neg = as(secondOrderExpr.child(), Neg.class); + var renamed_salary2 = as(neg.field(), ReferenceAttribute.class); + assert (renamed_salary2.semanticEquals(renamed_salary) && renamed_salary2.equals(renamed_salary)); + + Eval renamingEval = null; + if (overwritingCommand.startsWith("EVAL")) { + // Multiple EVALs should be merged, so there's only one. + renamingEval = as(topN.child(), Eval.class); + } + if (overwritingCommand.startsWith("DISSECT")) { + var dissect = as(topN.child(), Dissect.class); + renamingEval = as(dissect.child(), Eval.class); + } + if (overwritingCommand.startsWith("GROK")) { + var grok = as(topN.child(), Grok.class); + renamingEval = as(grok.child(), Eval.class); + } + if (overwritingCommand.startsWith("ENRICH")) { + var enrich = as(topN.child(), Enrich.class); + renamingEval = as(enrich.child(), Eval.class); + } + + AttributeSet attributesCreatedInEval = new AttributeSet(); + for (Alias field : renamingEval.fields()) { + attributesCreatedInEval.add(field.toAttribute()); + } + assert (attributesCreatedInEval.contains(renamed_emp_no)); + assert (attributesCreatedInEval.contains(renamed_salary)); + + assertThat(renamingEval.child(), instanceOf(EsRelation.class)); + } + } + private LogicalPlan optimizedPlan(String query) { return plan(query); } From acfb500402e720d43aeb9c16862fbc8f0413e1c4 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Mon, 26 Feb 2024 16:59:57 +0100 Subject: [PATCH 188/250] Mute HeapAttackIT See https://github.com/elastic/elasticsearch/issues/105814 --- .../org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java b/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java index 8c87ef5977114..f636b02bb3fa0 100644 --- a/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java +++ b/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java @@ -10,6 +10,8 @@ import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.apache.http.util.EntityUtils; +import org.apache.lucene.tests.util.LuceneTestCase; +import org.apache.lucene.tests.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; @@ -57,6 +59,7 @@ * Tests that run ESQL queries that have, in the past, used so much memory they * crash Elasticsearch. */ +@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105814") public class HeapAttackIT extends ESRestTestCase { @ClassRule From 5d0296a2eebc81b2da0305ef505a23babdd54d42 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Mon, 26 Feb 2024 17:09:24 +0100 Subject: [PATCH 189/250] Add reference docs links when jna fails to load (#105812) closes #105147 --- .../java/org/elasticsearch/bootstrap/Natives.java | 11 ++++++++++- .../java/org/elasticsearch/common/ReferenceDocs.java | 1 + .../elasticsearch/common/reference-docs-links.json | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/bootstrap/Natives.java b/server/src/main/java/org/elasticsearch/bootstrap/Natives.java index 2404d5075f844..4fa670b28872b 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/Natives.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/Natives.java @@ -10,8 +10,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.ReferenceDocs; import java.nio.file.Path; +import java.util.Locale; /** * The Natives class is a wrapper class that checks if the classes necessary for calling native methods are available on @@ -36,7 +38,14 @@ private Natives() {} } catch (ClassNotFoundException e) { logger.warn("JNA not found. native methods will be disabled.", e); } catch (UnsatisfiedLinkError e) { - logger.warn("unable to load JNA native support library, native methods will be disabled.", e); + logger.warn( + String.format( + Locale.ROOT, + "unable to load JNA native support library, native methods will be disabled. See %s", + ReferenceDocs.EXECUTABLE_JNA_TMPDIR + ), + e + ); } JNA_AVAILABLE = v; } diff --git a/server/src/main/java/org/elasticsearch/common/ReferenceDocs.java b/server/src/main/java/org/elasticsearch/common/ReferenceDocs.java index 4c401ab0ad52c..8e370158d166a 100644 --- a/server/src/main/java/org/elasticsearch/common/ReferenceDocs.java +++ b/server/src/main/java/org/elasticsearch/common/ReferenceDocs.java @@ -71,6 +71,7 @@ public enum ReferenceDocs { BOOTSTRAP_CHECK_SECURITY_MINIMAL_SETUP, CONTACT_SUPPORT, UNASSIGNED_SHARDS, + EXECUTABLE_JNA_TMPDIR, // this comment keeps the ';' on the next line so every entry above has a trailing ',' which makes the diff for adding new links cleaner ; diff --git a/server/src/main/resources/org/elasticsearch/common/reference-docs-links.json b/server/src/main/resources/org/elasticsearch/common/reference-docs-links.json index fef69aec2f543..ead7387b0e1ac 100644 --- a/server/src/main/resources/org/elasticsearch/common/reference-docs-links.json +++ b/server/src/main/resources/org/elasticsearch/common/reference-docs-links.json @@ -31,5 +31,6 @@ "BOOTSTRAP_CHECK_TOKEN_SSL": "bootstrap-checks-xpack.html#_token_ssl_check", "BOOTSTRAP_CHECK_SECURITY_MINIMAL_SETUP": "security-minimal-setup.html", "CONTACT_SUPPORT": "troubleshooting.html#troubleshooting-contact-support", - "UNASSIGNED_SHARDS": "red-yellow-cluster-status.html" + "UNASSIGNED_SHARDS": "red-yellow-cluster-status.html", + "EXECUTABLE_JNA_TMPDIR": "executable-jna-tmpdir.html" } From b4b32aa53a53975d1540dfc37c985729d622c6b6 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Mon, 26 Feb 2024 17:17:07 +0100 Subject: [PATCH 190/250] Fix spotless - silly imports --- .../org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java | 1 - 1 file changed, 1 deletion(-) diff --git a/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java b/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java index f636b02bb3fa0..bb3617b178a51 100644 --- a/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java +++ b/test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java @@ -10,7 +10,6 @@ import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.apache.http.util.EntityUtils; -import org.apache.lucene.tests.util.LuceneTestCase; import org.apache.lucene.tests.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.client.Request; import org.elasticsearch.client.RequestOptions; From 8df3a303260979e28a67b6cfc41b4f419301f2fe Mon Sep 17 00:00:00 2001 From: Jonathan Wilson Date: Tue, 27 Feb 2024 02:12:59 -0700 Subject: [PATCH 191/250] adding field data test (#105523) Co-authored-by: jonathan wilson Co-authored-by: Elastic Machine --- .../ExampleRescoreBuilderFieldDataTests.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 plugins/examples/rescore/src/test/java/org/elasticsearch/example/rescore/ExampleRescoreBuilderFieldDataTests.java diff --git a/plugins/examples/rescore/src/test/java/org/elasticsearch/example/rescore/ExampleRescoreBuilderFieldDataTests.java b/plugins/examples/rescore/src/test/java/org/elasticsearch/example/rescore/ExampleRescoreBuilderFieldDataTests.java new file mode 100644 index 0000000000000..b486a6730a9ad --- /dev/null +++ b/plugins/examples/rescore/src/test/java/org/elasticsearch/example/rescore/ExampleRescoreBuilderFieldDataTests.java @@ -0,0 +1,111 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.example.rescore; + +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.FloatField; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.TotalHits; +import org.apache.lucene.store.Directory; +import org.apache.lucene.tests.index.RandomIndexWriter; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.search.rescore.RescoreContext; +import org.elasticsearch.test.AbstractBuilderTestCase; + +import java.io.IOException; + +public class ExampleRescoreBuilderFieldDataTests extends AbstractBuilderTestCase { + + //to test that the rescore plugin is able to pull data from the indexed documents + //these following helper methods are called from the test below, + //some helpful examples related to this are located circa feb 14 2024 at: + //https://github.com/apache/lucene/blob/main/lucene/core/src/test/org/apache/lucene/search/TestQueryRescorer.java + + private String fieldFactorFieldName = "literalNameOfFieldUsedAsFactor"; + private float fieldFactorValue = 2.0f; + + private IndexSearcher getSearcher(IndexReader r) { + IndexSearcher searcher = newSearcher(r); + return searcher; + } + + private IndexReader publishDocs(int numDocs, String fieldName, Directory dir) throws Exception { + //here we populate a collection of documents into the mock search context + //note they all have the same field factor value for convenience + RandomIndexWriter w = new RandomIndexWriter(random(), dir, newIndexWriterConfig()); + for (int i = 0; i < numDocs; i++) { + Document d = new Document(); + d.add(newStringField("id", Integer.toString(i), Field.Store.YES)); + d.add(new FloatField(fieldName, fieldFactorValue, Field.Store.YES )); + w.addDocument(d); + } + IndexReader reader = w.getReader(); + w.close(); + return reader; + } + @Override + protected void initializeAdditionalMappings(MapperService mapperService) throws IOException { + + mapperService.merge( + "_doc", + new CompressedXContent(Strings.toString(PutMappingRequest.simpleMapping(fieldFactorFieldName, "type=float"))), + MapperService.MergeReason.MAPPING_UPDATE + ); + } + + + public void testRescoreUsingFieldData() throws Exception { + //we want the originalScoreOfTopDocs to be lower than the rescored values + //so that the order of the result has moved the rescored window to the top of the results + float originalScoreOfTopDocs = 1.0f; + + //just like in the associated rescore builder factor testing + //we will test a random factor on the incoming score docs + //the division is just to leave room for whatever values are picked + float factor = (float) randomDoubleBetween(1.0d, Float.MAX_VALUE/(fieldFactorValue * originalScoreOfTopDocs)-1, false); + + // Testing factorField specifically here for more example rescore debugging + // setup a mock search context that will be able to locate fieldIndexData + // provided from the index reader that follows + + Directory dir = newDirectory(); + //the rest of this test does not actually need more than 3 docs in the mock + //however any number >= 3 is fine + int numDocs = 3; + IndexReader reader = publishDocs(numDocs, fieldFactorFieldName, dir); + IndexSearcher searcher = getSearcher(reader); + + ExampleRescoreBuilder builder = new ExampleRescoreBuilder(factor, fieldFactorFieldName).windowSize(2); + + RescoreContext context = builder.buildContext(createSearchExecutionContext(searcher)); + + //create and populate the TopDocs that will be provided to the rescore function + TopDocs docs = new TopDocs(new TotalHits(10, TotalHits.Relation.EQUAL_TO), new ScoreDoc[3]); + docs.scoreDocs[0] = new ScoreDoc(0, originalScoreOfTopDocs); + docs.scoreDocs[1] = new ScoreDoc(1, originalScoreOfTopDocs); + docs.scoreDocs[2] = new ScoreDoc(2, originalScoreOfTopDocs); + context.rescorer().rescore(docs, searcher, context); + + //here we expect that windowSize docs have been re-scored, with remaining doc in the original state + assertEquals(originalScoreOfTopDocs*factor*fieldFactorValue, docs.scoreDocs[0].score, 0.0f); + assertEquals(originalScoreOfTopDocs*factor*fieldFactorValue, docs.scoreDocs[1].score, 0.0f); + assertEquals(originalScoreOfTopDocs, docs.scoreDocs[2].score, 0.0f); + + //just to clean up the mocks + reader.close(); + dir.close(); + } +} From e390edbdf828c3ab856d13f85f764d6435af0960 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 27 Feb 2024 06:53:00 -0500 Subject: [PATCH 192/250] ESQL: Add timers to many status results (#105421) This adds many "timers" to the profile and status results. These look like: ``` "profile" : { "drivers" : [ { "took_nanos" : 57252128, "took_time" : "57.2ms", "cpu_nanos" : 56733751, "cpu_time" : "56.7ms", "operators" : [ ... "operator" : "LuceneSourceOperator[maxPageSize=9362, remainingDocs=2147383647]", ... "processing_nanos" : 856914, "processing_time" : "856.9micros", ... "operator" : "ValuesSourceReaderOperator[fields = [a, b]]", ... "process_nanos" : 9671406, "process_time" : "9.6ms", ... "operator" : "EvalOperator[evaluator=AddLongsEvaluator[lhs=Attribute[channel=1], rhs=Attribute[channel=2]]]", ... "process_nanos" : 3344354, "process_time" : "3.3ms", ... "operator" : "HashAggregationOperator[blockHash=LongBlockHash{channel=1, entries=100, seenNull=false}, aggregators=[GroupingAggregator[aggregatorFunction=MaxLongGroupingAggregatorFunction[channels=[3]], mode=INITIAL], GroupingAggregator[aggregatorFunction=MinLongGroupingAggregatorFunction[channels=[3]], mode=INITIAL]]]", ... "hash_nanos" : 11845071, "hash_time" : "11.8ms", "aggregation_nanos" : 27910054, "aggregation_time" : "27.9ms", ... ``` This let's us compare the runtime of each operator in a query. You can get the run times of a running ESQL task with the `_tasks` API or you can use the `profile` API to run an ESQL query and get these times in the result. The example above is from `profile`. In the example above we can see we're spending 500 microseconds waiting in the queue. Of the remaining 56ms a little under half of that is spent running the aggregations - in this case a `MAX` and `MIN`. And 20% of the time is hashing to build group keys. Similarly 20% is spent on field loading. Co-authored-by: Alexander Spies --- docs/changelog/105421.yaml | 5 + .../org/elasticsearch/TransportVersions.java | 1 + .../test/rest/yaml/section/VersionRange.java | 5 + .../compute/lucene/LuceneCountOperator.java | 3 + .../compute/lucene/LuceneOperator.java | 25 +++ .../compute/lucene/LuceneSourceOperator.java | 3 + .../lucene/LuceneTopNSourceOperator.java | 13 +- .../lucene/ValuesSourceReaderOperator.java | 17 +- .../operator/AbstractPageMappingOperator.java | 53 +++++- .../compute/operator/AggregationOperator.java | 118 ++++++++++++ .../compute/operator/Driver.java | 124 ++++++++---- .../compute/operator/DriverProfile.java | 91 ++++++++- .../compute/operator/DriverStatus.java | 73 +++++++- .../operator/HashAggregationOperator.java | 177 +++++++++++++++++- .../compute/operator/LimitOperator.java | 7 + .../compute/operator/MvExpandOperator.java | 7 + .../compute/operator/Operator.java | 4 +- .../exchange/ExchangeSinkOperator.java | 7 + .../exchange/ExchangeSourceOperator.java | 7 + .../operator/topn/TopNOperatorStatus.java | 7 + .../LuceneSourceOperatorStatusTests.java | 23 ++- ...ValuesSourceReaderOperatorStatusTests.java | 29 ++- ...bstractPageMappingOperatorStatusTests.java | 21 ++- .../AggregationOperatorStatusTests.java | 56 ++++++ .../compute/operator/DriverProfileTests.java | 30 ++- .../compute/operator/DriverStatusTests.java | 51 +++-- .../compute/operator/DriverTests.java | 167 +++++++++++++++++ .../HashAggregationOperatorStatusTests.java | 60 ++++++ .../compute/operator/OperatorTestCase.java | 2 + .../exchange/ExchangeServiceTests.java | 4 + .../esql/qa/multi_node/EsqlClientYamlIT.java | 2 +- .../esql/enrich/EnrichLookupService.java | 2 + .../esql/planner/LocalExecutionPlanner.java | 2 + .../xpack/esql/plugin/ComputeService.java | 2 +- .../xpack/esql/plugin/EsqlPlugin.java | 4 + .../action/EsqlQueryResponseProfileTests.java | 11 +- .../esql/action/EsqlQueryResponseTests.java | 47 ++++- .../rest-api-spec/test/esql/120_profile.yml | 143 ++++++++++++++ 38 files changed, 1270 insertions(+), 133 deletions(-) create mode 100644 docs/changelog/105421.yaml create mode 100644 x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/AggregationOperatorStatusTests.java create mode 100644 x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/HashAggregationOperatorStatusTests.java create mode 100644 x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/120_profile.yml diff --git a/docs/changelog/105421.yaml b/docs/changelog/105421.yaml new file mode 100644 index 0000000000000..2ff9ef008c803 --- /dev/null +++ b/docs/changelog/105421.yaml @@ -0,0 +1,5 @@ +pr: 105421 +summary: "ESQL: Add timers to many status results" +area: ES|QL +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/TransportVersions.java b/server/src/main/java/org/elasticsearch/TransportVersions.java index c88b56ba25022..055fcb6d9cf7b 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersions.java +++ b/server/src/main/java/org/elasticsearch/TransportVersions.java @@ -134,6 +134,7 @@ static TransportVersion def(int id) { public static final TransportVersion INGEST_GRAPH_STRUCTURE_EXCEPTION = def(8_594_00_0); public static final TransportVersion ML_MODEL_IN_SERVICE_SETTINGS = def(8_595_00_0); public static final TransportVersion RANDOM_AGG_SHARD_SEED = def(8_596_00_0); + public static final TransportVersion ESQL_TIMINGS = def(8_597_00_0); /* * STOP! READ THIS FIRST! No, really, diff --git a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/VersionRange.java b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/VersionRange.java index df4eba050dc27..20b9708c5ac25 100644 --- a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/VersionRange.java +++ b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/VersionRange.java @@ -57,6 +57,11 @@ public boolean test(Set nodesVersions) { .orElseThrow(() -> new IllegalArgumentException("Checks against a version range require semantic version format (x.y.z)")); return minimumNodeVersion.onOrAfter(lower) && minimumNodeVersion.onOrBefore(upper); } + + @Override + public String toString() { + return "MinimumContainedInVersionRange{lower=" + lower + ", upper=" + upper + '}'; + } } static List>> parseVersionRanges(String rawRanges) { diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneCountOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneCountOperator.java index 4dda5c16295fb..d05593015211b 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneCountOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneCountOperator.java @@ -123,6 +123,7 @@ public Page getOutput() { assert remainingDocs <= 0 : remainingDocs; return null; } + long start = System.nanoTime(); try { final LuceneScorer scorer = getCurrentOrLoadNextScorer(); // no scorer means no more docs @@ -171,6 +172,8 @@ public Page getOutput() { return page; } catch (IOException e) { throw new UncheckedIOException(e); + } finally { + processingNanos += System.nanoTime() - start; } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneOperator.java index 1eeedd06d058d..d43eb8c280695 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneOperator.java @@ -16,6 +16,7 @@ import org.apache.lucene.search.ScoreMode; import org.apache.lucene.search.Weight; import org.apache.lucene.util.Bits; +import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; @@ -24,6 +25,7 @@ import org.elasticsearch.compute.data.BlockFactory; import org.elasticsearch.compute.operator.Operator; import org.elasticsearch.compute.operator.SourceOperator; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; import org.elasticsearch.xcontent.XContentBuilder; @@ -60,6 +62,7 @@ public abstract class LuceneOperator extends SourceOperator { private LuceneScorer currentScorer; + long processingNanos; int pagesEmitted; boolean doneCollecting; @@ -198,6 +201,7 @@ public static class Status implements Operator.Status { private final int processedSlices; private final Set processedQueries; private final Set processedShards; + private final long processingNanos; private final int totalSlices; private final int pagesEmitted; private final int sliceIndex; @@ -208,6 +212,7 @@ public static class Status implements Operator.Status { private Status(LuceneOperator operator) { processedSlices = operator.processedSlices; processedQueries = operator.processedQueries.stream().map(Query::toString).collect(Collectors.toCollection(TreeSet::new)); + processingNanos = operator.processingNanos; processedShards = new TreeSet<>(operator.processedShards); sliceIndex = operator.sliceIndex; totalSlices = operator.sliceQueue.totalSlices(); @@ -233,6 +238,7 @@ private Status(LuceneOperator operator) { int processedSlices, Set processedQueries, Set processedShards, + long processingNanos, int sliceIndex, int totalSlices, int pagesEmitted, @@ -243,6 +249,7 @@ private Status(LuceneOperator operator) { this.processedSlices = processedSlices; this.processedQueries = processedQueries; this.processedShards = processedShards; + this.processingNanos = processingNanos; this.sliceIndex = sliceIndex; this.totalSlices = totalSlices; this.pagesEmitted = pagesEmitted; @@ -260,6 +267,7 @@ private Status(LuceneOperator operator) { processedQueries = Collections.emptySet(); processedShards = Collections.emptySet(); } + processingNanos = in.getTransportVersion().onOrAfter(TransportVersions.ESQL_TIMINGS) ? in.readVLong() : 0; sliceIndex = in.readVInt(); totalSlices = in.readVInt(); pagesEmitted = in.readVInt(); @@ -275,6 +283,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeCollection(processedQueries, StreamOutput::writeString); out.writeCollection(processedShards, StreamOutput::writeString); } + if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_TIMINGS)) { + out.writeVLong(processingNanos); + } out.writeVInt(sliceIndex); out.writeVInt(totalSlices); out.writeVInt(pagesEmitted); @@ -300,6 +311,10 @@ public Set processedShards() { return processedShards; } + public long processNanos() { + return processingNanos; + } + public int sliceIndex() { return sliceIndex; } @@ -330,6 +345,10 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field("processed_slices", processedSlices); builder.field("processed_queries", processedQueries); builder.field("processed_shards", processedShards); + builder.field("processing_nanos", processingNanos); + if (builder.humanReadable()) { + builder.field("processing_time", TimeValue.timeValueNanos(processingNanos)); + } builder.field("slice_index", sliceIndex); builder.field("total_slices", totalSlices); builder.field("pages_emitted", pagesEmitted); @@ -347,6 +366,7 @@ public boolean equals(Object o) { return processedSlices == status.processedSlices && processedQueries.equals(status.processedQueries) && processedShards.equals(status.processedShards) + && processingNanos == status.processingNanos && sliceIndex == status.sliceIndex && totalSlices == status.totalSlices && pagesEmitted == status.pagesEmitted @@ -364,6 +384,11 @@ public int hashCode() { public String toString() { return Strings.toString(this); } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.V_8_11_X; + } } static Function weightFunction(Function queryFunction, ScoreMode scoreMode) { diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneSourceOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneSourceOperator.java index 9d6e3f46d0e1e..f2ab362278c4c 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneSourceOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneSourceOperator.java @@ -128,6 +128,7 @@ public Page getOutput() { assert currentPagePos == 0 : currentPagePos; return null; } + long start = System.nanoTime(); try { final LuceneScorer scorer = getCurrentOrLoadNextScorer(); if (scorer == null) { @@ -163,6 +164,8 @@ public Page getOutput() { return page; } catch (IOException e) { throw new UncheckedIOException(e); + } finally { + processingNanos += System.nanoTime() - start; } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneTopNSourceOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneTopNSourceOperator.java index 8cb9173adc197..df95e49ab2492 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneTopNSourceOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/LuceneTopNSourceOperator.java @@ -140,10 +140,15 @@ public Page getOutput() { if (isFinished()) { return null; } - if (isEmitting()) { - return emit(false); - } else { - return collect(); + long start = System.nanoTime(); + try { + if (isEmitting()) { + return emit(false); + } else { + return collect(); + } + } finally { + processingNanos += System.nanoTime() - start; } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperator.java index b9be899cec4f3..08be21f95786f 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperator.java @@ -475,8 +475,8 @@ public String toString() { } @Override - protected Status status(int pagesProcessed) { - return new Status(new TreeMap<>(readersBuilt), pagesProcessed); + protected Status status(long processNanos, int pagesProcessed) { + return new Status(new TreeMap<>(readersBuilt), processNanos, pagesProcessed); } public static class Status extends AbstractPageMappingOperator.Status { @@ -488,8 +488,8 @@ public static class Status extends AbstractPageMappingOperator.Status { private final Map readersBuilt; - Status(Map readersBuilt, int pagesProcessed) { - super(pagesProcessed); + Status(Map readersBuilt, long processNanos, int pagesProcessed) { + super(processNanos, pagesProcessed); this.readersBuilt = readersBuilt; } @@ -521,21 +521,20 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.field(e.getKey(), e.getValue()); } builder.endObject(); - builder.field("pages_processed", pagesProcessed()); + innerToXContent(builder); return builder.endObject(); } @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (super.equals(o) == false) return false; Status status = (Status) o; - return pagesProcessed() == status.pagesProcessed() && readersBuilt.equals(status.readersBuilt); + return readersBuilt.equals(status.readersBuilt); } @Override public int hashCode() { - return Objects.hash(readersBuilt, pagesProcessed()); + return Objects.hash(super.hashCode(), readersBuilt); } @Override diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/AbstractPageMappingOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/AbstractPageMappingOperator.java index 5924e4086c743..800b648711f26 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/AbstractPageMappingOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/AbstractPageMappingOperator.java @@ -7,12 +7,15 @@ package org.elasticsearch.compute.operator; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.compute.data.Page; import org.elasticsearch.core.Releasables; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.xcontent.XContentBuilder; import java.io.IOException; @@ -25,6 +28,11 @@ public abstract class AbstractPageMappingOperator implements Operator { private Page prev; private boolean finished = false; + /** + * Number of milliseconds this operation has run. + */ + private long processNanos; + /** * Count of pages that have been processed by this operator. */ @@ -64,19 +72,21 @@ public final Page getOutput() { if (prev.getPositionCount() == 0) { return prev; } - pagesProcessed++; + long start = System.nanoTime(); Page p = process(prev); + pagesProcessed++; + processNanos += System.nanoTime() - start; prev = null; return p; } @Override public final Status status() { - return status(pagesProcessed); + return status(processNanos, pagesProcessed); } - protected Status status(int pagesProcessed) { - return new Status(pagesProcessed); + protected Status status(long processNanos, int pagesProcessed) { + return new Status(processNanos, pagesProcessed); } @Override @@ -93,18 +103,24 @@ public static class Status implements Operator.Status { Status::new ); + private final long processNanos; private final int pagesProcessed; - public Status(int pagesProcessed) { + public Status(long processNanos, int pagesProcessed) { + this.processNanos = processNanos; this.pagesProcessed = pagesProcessed; } protected Status(StreamInput in) throws IOException { + processNanos = in.getTransportVersion().onOrAfter(TransportVersions.ESQL_TIMINGS) ? in.readVLong() : 0; pagesProcessed = in.readVInt(); } @Override public void writeTo(StreamOutput out) throws IOException { + if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_TIMINGS)) { + out.writeVLong(processNanos); + } out.writeVInt(pagesProcessed); } @@ -117,29 +133,50 @@ public int pagesProcessed() { return pagesProcessed; } + public long processNanos() { + return processNanos; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field("pages_processed", pagesProcessed); + innerToXContent(builder); return builder.endObject(); } + /** + * Render the body of the object for this status. Protected so subclasses + * can call it to render the "default" body. + */ + protected final XContentBuilder innerToXContent(XContentBuilder builder) throws IOException { + builder.field("process_nanos", processNanos); + if (builder.humanReadable()) { + builder.field("process_time", TimeValue.timeValueNanos(processNanos)); + } + return builder.field("pages_processed", pagesProcessed); + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Status status = (Status) o; - return pagesProcessed == status.pagesProcessed; + return processNanos == status.processNanos && pagesProcessed == status.pagesProcessed; } @Override public int hashCode() { - return Objects.hash(pagesProcessed); + return Objects.hash(processNanos, pagesProcessed); } @Override public String toString() { return Strings.toString(this); } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.V_8_11_X; + } } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/AggregationOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/AggregationOperator.java index 07d1809262c9b..20d3f0166f1cb 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/AggregationOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/AggregationOperator.java @@ -7,13 +7,22 @@ package org.elasticsearch.compute.operator; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.compute.aggregation.Aggregator; import org.elasticsearch.compute.aggregation.Aggregator.Factory; import org.elasticsearch.compute.aggregation.AggregatorMode; import org.elasticsearch.compute.data.Block; import org.elasticsearch.compute.data.Page; import org.elasticsearch.core.Releasables; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.xcontent.XContentBuilder; +import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -36,6 +45,15 @@ public class AggregationOperator implements Operator { private final List aggregators; private final DriverContext driverContext; + /** + * Nanoseconds this operator has spent running the aggregations. + */ + private long aggregationNanos; + /** + * Count of pages this operator has processed. + */ + private int pagesProcessed; + public record AggregationOperatorFactory(List aggregators, AggregatorMode mode) implements OperatorFactory { @Override @@ -72,6 +90,7 @@ public boolean needsInput() { @Override public void addInput(Page page) { + long start = System.nanoTime(); checkState(needsInput(), "Operator is already finishing"); requireNonNull(page, "page is null"); try { @@ -80,6 +99,8 @@ public void addInput(Page page) { } } finally { page.releaseBlocks(); + aggregationNanos += System.nanoTime() - start; + pagesProcessed++; } } @@ -150,4 +171,101 @@ public String toString() { sb.append("aggregators=").append(aggregators).append("]"); return sb.toString(); } + + @Override + public Operator.Status status() { + return new Status(aggregationNanos, pagesProcessed); + } + + public static class Status implements Operator.Status { + public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( + Operator.Status.class, + "agg", + Status::new + ); + + /** + * Nanoseconds this operator has spent running the aggregations. + */ + private final long aggregationNanos; + /** + * Count of pages this operator has processed. + */ + private final int pagesProcessed; + + /** + * Build. + * @param aggregationNanos Nanoseconds this operator has spent running the aggregations. + * @param pagesProcessed Count of pages this operator has processed. + */ + public Status(long aggregationNanos, int pagesProcessed) { + this.aggregationNanos = aggregationNanos; + this.pagesProcessed = pagesProcessed; + } + + protected Status(StreamInput in) throws IOException { + aggregationNanos = in.readVLong(); + pagesProcessed = in.readVInt(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVLong(aggregationNanos); + out.writeVInt(pagesProcessed); + } + + @Override + public String getWriteableName() { + return ENTRY.name; + } + + /** + * Nanoseconds this operator has spent running the aggregations. + */ + public long aggregationNanos() { + return aggregationNanos; + } + + /** + * Count of pages this operator has processed. + */ + public int pagesProcessed() { + return pagesProcessed; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("aggregation_nanos", aggregationNanos); + if (builder.humanReadable()) { + builder.field("aggregation_time", TimeValue.timeValueNanos(aggregationNanos)); + } + builder.field("pages_processed", pagesProcessed); + return builder.endObject(); + + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Status status = (Status) o; + return aggregationNanos == status.aggregationNanos && pagesProcessed == status.pagesProcessed; + } + + @Override + public int hashCode() { + return Objects.hash(aggregationNanos, pagesProcessed); + } + + @Override + public String toString() { + return Strings.toString(this); + } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.ESQL_TIMINGS; + } + } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/Driver.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/Driver.java index 3e9793ef87b2a..2537809fbd8ec 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/Driver.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/Driver.java @@ -26,6 +26,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.LongSupplier; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -49,6 +50,22 @@ public class Driver implements Releasable, Describable { public static final TimeValue DEFAULT_STATUS_INTERVAL = TimeValue.timeValueSeconds(1); private final String sessionId; + + /** + * The wall clock time when this driver was created in milliseconds since epoch. + * Compared to {@link #startNanos} this is less accurate and is measured by a + * timer that can go backwards. This is only useful for presenting times to a + * user, like over the status API. + */ + private final long startTime; + + /** + * The time when this driver was created in nanos. This time is relative to + * some arbitrary point - imagine its program startup. The timer that generates + * this is monotonically increasing so even if NTP or something changes the + * clock it won't change. As such, this is only useful for measuring durations. + */ + private final long startNanos; private final DriverContext driverContext; private final Supplier description; private final List activeOperators; @@ -69,6 +86,13 @@ public class Driver implements Releasable, Describable { */ private final AtomicReference status; + /** + * The time this driver finished. Only set once the driver is finished, defaults to 0 + * which is *possibly* a valid value, so always use the driver status to check + * if the driver is actually finished. + */ + private long finishNanos; + /** * Creates a new driver with a chain of operators. * @param sessionId session Id @@ -81,6 +105,8 @@ public class Driver implements Releasable, Describable { */ public Driver( String sessionId, + long startTime, + long startNanos, DriverContext driverContext, Supplier description, SourceOperator source, @@ -90,6 +116,8 @@ public Driver( Releasable releasable ) { this.sessionId = sessionId; + this.startTime = startTime; + this.startNanos = startNanos; this.driverContext = driverContext; this.description = description; this.activeOperators = new ArrayList<>(); @@ -99,7 +127,7 @@ public Driver( this.statusNanos = statusInterval.nanos(); this.releasable = releasable; this.status = new AtomicReference<>( - new DriverStatus(sessionId, System.currentTimeMillis(), DriverStatus.Status.QUEUED, List.of(), List.of()) + new DriverStatus(sessionId, startTime, System.currentTimeMillis(), 0, 0, DriverStatus.Status.QUEUED, List.of(), List.of()) ); } @@ -118,7 +146,18 @@ public Driver( SinkOperator sink, Releasable releasable ) { - this("unset", driverContext, () -> null, source, intermediateOperators, sink, DEFAULT_STATUS_INTERVAL, releasable); + this( + "unset", + System.currentTimeMillis(), + System.nanoTime(), + driverContext, + () -> null, + source, + intermediateOperators, + sink, + DEFAULT_STATUS_INTERVAL, + releasable + ); } public DriverContext driverContext() { @@ -130,38 +169,39 @@ public DriverContext driverContext() { * Returns a blocked future when the chain of operators is blocked, allowing the caller * thread to do other work instead of blocking or busy-spinning on the blocked operator. */ - private SubscribableListener run(TimeValue maxTime, int maxIterations) { + SubscribableListener run(TimeValue maxTime, int maxIterations, LongSupplier nowSupplier) { long maxTimeNanos = maxTime.nanos(); - long startTime = System.nanoTime(); + long startTime = nowSupplier.getAsLong(); long nextStatus = startTime + statusNanos; int iter = 0; - while (isFinished() == false) { + while (true) { SubscribableListener fut = runSingleLoopIteration(); + iter++; if (fut.isDone() == false) { - status.set(updateStatus(DriverStatus.Status.ASYNC)); + updateStatus(nowSupplier.getAsLong() - startTime, iter, DriverStatus.Status.ASYNC); return fut; } + if (isFinished()) { + finishNanos = nowSupplier.getAsLong(); + updateStatus(finishNanos - startTime, iter, DriverStatus.Status.DONE); + driverContext.finish(); + Releasables.close(releasable, driverContext.getSnapshot()); + return Operator.NOT_BLOCKED; + } + long now = nowSupplier.getAsLong(); if (iter >= maxIterations) { - break; + updateStatus(now - startTime, iter, DriverStatus.Status.WAITING); + return Operator.NOT_BLOCKED; + } + if (now - startTime >= maxTimeNanos) { + updateStatus(now - startTime, iter, DriverStatus.Status.WAITING); + return Operator.NOT_BLOCKED; } - long now = System.nanoTime(); if (now > nextStatus) { - status.set(updateStatus(DriverStatus.Status.RUNNING)); + updateStatus(now - startTime, iter, DriverStatus.Status.RUNNING); nextStatus = now + statusNanos; } - iter++; - if (now - startTime > maxTimeNanos) { - break; - } - } - if (isFinished()) { - status.set(updateStatus(DriverStatus.Status.DONE)); - driverContext.finish(); - Releasables.close(releasable, driverContext.getSnapshot()); - } else { - status.set(updateStatus(DriverStatus.Status.WAITING)); } - return Operator.NOT_BLOCKED; } /** @@ -180,6 +220,7 @@ public void close() { * Abort the driver and wait for it to finish */ public void abort(Exception reason, ActionListener listener) { + finishNanos = System.nanoTime(); completionListener.addListener(listener); if (started.compareAndSet(false, true)) { drainAndCloseOperators(reason); @@ -286,7 +327,7 @@ public static void start( ) { driver.completionListener.addListener(listener); if (driver.started.compareAndSet(false, true)) { - driver.status.set(driver.updateStatus(DriverStatus.Status.STARTING)); + driver.updateStatus(0, 0, DriverStatus.Status.STARTING); schedule(DEFAULT_TIME_BEFORE_YIELDING, maxIterations, threadContext, executor, driver, driver.completionListener); } } @@ -324,7 +365,7 @@ protected void doRun() { onComplete(listener); return; } - SubscribableListener fut = driver.run(maxTime, maxIterations); + SubscribableListener fut = driver.run(maxTime, maxIterations, System::nanoTime); if (fut.isDone()) { schedule(maxTime, maxIterations, threadContext, executor, driver, listener); } else { @@ -384,23 +425,42 @@ public String sessionId() { /** * Get the last status update from the driver. These updates are made * when the driver is queued and after every - * processing {@link #run(TimeValue, int) batch}. + * processing {@link #run batch}. */ public DriverStatus status() { return status.get(); } + /** + * Build a "profile" of this driver's operations after it's been completed. + * This doesn't make sense to call before the driver is done. + */ + public DriverProfile profile() { + DriverStatus status = status(); + if (status.status() != DriverStatus.Status.DONE) { + throw new IllegalStateException("can only get profile from finished driver"); + } + return new DriverProfile(finishNanos - startNanos, status.cpuNanos(), status.iterations(), status.completedOperators()); + } + /** * Update the status. + * @param extraCpuNanos how many cpu nanoseconds to add to the previous status + * @param extraIterations how many iterations to add to the previous status * @param status the status of the overall driver request */ - private DriverStatus updateStatus(DriverStatus.Status status) { - return new DriverStatus( - sessionId, - System.currentTimeMillis(), - status, - statusOfCompletedOperators, - activeOperators.stream().map(op -> new DriverStatus.OperatorStatus(op.toString(), op.status())).toList() - ); + private void updateStatus(long extraCpuNanos, int extraIterations, DriverStatus.Status status) { + this.status.getAndUpdate(prev -> { + return new DriverStatus( + sessionId, + startTime, + System.currentTimeMillis(), + prev.cpuNanos() + extraCpuNanos, + prev.iterations() + extraIterations, + status, + statusOfCompletedOperators, + activeOperators.stream().map(op -> new DriverStatus.OperatorStatus(op.toString(), op.status())).toList() + ); + }); } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/DriverProfile.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/DriverProfile.java index d82ddc1899b1c..5f6e1ed12e204 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/DriverProfile.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/DriverProfile.java @@ -7,12 +7,15 @@ package org.elasticsearch.compute.operator; +import org.elasticsearch.TransportVersions; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Iterators; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.ChunkedToXContentHelper; import org.elasticsearch.common.xcontent.ChunkedToXContentObject; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.xcontent.ToXContent; import java.io.IOException; @@ -24,35 +27,99 @@ * Profile results from a single {@link Driver}. */ public class DriverProfile implements Writeable, ChunkedToXContentObject { + /** + * Nanos between creation and completion of the {@link Driver}. + */ + private final long tookNanos; + + /** + * Nanos this {@link Driver} has been running on the cpu. Does not + * include async or waiting time. + */ + private final long cpuNanos; + + /** + * The number of times the driver has moved a single page up the + * chain of operators as far as it'll go. + */ + private final long iterations; + /** * Status of each {@link Operator} in the driver when it finishes. */ private final List operators; - public DriverProfile(List operators) { + public DriverProfile(long tookNanos, long cpuNanos, long iterations, List operators) { + this.tookNanos = tookNanos; + this.cpuNanos = cpuNanos; + this.iterations = iterations; this.operators = operators; } public DriverProfile(StreamInput in) throws IOException { + if (in.getTransportVersion().onOrAfter(TransportVersions.ESQL_TIMINGS)) { + this.tookNanos = in.readVLong(); + this.cpuNanos = in.readVLong(); + this.iterations = in.readVLong(); + } else { + this.tookNanos = 0; + this.cpuNanos = 0; + this.iterations = 0; + } this.operators = in.readCollectionAsImmutableList(DriverStatus.OperatorStatus::new); } @Override public void writeTo(StreamOutput out) throws IOException { + if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_TIMINGS)) { + out.writeVLong(tookNanos); + out.writeVLong(cpuNanos); + out.writeVLong(iterations); + } out.writeCollection(operators); } + /** + * Nanos between creation and completion of the {@link Driver}. + */ + public long tookNanos() { + return tookNanos; + } + + /** + * Nanos this {@link Driver} has been running on the cpu. Does not + * include async or waiting time. + */ + public long cpuNanos() { + return cpuNanos; + } + + /** + * The number of times the driver has moved a single page up the + * chain of operators as far as it'll go. + */ + public long iterations() { + return iterations; + } + List operators() { return operators; } @Override public Iterator toXContentChunked(ToXContent.Params params) { - return Iterators.concat( - ChunkedToXContentHelper.startObject(), - ChunkedToXContentHelper.array("operators", operators.iterator()), - ChunkedToXContentHelper.endObject() - ); + return Iterators.concat(ChunkedToXContentHelper.startObject(), Iterators.single((b, p) -> { + b.field("took_nanos", tookNanos); + if (b.humanReadable()) { + b.field("took_time", TimeValue.timeValueNanos(tookNanos)); + } + b.field("cpu_nanos", cpuNanos); + if (b.humanReadable()) { + b.field("cpu_time", TimeValue.timeValueNanos(cpuNanos)); + } + b.field("iterations", iterations); + return b; + }), ChunkedToXContentHelper.array("operators", operators.iterator()), ChunkedToXContentHelper.endObject()); } @Override @@ -64,11 +131,19 @@ public boolean equals(Object o) { return false; } DriverProfile that = (DriverProfile) o; - return Objects.equals(operators, that.operators); + return tookNanos == that.tookNanos + && cpuNanos == that.cpuNanos + && iterations == that.iterations + && Objects.equals(operators, that.operators); } @Override public int hashCode() { - return Objects.hash(operators); + return Objects.hash(tookNanos, cpuNanos, iterations, operators); + } + + @Override + public String toString() { + return Strings.toString(this); } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/DriverStatus.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/DriverStatus.java index 90713381deb07..f143216303d35 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/DriverStatus.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/DriverStatus.java @@ -12,8 +12,10 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.VersionedNamedWriteable; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.mapper.DateFieldMapper; import org.elasticsearch.tasks.Task; import org.elasticsearch.xcontent.ToXContentFragment; @@ -39,10 +41,29 @@ public class DriverStatus implements Task.Status { * The session for this driver. */ private final String sessionId; + + /** + * Milliseconds since epoch when this driver started. + */ + private final long started; + /** * When this status was generated. */ private final long lastUpdated; + + /** + * Nanos this {@link Driver} has been running on the cpu. Does not + * include async or waiting time. + */ + private final long cpuNanos; + + /** + * The number of times the driver has moved a single page up the + * chain of operators as far as it'll go. + */ + private final long iterations; + /** * The state of the overall driver - queue, starting, running, finished. */ @@ -60,13 +81,19 @@ public class DriverStatus implements Task.Status { DriverStatus( String sessionId, + long started, long lastUpdated, + long cpuTime, + long iterations, Status status, List completedOperators, List activeOperators ) { this.sessionId = sessionId; + this.started = started; this.lastUpdated = lastUpdated; + this.cpuNanos = cpuTime; + this.iterations = iterations; this.status = status; this.completedOperators = completedOperators; this.activeOperators = activeOperators; @@ -74,7 +101,10 @@ public class DriverStatus implements Task.Status { public DriverStatus(StreamInput in) throws IOException { this.sessionId = in.readString(); + this.started = in.getTransportVersion().onOrAfter(TransportVersions.ESQL_TIMINGS) ? in.readLong() : 0; this.lastUpdated = in.readLong(); + this.cpuNanos = in.getTransportVersion().onOrAfter(TransportVersions.ESQL_TIMINGS) ? in.readVLong() : 0; + this.iterations = in.getTransportVersion().onOrAfter(TransportVersions.ESQL_TIMINGS) ? in.readVLong() : 0; this.status = Status.valueOf(in.readString()); if (in.getTransportVersion().onOrAfter(TransportVersions.V_8_12_0)) { this.completedOperators = in.readCollectionAsImmutableList(OperatorStatus::new); @@ -87,7 +117,14 @@ public DriverStatus(StreamInput in) throws IOException { @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(sessionId); + if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_TIMINGS)) { + out.writeLong(started); + } out.writeLong(lastUpdated); + if (out.getTransportVersion().onOrAfter(TransportVersions.ESQL_TIMINGS)) { + out.writeVLong(cpuNanos); + out.writeVLong(iterations); + } out.writeString(status.toString()); if (out.getTransportVersion().onOrAfter(TransportVersions.V_8_12_0)) { out.writeCollection(completedOperators); @@ -107,6 +144,13 @@ public String sessionId() { return sessionId; } + /** + * When this {@link Driver} was started. + */ + public long started() { + return started; + } + /** * When this status was generated. */ @@ -114,6 +158,22 @@ public long lastUpdated() { return lastUpdated; } + /** + * Nanos this {@link Driver} has been running on the cpu. Does not + * include async or waiting time. + */ + public long cpuNanos() { + return cpuNanos; + } + + /** + * The number of times the driver has moved a single page up the + * chain of operators as far as it'll go. + */ + public long iterations() { + return iterations; + } + /** * The state of the overall driver - queue, starting, running, finished. */ @@ -139,7 +199,13 @@ public List activeOperators() { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field("sessionId", sessionId); + builder.field("started", DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(started)); builder.field("last_updated", DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.formatMillis(lastUpdated)); + builder.field("cpu_nanos", cpuNanos); + if (builder.humanReadable()) { + builder.field("cpu_time", TimeValue.timeValueNanos(cpuNanos)); + } + builder.field("iterations", iterations); builder.field("status", status.toString().toLowerCase(Locale.ROOT)); builder.startArray("completed_operators"); for (OperatorStatus completed : completedOperators) { @@ -160,7 +226,10 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; DriverStatus that = (DriverStatus) o; return sessionId.equals(that.sessionId) + && started == that.started && lastUpdated == that.lastUpdated + && cpuNanos == that.cpuNanos + && iterations == that.iterations && status == that.status && completedOperators.equals(that.completedOperators) && activeOperators.equals(that.activeOperators); @@ -168,7 +237,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(sessionId, lastUpdated, status, completedOperators, activeOperators); + return Objects.hash(sessionId, started, lastUpdated, cpuNanos, iterations, status, completedOperators, activeOperators); } @Override @@ -204,7 +273,7 @@ public OperatorStatus(String operator, Operator.Status status) { @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(operator); - out.writeOptionalNamedWriteable(status); + out.writeOptionalNamedWriteable(status != null && VersionedNamedWriteable.shouldSerialize(out, status) ? status : null); } public String operator() { 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 ad3dce98e34d9..6dcdd15fd1d1c 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 @@ -7,6 +7,12 @@ package org.elasticsearch.compute.operator; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.compute.Describable; import org.elasticsearch.compute.aggregation.GroupingAggregator; import org.elasticsearch.compute.aggregation.GroupingAggregatorFunction; @@ -17,10 +23,14 @@ import org.elasticsearch.compute.data.IntVector; import org.elasticsearch.compute.data.Page; import org.elasticsearch.core.Releasables; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.xcontent.XContentBuilder; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.function.Supplier; import static java.util.Objects.requireNonNull; @@ -61,6 +71,19 @@ public String describe() { private final DriverContext driverContext; + /** + * Nanoseconds this operator has spent hashing grouping keys. + */ + private long hashNanos; + /** + * Nanoseconds this operator has spent running the aggregations. + */ + private long aggregationNanos; + /** + * Count of pages this operator has processed. + */ + private int pagesProcessed; + @SuppressWarnings("this-escape") public HashAggregationOperator( List aggregators, @@ -91,36 +114,58 @@ public boolean needsInput() { @Override public void addInput(Page page) { try { - checkState(needsInput(), "Operator is already finishing"); - requireNonNull(page, "page is null"); - GroupingAggregatorFunction.AddInput[] prepared = new GroupingAggregatorFunction.AddInput[aggregators.size()]; - for (int i = 0; i < prepared.length; i++) { - prepared[i] = aggregators.get(i).prepareProcessPage(blockHash, page); - } + class AddInput implements GroupingAggregatorFunction.AddInput { + long hashStart = System.nanoTime(); + long aggStart; - blockHash.add(wrapPage(page), new GroupingAggregatorFunction.AddInput() { @Override public void add(int positionOffset, IntBlock groupIds) { IntVector groupIdsVector = groupIds.asVector(); if (groupIdsVector != null) { add(positionOffset, groupIdsVector); } else { + startAggEndHash(); for (GroupingAggregatorFunction.AddInput p : prepared) { p.add(positionOffset, groupIds); } + end(); } } @Override public void add(int positionOffset, IntVector groupIds) { + startAggEndHash(); for (GroupingAggregatorFunction.AddInput p : prepared) { p.add(positionOffset, groupIds); } + end(); } - }); + + private void startAggEndHash() { + aggStart = System.nanoTime(); + hashNanos += aggStart - hashStart; + } + + private void end() { + hashStart = System.nanoTime(); + aggregationNanos += hashStart - aggStart; + } + } + AddInput add = new AddInput(); + + checkState(needsInput(), "Operator is already finishing"); + requireNonNull(page, "page is null"); + + for (int i = 0; i < prepared.length; i++) { + prepared[i] = aggregators.get(i).prepareProcessPage(blockHash, page); + } + + blockHash.add(wrapPage(page), add); + hashNanos += System.nanoTime() - add.hashStart; } finally { page.releaseBlocks(); + pagesProcessed++; } } @@ -178,6 +223,11 @@ public void close() { Releasables.close(blockHash, () -> Releasables.close(aggregators)); } + @Override + public Operator.Status status() { + return new Status(hashNanos, aggregationNanos, pagesProcessed); + } + protected static void checkState(boolean condition, String msg) { if (condition == false) { throw new IllegalArgumentException(msg); @@ -197,4 +247,115 @@ public String toString() { sb.append("]"); return sb.toString(); } + + public static class Status implements Operator.Status { + public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( + Operator.Status.class, + "hashagg", + Status::new + ); + + /** + * Nanoseconds this operator has spent hashing grouping keys. + */ + private final long hashNanos; + /** + * Nanoseconds this operator has spent running the aggregations. + */ + private final long aggregationNanos; + /** + * Count of pages this operator has processed. + */ + private final int pagesProcessed; + + /** + * Build. + * @param hashNanos Nanoseconds this operator has spent hashing grouping keys. + * @param aggregationNanos Nanoseconds this operator has spent running the aggregations. + * @param pagesProcessed Count of pages this operator has processed. + */ + public Status(long hashNanos, long aggregationNanos, int pagesProcessed) { + this.hashNanos = hashNanos; + this.aggregationNanos = aggregationNanos; + this.pagesProcessed = pagesProcessed; + } + + protected Status(StreamInput in) throws IOException { + hashNanos = in.readVLong(); + aggregationNanos = in.readVLong(); + pagesProcessed = in.readVInt(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeVLong(hashNanos); + out.writeVLong(aggregationNanos); + out.writeVInt(pagesProcessed); + } + + @Override + public String getWriteableName() { + return ENTRY.name; + } + + /** + * Nanoseconds this operator has spent hashing grouping keys. + */ + public long hashNanos() { + return hashNanos; + } + + /** + * Nanoseconds this operator has spent running the aggregations. + */ + public long aggregationNanos() { + return aggregationNanos; + } + + /** + * Count of pages this operator has processed. + */ + public int pagesProcessed() { + return pagesProcessed; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("hash_nanos", hashNanos); + if (builder.humanReadable()) { + builder.field("hash_time", TimeValue.timeValueNanos(hashNanos)); + } + builder.field("aggregation_nanos", aggregationNanos); + if (builder.humanReadable()) { + builder.field("aggregation_time", TimeValue.timeValueNanos(aggregationNanos)); + } + builder.field("pages_processed", pagesProcessed); + return builder.endObject(); + + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Status status = (Status) o; + return hashNanos == status.hashNanos && aggregationNanos == status.aggregationNanos && pagesProcessed == status.pagesProcessed; + } + + @Override + public int hashCode() { + return Objects.hash(hashNanos, aggregationNanos, pagesProcessed); + } + + @Override + public String toString() { + return Strings.toString(this); + } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.ESQL_TIMINGS; + } + } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/LimitOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/LimitOperator.java index bcd2ffa1f3855..34e37031e6f11 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/LimitOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/LimitOperator.java @@ -7,6 +7,8 @@ package org.elasticsearch.compute.operator; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; @@ -229,5 +231,10 @@ public int hashCode() { public String toString() { return Strings.toString(this); } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.V_8_11_X; + } } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/MvExpandOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/MvExpandOperator.java index 629cacb82a97f..e87329a907054 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/MvExpandOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/MvExpandOperator.java @@ -7,6 +7,8 @@ package org.elasticsearch.compute.operator; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; @@ -322,5 +324,10 @@ public int hashCode() { public String toString() { return Strings.toString(this); } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.V_8_11_X; + } } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/Operator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/Operator.java index fd6589bf5a913..1038277c39fe1 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/Operator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/Operator.java @@ -8,7 +8,7 @@ package org.elasticsearch.compute.operator; import org.elasticsearch.action.support.SubscribableListener; -import org.elasticsearch.common.io.stream.NamedWriteable; +import org.elasticsearch.common.io.stream.VersionedNamedWriteable; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.compute.Describable; import org.elasticsearch.compute.data.Block; @@ -105,5 +105,5 @@ interface OperatorFactory extends Describable { /** * Status of an {@link Operator} to be returned by the tasks API. */ - interface Status extends ToXContentObject, NamedWriteable {} + interface Status extends ToXContentObject, VersionedNamedWriteable {} } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSinkOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSinkOperator.java index fed0b2de4454b..01354d681017a 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSinkOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSinkOperator.java @@ -7,6 +7,8 @@ package org.elasticsearch.compute.operator.exchange; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; import org.elasticsearch.action.support.SubscribableListener; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; @@ -148,5 +150,10 @@ public int hashCode() { public String toString() { return Strings.toString(this); } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.V_8_11_X; + } } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSourceOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSourceOperator.java index 8719ed6ab90ea..1efba31bd831b 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSourceOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/exchange/ExchangeSourceOperator.java @@ -7,6 +7,8 @@ package org.elasticsearch.compute.operator.exchange; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; import org.elasticsearch.action.support.SubscribableListener; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; @@ -157,5 +159,10 @@ public int hashCode() { public String toString() { return Strings.toString(this); } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.V_8_11_X; + } } } diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/TopNOperatorStatus.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/TopNOperatorStatus.java index 1261332ea1423..1617a546be2cc 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/TopNOperatorStatus.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/topn/TopNOperatorStatus.java @@ -7,6 +7,8 @@ package org.elasticsearch.compute.operator.topn; +import org.elasticsearch.TransportVersion; +import org.elasticsearch.TransportVersions; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -77,4 +79,9 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(occupiedRows, ramBytesUsed); } + + @Override + public TransportVersion getMinimalSupportedVersion() { + return TransportVersions.V_8_11_X; + } } diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneSourceOperatorStatusTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneSourceOperatorStatusTests.java index 6c787052a8ae7..def0710644d22 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneSourceOperatorStatusTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/LuceneSourceOperatorStatusTests.java @@ -20,7 +20,7 @@ public class LuceneSourceOperatorStatusTests extends AbstractWireSerializingTestCase { public static LuceneSourceOperator.Status simple() { - return new LuceneSourceOperator.Status(2, Set.of("*:*"), new TreeSet<>(List.of("a:0", "a:1")), 0, 1, 5, 123, 99990, 8000); + return new LuceneSourceOperator.Status(2, Set.of("*:*"), new TreeSet<>(List.of("a:0", "a:1")), 1002, 0, 1, 5, 123, 99990, 8000); } public static String simpleToJson() { @@ -34,6 +34,8 @@ public static String simpleToJson() { "a:0", "a:1" ], + "processing_nanos" : 1002, + "processing_time" : "1micros", "slice_index" : 0, "total_slices" : 1, "pages_emitted" : 5, @@ -58,6 +60,7 @@ public LuceneSourceOperator.Status createTestInstance() { randomNonNegativeInt(), randomProcessedQueries(), randomProcessedShards(), + randomNonNegativeLong(), randomNonNegativeInt(), randomNonNegativeInt(), randomNonNegativeInt(), @@ -90,29 +93,31 @@ protected LuceneSourceOperator.Status mutateInstance(LuceneSourceOperator.Status int processedSlices = instance.processedSlices(); Set processedQueries = instance.processedQueries(); Set processedShards = instance.processedShards(); + long processNanos = instance.processNanos(); int sliceIndex = instance.sliceIndex(); int totalSlices = instance.totalSlices(); int pagesEmitted = instance.pagesEmitted(); int sliceMin = instance.sliceMin(); int sliceMax = instance.sliceMax(); int current = instance.current(); - switch (between(0, 8)) { + switch (between(0, 9)) { case 0 -> processedSlices = randomValueOtherThan(processedSlices, ESTestCase::randomNonNegativeInt); case 1 -> processedQueries = randomValueOtherThan(processedQueries, LuceneSourceOperatorStatusTests::randomProcessedQueries); case 2 -> processedShards = randomValueOtherThan(processedShards, LuceneSourceOperatorStatusTests::randomProcessedShards); - case 3 -> sliceIndex = randomValueOtherThan(sliceIndex, ESTestCase::randomNonNegativeInt); - case 4 -> totalSlices = randomValueOtherThan(totalSlices, ESTestCase::randomNonNegativeInt); - case 5 -> pagesEmitted = randomValueOtherThan(pagesEmitted, ESTestCase::randomNonNegativeInt); - case 6 -> sliceMin = randomValueOtherThan(sliceMin, ESTestCase::randomNonNegativeInt); - case 7 -> sliceMax = randomValueOtherThan(sliceMax, ESTestCase::randomNonNegativeInt); - case 8 -> current = randomValueOtherThan(current, ESTestCase::randomNonNegativeInt); + case 3 -> processNanos = randomValueOtherThan(processNanos, ESTestCase::randomNonNegativeLong); + case 4 -> sliceIndex = randomValueOtherThan(sliceIndex, ESTestCase::randomNonNegativeInt); + case 5 -> totalSlices = randomValueOtherThan(totalSlices, ESTestCase::randomNonNegativeInt); + case 6 -> pagesEmitted = randomValueOtherThan(pagesEmitted, ESTestCase::randomNonNegativeInt); + case 7 -> sliceMin = randomValueOtherThan(sliceMin, ESTestCase::randomNonNegativeInt); + case 8 -> sliceMax = randomValueOtherThan(sliceMax, ESTestCase::randomNonNegativeInt); + case 9 -> current = randomValueOtherThan(current, ESTestCase::randomNonNegativeInt); default -> throw new UnsupportedOperationException(); } - ; return new LuceneSourceOperator.Status( processedSlices, processedQueries, processedShards, + processNanos, sliceIndex, totalSlices, pagesEmitted, diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperatorStatusTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperatorStatusTests.java index 1851f7ac948cc..5887da0bc466b 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperatorStatusTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/lucene/ValuesSourceReaderOperatorStatusTests.java @@ -10,6 +10,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.test.ESTestCase; import java.io.IOException; import java.util.Map; @@ -19,7 +20,7 @@ public class ValuesSourceReaderOperatorStatusTests extends AbstractWireSerializingTestCase { public static ValuesSourceReaderOperator.Status simple() { - return new ValuesSourceReaderOperator.Status(Map.of("ReaderType", 3), 123); + return new ValuesSourceReaderOperator.Status(Map.of("ReaderType", 3), 1022323, 123); } public static String simpleToJson() { @@ -28,6 +29,8 @@ public static String simpleToJson() { "readers_built" : { "ReaderType" : 3 }, + "process_nanos" : 1022323, + "process_time" : "1ms", "pages_processed" : 123 }"""; } @@ -43,7 +46,7 @@ protected Writeable.Reader instanceReader() { @Override public ValuesSourceReaderOperator.Status createTestInstance() { - return new ValuesSourceReaderOperator.Status(randomReadersBuilt(), between(0, Integer.MAX_VALUE)); + return new ValuesSourceReaderOperator.Status(randomReadersBuilt(), randomNonNegativeLong(), randomNonNegativeInt()); } private Map randomReadersBuilt() { @@ -57,19 +60,15 @@ private Map randomReadersBuilt() { @Override protected ValuesSourceReaderOperator.Status mutateInstance(ValuesSourceReaderOperator.Status instance) throws IOException { - switch (between(0, 1)) { - case 0: - return new ValuesSourceReaderOperator.Status( - randomValueOtherThan(instance.readersBuilt(), this::randomReadersBuilt), - instance.pagesProcessed() - ); - case 1: - return new ValuesSourceReaderOperator.Status( - instance.readersBuilt(), - randomValueOtherThan(instance.pagesProcessed(), () -> between(0, Integer.MAX_VALUE)) - ); - default: - throw new UnsupportedOperationException(); + Map readersBuilt = instance.readersBuilt(); + long processNanos = instance.processNanos(); + int pagesProcessed = instance.pagesProcessed(); + switch (between(0, 2)) { + case 0 -> readersBuilt = randomValueOtherThan(readersBuilt, this::randomReadersBuilt); + case 1 -> processNanos = randomValueOtherThan(processNanos, ESTestCase::randomNonNegativeLong); + case 2 -> pagesProcessed = randomValueOtherThan(pagesProcessed, ESTestCase::randomNonNegativeInt); + default -> throw new UnsupportedOperationException(); } + return new ValuesSourceReaderOperator.Status(readersBuilt, processNanos, pagesProcessed); } } diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/AbstractPageMappingOperatorStatusTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/AbstractPageMappingOperatorStatusTests.java index c72e87bb96a81..3c04e6e5a9f57 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/AbstractPageMappingOperatorStatusTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/AbstractPageMappingOperatorStatusTests.java @@ -16,16 +16,20 @@ public class AbstractPageMappingOperatorStatusTests extends AbstractWireSerializingTestCase { public static AbstractPageMappingOperator.Status simple() { - return new AbstractPageMappingOperator.Status(123); + return new AbstractPageMappingOperator.Status(200012, 123); } public static String simpleToJson() { return """ - {"pages_processed":123}"""; + { + "process_nanos" : 200012, + "process_time" : "200micros", + "pages_processed" : 123 + }"""; } public void testToXContent() { - assertThat(Strings.toString(simple()), equalTo(simpleToJson())); + assertThat(Strings.toString(simple(), true, true), equalTo(simpleToJson())); } @Override @@ -35,11 +39,18 @@ protected Writeable.Reader instanceReader() @Override public AbstractPageMappingOperator.Status createTestInstance() { - return new AbstractPageMappingOperator.Status(randomNonNegativeInt()); + return new AbstractPageMappingOperator.Status(randomNonNegativeLong(), randomNonNegativeInt()); } @Override protected AbstractPageMappingOperator.Status mutateInstance(AbstractPageMappingOperator.Status instance) { - return new AbstractPageMappingOperator.Status(randomValueOtherThan(instance.pagesProcessed(), ESTestCase::randomNonNegativeInt)); + long processNanos = instance.processNanos(); + int pagesProcessed = instance.pagesProcessed(); + switch (between(0, 1)) { + case 0 -> processNanos = randomValueOtherThan(processNanos, ESTestCase::randomNonNegativeLong); + case 1 -> pagesProcessed = randomValueOtherThan(pagesProcessed, ESTestCase::randomNonNegativeInt); + default -> throw new UnsupportedOperationException(); + } + return new AbstractPageMappingOperator.Status(processNanos, pagesProcessed); } } diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/AggregationOperatorStatusTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/AggregationOperatorStatusTests.java new file mode 100644 index 0000000000000..5d17538ee85ae --- /dev/null +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/AggregationOperatorStatusTests.java @@ -0,0 +1,56 @@ +/* + * 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.operator; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.test.ESTestCase; + +import static org.hamcrest.Matchers.equalTo; + +public class AggregationOperatorStatusTests extends AbstractWireSerializingTestCase { + public static AggregationOperator.Status simple() { + return new AggregationOperator.Status(200012, 123); + } + + public static String simpleToJson() { + return """ + { + "aggregation_nanos" : 200012, + "aggregation_time" : "200micros", + "pages_processed" : 123 + }"""; + } + + public void testToXContent() { + assertThat(Strings.toString(simple(), true, true), equalTo(simpleToJson())); + } + + @Override + protected Writeable.Reader instanceReader() { + return AggregationOperator.Status::new; + } + + @Override + public AggregationOperator.Status createTestInstance() { + return new AggregationOperator.Status(randomNonNegativeLong(), randomNonNegativeInt()); + } + + @Override + protected AggregationOperator.Status mutateInstance(AggregationOperator.Status instance) { + long aggregationNanos = instance.aggregationNanos(); + int pagesProcessed = instance.pagesProcessed(); + switch (between(0, 1)) { + case 0 -> aggregationNanos = randomValueOtherThan(aggregationNanos, ESTestCase::randomNonNegativeLong); + case 1 -> pagesProcessed = randomValueOtherThan(pagesProcessed, ESTestCase::randomNonNegativeInt); + default -> throw new UnsupportedOperationException(); + } + return new AggregationOperator.Status(aggregationNanos, pagesProcessed); + } +} diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverProfileTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverProfileTests.java index ec9952cdce022..86655bd3b7f73 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverProfileTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverProfileTests.java @@ -16,6 +16,7 @@ import org.elasticsearch.compute.lucene.ValuesSourceReaderOperatorStatusTests; import org.elasticsearch.compute.operator.exchange.ExchangeSinkOperator; import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.test.ESTestCase; import java.io.IOException; import java.util.List; @@ -25,6 +26,9 @@ public class DriverProfileTests extends AbstractWireSerializingTestCase { public void testToXContent() { DriverProfile status = new DriverProfile( + 10012, + 10000, + 12, List.of( new DriverStatus.OperatorStatus("LuceneSource", LuceneSourceOperatorStatusTests.simple()), new DriverStatus.OperatorStatus("ValuesSourceReader", ValuesSourceReaderOperatorStatusTests.simple()) @@ -32,6 +36,11 @@ public void testToXContent() { ); assertThat(Strings.toString(status, true, true), equalTo(""" { + "took_nanos" : 10012, + "took_time" : "10micros", + "cpu_nanos" : 10000, + "cpu_time" : "10micros", + "iterations" : 12, "operators" : [ { "operator" : "LuceneSource", @@ -56,13 +65,28 @@ protected Writeable.Reader instanceReader() { @Override protected DriverProfile createTestInstance() { - return new DriverProfile(DriverStatusTests.randomOperatorStatuses()); + return new DriverProfile( + randomNonNegativeLong(), + randomNonNegativeLong(), + randomNonNegativeLong(), + DriverStatusTests.randomOperatorStatuses() + ); } @Override protected DriverProfile mutateInstance(DriverProfile instance) throws IOException { - var operators = randomValueOtherThan(instance.operators(), DriverStatusTests::randomOperatorStatuses); - return new DriverProfile(operators); + long tookNanos = instance.tookNanos(); + long cpuNanos = instance.cpuNanos(); + long iterations = instance.iterations(); + var operators = instance.operators(); + switch (between(0, 3)) { + case 0 -> tookNanos = randomValueOtherThan(tookNanos, ESTestCase::randomNonNegativeLong); + case 1 -> cpuNanos = randomValueOtherThan(cpuNanos, ESTestCase::randomNonNegativeLong); + case 2 -> iterations = randomValueOtherThan(iterations, ESTestCase::randomNonNegativeLong); + case 3 -> operators = randomValueOtherThan(operators, DriverStatusTests::randomOperatorStatuses); + default -> throw new UnsupportedOperationException(); + } + return new DriverProfile(tookNanos, cpuNanos, iterations, operators); } @Override diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverStatusTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverStatusTests.java index c10bcf8d49ca4..e82cbb831cff2 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverStatusTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverStatusTests.java @@ -31,7 +31,10 @@ public class DriverStatusTests extends AbstractWireSerializingTestCase instanceReader() { @Override protected DriverStatus createTestInstance() { - return new DriverStatus(randomSessionId(), randomLong(), randomStatus(), randomOperatorStatuses(), randomOperatorStatuses()); + return new DriverStatus( + randomSessionId(), + randomNonNegativeLong(), + randomNonNegativeLong(), + randomNonNegativeLong(), + randomNonNegativeLong(), + randomStatus(), + randomOperatorStatuses(), + randomOperatorStatuses() + ); } private String randomSessionId() { @@ -104,30 +120,25 @@ private static DriverStatus.OperatorStatus randomOperatorStatus() { @Override protected DriverStatus mutateInstance(DriverStatus instance) throws IOException { var sessionId = instance.sessionId(); + long started = instance.started(); long lastUpdated = instance.lastUpdated(); + long cpuNanos = instance.cpuNanos(); + long iterations = instance.iterations(); var status = instance.status(); var completedOperators = instance.completedOperators(); var activeOperators = instance.activeOperators(); - switch (between(0, 4)) { - case 0: - sessionId = randomValueOtherThan(sessionId, this::randomSessionId); - break; - case 1: - lastUpdated = randomValueOtherThan(lastUpdated, ESTestCase::randomLong); - break; - case 2: - status = randomValueOtherThan(status, this::randomStatus); - break; - case 3: - completedOperators = randomValueOtherThan(completedOperators, DriverStatusTests::randomOperatorStatuses); - break; - case 4: - activeOperators = randomValueOtherThan(activeOperators, DriverStatusTests::randomOperatorStatuses); - break; - default: - throw new UnsupportedOperationException(); + switch (between(0, 7)) { + case 0 -> sessionId = randomValueOtherThan(sessionId, this::randomSessionId); + case 1 -> started = randomValueOtherThan(started, ESTestCase::randomNonNegativeLong); + case 2 -> lastUpdated = randomValueOtherThan(lastUpdated, ESTestCase::randomNonNegativeLong); + case 3 -> cpuNanos = randomValueOtherThan(cpuNanos, ESTestCase::randomNonNegativeLong); + case 4 -> iterations = randomValueOtherThan(iterations, ESTestCase::randomNonNegativeLong); + case 5 -> status = randomValueOtherThan(status, this::randomStatus); + case 6 -> completedOperators = randomValueOtherThan(completedOperators, DriverStatusTests::randomOperatorStatuses); + case 7 -> activeOperators = randomValueOtherThan(activeOperators, DriverStatusTests::randomOperatorStatuses); + default -> throw new UnsupportedOperationException(); } - return new DriverStatus(sessionId, lastUpdated, status, completedOperators, activeOperators); + return new DriverStatus(sessionId, started, lastUpdated, cpuNanos, iterations, status, completedOperators, activeOperators); } @Override diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverTests.java index ba45db3c48299..694aaba4bd85e 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/DriverTests.java @@ -35,10 +35,177 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.TimeUnit; +import java.util.function.LongSupplier; import static org.hamcrest.Matchers.equalTo; public class DriverTests extends ESTestCase { + /** + * Runs a driver to completion in a single call and asserts that the + * status and profile returned makes sense. + */ + public void testProfileAndStatusFinishInOneRound() { + DriverContext driverContext = driverContext(); + List inPages = randomList(1, 100, DriverTests::randomPage); + List outPages = new ArrayList<>(); + + long startEpoch = randomNonNegativeLong(); + long startNanos = randomLong(); + long waitTime = randomLongBetween(1000, 100000); + long tickTime = randomLongBetween(1, 10000); + + Driver driver = new Driver( + "unset", + startEpoch, + startNanos, + driverContext, + () -> "unset", + new CannedSourceOperator(inPages.iterator()), + List.of(), + new TestResultPageSinkOperator(outPages::add), + TimeValue.timeValueDays(10), + () -> {} + ); + + NowSupplier nowSupplier = new NowSupplier(startNanos, waitTime, tickTime); + + logger.info("status {}", driver.status()); + assertThat(driver.status().status(), equalTo(DriverStatus.Status.QUEUED)); + assertThat(driver.status().started(), equalTo(startEpoch)); + assertThat(driver.status().cpuNanos(), equalTo(0L)); + assertThat(driver.status().iterations(), equalTo(0L)); + driver.run(TimeValue.timeValueSeconds(Long.MAX_VALUE), Integer.MAX_VALUE, nowSupplier); + logger.info("status {}", driver.status()); + assertThat(driver.status().status(), equalTo(DriverStatus.Status.DONE)); + assertThat(driver.status().started(), equalTo(startEpoch)); + long sumRunningTime = tickTime * (nowSupplier.callCount - 1); + assertThat(driver.status().cpuNanos(), equalTo(sumRunningTime)); + assertThat(driver.status().iterations(), equalTo((long) inPages.size())); + + logger.info("profile {}", driver.profile()); + assertThat(driver.profile().tookNanos(), equalTo(waitTime + sumRunningTime)); + assertThat(driver.profile().cpuNanos(), equalTo(sumRunningTime)); + assertThat(driver.profile().iterations(), equalTo((long) inPages.size())); + } + + /** + * Runs the driver processing a single page at a time and asserting that + * the status reported between each call is sane. And that the profile + * returned after completion is sane. + */ + public void testProfileAndStatusOneIterationAtATime() { + DriverContext driverContext = driverContext(); + List inPages = randomList(2, 100, DriverTests::randomPage); + List outPages = new ArrayList<>(); + + long startEpoch = randomNonNegativeLong(); + long startNanos = randomLong(); + long waitTime = randomLongBetween(1000, 100000); + long tickTime = randomLongBetween(1, 10000); + + Driver driver = new Driver( + "unset", + startEpoch, + startNanos, + driverContext, + () -> "unset", + new CannedSourceOperator(inPages.iterator()), + List.of(), + new TestResultPageSinkOperator(outPages::add), + TimeValue.timeValueDays(10), + () -> {} + ); + + NowSupplier nowSupplier = new NowSupplier(startNanos, waitTime, tickTime); + for (int i = 0; i < inPages.size(); i++) { + logger.info("status {} {}", i, driver.status()); + assertThat(driver.status().status(), equalTo(i == 0 ? DriverStatus.Status.QUEUED : DriverStatus.Status.WAITING)); + assertThat(driver.status().started(), equalTo(startEpoch)); + assertThat(driver.status().iterations(), equalTo((long) i)); + assertThat(driver.status().cpuNanos(), equalTo(tickTime * i)); + driver.run(TimeValue.timeValueSeconds(Long.MAX_VALUE), 1, nowSupplier); + } + + logger.info("status {}", driver.status()); + assertThat(driver.status().status(), equalTo(DriverStatus.Status.DONE)); + assertThat(driver.status().started(), equalTo(startEpoch)); + assertThat(driver.status().iterations(), equalTo((long) inPages.size())); + assertThat(driver.status().cpuNanos(), equalTo(tickTime * inPages.size())); + + logger.info("profile {}", driver.profile()); + assertThat(driver.profile().tookNanos(), equalTo(waitTime + tickTime * (nowSupplier.callCount - 1))); + assertThat(driver.profile().cpuNanos(), equalTo(tickTime * inPages.size())); + assertThat(driver.profile().iterations(), equalTo((long) inPages.size())); + } + + /** + * Runs the driver processing a single page at a time via a synthetic timeout + * and asserting that the status reported between each call is sane. And that + * the profile returned after completion is sane. + */ + public void testProfileAndStatusTimeout() { + DriverContext driverContext = driverContext(); + List inPages = randomList(2, 100, DriverTests::randomPage); + List outPages = new ArrayList<>(); + + long startEpoch = randomNonNegativeLong(); + long startNanos = randomLong(); + long waitTime = randomLongBetween(1000, 100000); + long tickTime = randomLongBetween(1, 10000); + + Driver driver = new Driver( + "unset", + startEpoch, + startNanos, + driverContext, + () -> "unset", + new CannedSourceOperator(inPages.iterator()), + List.of(), + new TestResultPageSinkOperator(outPages::add), + TimeValue.timeValueNanos(tickTime), + () -> {} + ); + + NowSupplier nowSupplier = new NowSupplier(startNanos, waitTime, tickTime); + for (int i = 0; i < inPages.size(); i++) { + logger.info("status {} {}", i, driver.status()); + assertThat(driver.status().status(), equalTo(i == 0 ? DriverStatus.Status.QUEUED : DriverStatus.Status.WAITING)); + assertThat(driver.status().started(), equalTo(startEpoch)); + assertThat(driver.status().iterations(), equalTo((long) i)); + assertThat(driver.status().cpuNanos(), equalTo(tickTime * i)); + driver.run(TimeValue.timeValueNanos(tickTime), Integer.MAX_VALUE, nowSupplier); + } + + logger.info("status {}", driver.status()); + assertThat(driver.status().status(), equalTo(DriverStatus.Status.DONE)); + assertThat(driver.status().started(), equalTo(startEpoch)); + assertThat(driver.status().iterations(), equalTo((long) inPages.size())); + assertThat(driver.status().cpuNanos(), equalTo(tickTime * inPages.size())); + + logger.info("profile {}", driver.profile()); + assertThat(driver.profile().tookNanos(), equalTo(waitTime + tickTime * (nowSupplier.callCount - 1))); + assertThat(driver.profile().cpuNanos(), equalTo(tickTime * inPages.size())); + assertThat(driver.profile().iterations(), equalTo((long) inPages.size())); + } + + class NowSupplier implements LongSupplier { + private final long startNanos; + private final long waitTime; + private final long tickTime; + + private int callCount; + + NowSupplier(long startNanos, long waitTime, long tickTime) { + this.startNanos = startNanos; + this.waitTime = waitTime; + this.tickTime = tickTime; + } + + @Override + public long getAsLong() { + return startNanos + waitTime + tickTime * callCount++; + } + } public void testThreadContext() throws Exception { DriverContext driverContext = driverContext(); diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/HashAggregationOperatorStatusTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/HashAggregationOperatorStatusTests.java new file mode 100644 index 0000000000000..245ae171c630b --- /dev/null +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/HashAggregationOperatorStatusTests.java @@ -0,0 +1,60 @@ +/* + * 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.operator; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.test.ESTestCase; + +import static org.hamcrest.Matchers.equalTo; + +public class HashAggregationOperatorStatusTests extends AbstractWireSerializingTestCase { + public static HashAggregationOperator.Status simple() { + return new HashAggregationOperator.Status(500012, 200012, 123); + } + + public static String simpleToJson() { + return """ + { + "hash_nanos" : 500012, + "hash_time" : "500micros", + "aggregation_nanos" : 200012, + "aggregation_time" : "200micros", + "pages_processed" : 123 + }"""; + } + + public void testToXContent() { + assertThat(Strings.toString(simple(), true, true), equalTo(simpleToJson())); + } + + @Override + protected Writeable.Reader instanceReader() { + return HashAggregationOperator.Status::new; + } + + @Override + public HashAggregationOperator.Status createTestInstance() { + return new HashAggregationOperator.Status(randomNonNegativeLong(), randomNonNegativeLong(), randomNonNegativeInt()); + } + + @Override + protected HashAggregationOperator.Status mutateInstance(HashAggregationOperator.Status instance) { + long hashNanos = instance.hashNanos(); + long aggregationNanos = instance.aggregationNanos(); + int pagesProcessed = instance.pagesProcessed(); + switch (between(0, 2)) { + case 0 -> hashNanos = randomValueOtherThan(hashNanos, ESTestCase::randomNonNegativeLong); + case 1 -> aggregationNanos = randomValueOtherThan(aggregationNanos, ESTestCase::randomNonNegativeLong); + case 2 -> pagesProcessed = randomValueOtherThan(pagesProcessed, ESTestCase::randomNonNegativeInt); + default -> throw new UnsupportedOperationException(); + } + return new HashAggregationOperator.Status(hashNanos, aggregationNanos, pagesProcessed); + } +} diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/OperatorTestCase.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/OperatorTestCase.java index 68a2bde0c2f6c..f8b53a9bcd3c0 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/OperatorTestCase.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/OperatorTestCase.java @@ -284,6 +284,8 @@ public static void runDriver(List drivers) { drivers.add( new Driver( "dummy-session", + 0, + 0, new DriverContext(BigArrays.NON_RECYCLING_INSTANCE, TestBlockFactory.getNonBreakingInstance()), () -> "dummy-driver", new SequenceLongBlockSourceOperator( diff --git a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/exchange/ExchangeServiceTests.java b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/exchange/ExchangeServiceTests.java index f45bda077da05..bdaa045633dc0 100644 --- a/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/exchange/ExchangeServiceTests.java +++ b/x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/exchange/ExchangeServiceTests.java @@ -275,6 +275,8 @@ void runConcurrentTest( DriverContext dc = driverContext(); Driver d = new Driver( "test-session:1", + 0, + 0, dc, () -> description, seqNoGenerator.get(dc), @@ -291,6 +293,8 @@ void runConcurrentTest( DriverContext dc = driverContext(); Driver d = new Driver( "test-session:2", + 0, + 0, dc, () -> description, sourceOperator, diff --git a/x-pack/plugin/esql/qa/server/multi-node/src/yamlRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlClientYamlIT.java b/x-pack/plugin/esql/qa/server/multi-node/src/yamlRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlClientYamlIT.java index 5a615def1186f..a90cce0a566e7 100644 --- a/x-pack/plugin/esql/qa/server/multi-node/src/yamlRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlClientYamlIT.java +++ b/x-pack/plugin/esql/qa/server/multi-node/src/yamlRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlClientYamlIT.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.esql.qa.mixed; +package org.elasticsearch.xpack.esql.qa.multi_node; import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/EnrichLookupService.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/EnrichLookupService.java index c0a3149dafb4c..8e5db20d7c849 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/EnrichLookupService.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/EnrichLookupService.java @@ -332,6 +332,8 @@ private void doLookup( OutputOperator outputOperator = new OutputOperator(List.of(), Function.identity(), result::set); Driver driver = new Driver( "enrich-lookup:" + sessionId, + System.currentTimeMillis(), + System.nanoTime(), driverContext, () -> lookupDescription(sessionId, shardId, matchType, matchField, extractFields, inputPage.getPositionCount()), queryOperator, diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java index 8f4dd902a44e4..d70b0c3c0846e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java @@ -716,6 +716,8 @@ public Driver apply(String sessionId) { success = true; return new Driver( sessionId, + System.currentTimeMillis(), + System.nanoTime(), driverContext, physicalOperation::describe, source, diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java index b747026dcbfb1..64f393ccdf2b0 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/ComputeService.java @@ -426,7 +426,7 @@ void runCompute(CancellableTask task, ComputeContext context, PhysicalPlan plan, } ActionListener listenerCollectingStatus = listener.map(ignored -> { if (context.configuration.profile()) { - return drivers.stream().map(d -> new DriverProfile(d.status().completedOperators())).toList(); + return drivers.stream().map(Driver::profile).toList(); } return null; }); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java index 14ebf3da2cd7e..80667b855ccb1 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java @@ -25,7 +25,9 @@ import org.elasticsearch.compute.lucene.LuceneOperator; import org.elasticsearch.compute.lucene.ValuesSourceReaderOperator; import org.elasticsearch.compute.operator.AbstractPageMappingOperator; +import org.elasticsearch.compute.operator.AggregationOperator; import org.elasticsearch.compute.operator.DriverStatus; +import org.elasticsearch.compute.operator.HashAggregationOperator; import org.elasticsearch.compute.operator.LimitOperator; import org.elasticsearch.compute.operator.MvExpandOperator; import org.elasticsearch.compute.operator.exchange.ExchangeService; @@ -163,8 +165,10 @@ public List getNamedWriteables() { List.of( DriverStatus.ENTRY, AbstractPageMappingOperator.Status.ENTRY, + AggregationOperator.Status.ENTRY, ExchangeSinkOperator.Status.ENTRY, ExchangeSourceOperator.Status.ENTRY, + HashAggregationOperator.Status.ENTRY, LimitOperator.Status.ENTRY, LuceneOperator.Status.ENTRY, TopNOperatorStatus.ENTRY, diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponseProfileTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponseProfileTests.java index af8f6dcd550c4..782e1fb4333d8 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponseProfileTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponseProfileTests.java @@ -47,12 +47,19 @@ private List randomDriverProfiles() { } private DriverProfile randomDriverProfile() { - return new DriverProfile(randomList(10, this::randomOperatorStatus)); + return new DriverProfile( + randomNonNegativeLong(), + randomNonNegativeLong(), + randomNonNegativeLong(), + randomList(10, this::randomOperatorStatus) + ); } private DriverStatus.OperatorStatus randomOperatorStatus() { String name = randomAlphaOfLength(4); - Operator.Status status = randomBoolean() ? null : new AbstractPageMappingOperator.Status(between(0, Integer.MAX_VALUE)); + Operator.Status status = randomBoolean() + ? null + : new AbstractPageMappingOperator.Status(randomNonNegativeLong(), between(0, Integer.MAX_VALUE)); return new DriverStatus.OperatorStatus(name, status); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponseTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponseTests.java index 3b64870a15839..839e9c323bf74 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponseTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/action/EsqlQueryResponseTests.java @@ -458,15 +458,54 @@ public void testProfileXContent() { List.of(new ColumnInfo("foo", "integer")), List.of(new Page(blockFactory.newIntArrayVector(new int[] { 40, 80 }, 2).asBlock())), new EsqlQueryResponse.Profile( - List.of(new DriverProfile(List.of(new DriverStatus.OperatorStatus("asdf", new AbstractPageMappingOperator.Status(10))))) + List.of( + new DriverProfile( + 20021, + 20000, + 12, + List.of(new DriverStatus.OperatorStatus("asdf", new AbstractPageMappingOperator.Status(10021, 10))) + ) + ) ), false, false ); ) { - assertThat(Strings.toString(response), equalTo(""" - {"columns":[{"name":"foo","type":"integer"}],"values":[[40],[80]],"profile":{"drivers":[""" + """ - {"operators":[{"operator":"asdf","status":{"pages_processed":10}}]}]}}""")); + assertThat(Strings.toString(response, true, false), equalTo(""" + { + "columns" : [ + { + "name" : "foo", + "type" : "integer" + } + ], + "values" : [ + [ + 40 + ], + [ + 80 + ] + ], + "profile" : { + "drivers" : [ + { + "took_nanos" : 20021, + "cpu_nanos" : 20000, + "iterations" : 12, + "operators" : [ + { + "operator" : "asdf", + "status" : { + "process_nanos" : 10021, + "pages_processed" : 10 + } + } + ] + } + ] + } + }""")); } } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/120_profile.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/120_profile.yml new file mode 100644 index 0000000000000..81d87435ad39e --- /dev/null +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/120_profile.yml @@ -0,0 +1,143 @@ +--- +setup: + - skip: + version: " - 8.11.99" + reason: "profile option added in 8.12" + features: warnings + - do: + indices.create: + index: test + body: + settings: + number_of_shards: 1 + mappings: + properties: + data: + type: long + data_d: + type: double + count: + type: long + count_d: + type: double + time: + type: long + color: + type: keyword + text: + type: text + + - do: + cluster.health: # older versions of ESQL don't wait for the nodes to become available. + wait_for_no_initializing_shards: true + wait_for_events: languid + + - do: + bulk: + index: "test" + refresh: true + body: + - { "index": { } } + - { "data": 1, "count": 40, "data_d": 1, "count_d": 40, "time": 1674835275187, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 2, "count": 42, "data_d": 2, "count_d": 42, "time": 1674835275188, "color": "blue", "text": "bb blue" } + - { "index": { } } + - { "data": 1, "count": 44, "data_d": 1, "count_d": 44, "time": 1674835275189, "color": "green", "text": "gg green" } + - { "index": { } } + - { "data": 2, "count": 46, "data_d": 2, "count_d": 46, "time": 1674835275190, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 1, "count": 40, "data_d": 1, "count_d": 40, "time": 1674835275191, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 2, "count": 42, "data_d": 2, "count_d": 42, "time": 1674835275192, "color": "blue", "text": "bb blue" } + - { "index": { } } + - { "data": 1, "count": 44, "data_d": 1, "count_d": 44, "time": 1674835275193, "color": "green", "text": "gg green" } + - { "index": { } } + - { "data": 2, "count": 46, "data_d": 2, "count_d": 46, "time": 1674835275194, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 1, "count": 40, "data_d": 1, "count_d": 40, "time": 1674835275195, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 2, "count": 42, "data_d": 2, "count_d": 42, "time": 1674835275196, "color": "blue", "text": "bb blue" } + - { "index": { } } + - { "data": 1, "count": 44, "data_d": 1, "count_d": 44, "time": 1674835275197, "color": "green", "text": "gg green" } + - { "index": { } } + - { "data": 2, "count": 46, "data_d": 2, "count_d": 46, "time": 1674835275198, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 1, "count": 40, "data_d": 1, "count_d": 40, "time": 1674835275199, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 2, "count": 42, "data_d": 2, "count_d": 42, "time": 1674835275200, "color": "blue", "text": "bb blue" } + - { "index": { } } + - { "data": 1, "count": 44, "data_d": 1, "count_d": 44, "time": 1674835275201, "color": "green", "text": "gg green" } + - { "index": { } } + - { "data": 2, "count": 46, "data_d": 2, "count_d": 46, "time": 1674835275202, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 1, "count": 40, "data_d": 1, "count_d": 40, "time": 1674835275203, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 2, "count": 42, "data_d": 2, "count_d": 42, "time": 1674835275204, "color": "blue", "text": "bb blue" } + - { "index": { } } + - { "data": 1, "count": 44, "data_d": 1, "count_d": 44, "time": 1674835275205, "color": "green", "text": "gg green" } + - { "index": { } } + - { "data": 2, "count": 46, "data_d": 2, "count_d": 46, "time": 1674835275206, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 1, "count": 40, "data_d": 1, "count_d": 40, "time": 1674835275207, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 2, "count": 42, "data_d": 2, "count_d": 42, "time": 1674835275208, "color": "blue", "text": "bb blue" } + - { "index": { } } + - { "data": 1, "count": 44, "data_d": 1, "count_d": 44, "time": 1674835275209, "color": "green", "text": "gg green" } + - { "index": { } } + - { "data": 2, "count": 46, "data_d": 2, "count_d": 46, "time": 1674835275210, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 1, "count": 40, "data_d": 1, "count_d": 40, "time": 1674835275211, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 2, "count": 42, "data_d": 2, "count_d": 42, "time": 1674835275212, "color": "blue", "text": "bb blue" } + - { "index": { } } + - { "data": 1, "count": 44, "data_d": 1, "count_d": 44, "time": 1674835275213, "color": "green", "text": "gg green" } + - { "index": { } } + - { "data": 2, "count": 46, "data_d": 2, "count_d": 46, "time": 1674835275214, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 1, "count": 40, "data_d": 1, "count_d": 40, "time": 1674835275215, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 2, "count": 42, "data_d": 2, "count_d": 42, "time": 1674835275216, "color": "blue", "text": "bb blue" } + - { "index": { } } + - { "data": 1, "count": 44, "data_d": 1, "count_d": 44, "time": 1674835275217, "color": "green", "text": "gg green" } + - { "index": { } } + - { "data": 2, "count": 46, "data_d": 2, "count_d": 46, "time": 1674835275218, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 1, "count": 40, "data_d": 1, "count_d": 40, "time": 1674835275219, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 2, "count": 42, "data_d": 2, "count_d": 42, "time": 1674835275220, "color": "blue", "text": "bb blue" } + - { "index": { } } + - { "data": 1, "count": 44, "data_d": 1, "count_d": 44, "time": 1674835275221, "color": "green", "text": "gg green" } + - { "index": { } } + - { "data": 2, "count": 46, "data_d": 2, "count_d": 46, "time": 1674835275222, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 1, "count": 40, "data_d": 1, "count_d": 40, "time": 1674835275223, "color": "red", "text": "rr red" } + - { "index": { } } + - { "data": 2, "count": 42, "data_d": 2, "count_d": 42, "time": 1674835275224, "color": "blue", "text": "bb blue" } + - { "index": { } } + - { "data": 1, "count": 44, "data_d": 1, "count_d": 44, "time": 1674835275225, "color": "green", "text": "gg green" } + - { "index": { } } + - { "data": 2, "count": 46, "data_d": 2, "count_d": 46, "time": 1674835275226, "color": "red", "text": "rr red" } + +--- +avg 8.14 or after: + - skip: + features: ["node_selector"] + + - do: + node_selector: + version: "8.13.99 - " + esql.query: + body: + query: 'FROM test | STATS AVG(data) | LIMIT 1' + columnar: true + profile: true + + - match: {columns.0.name: "AVG(data)"} + - match: {columns.0.type: "double"} + - match: {values.0.0: 1.5} + - match: {profile.drivers.0.operators.0.operator: /ExchangeSourceOperator|LuceneSourceOperator.+/} + - gte: {profile.drivers.0.took_nanos: 0} + - gte: {profile.drivers.0.cpu_nanos: 0} + - gte: {profile.drivers.1.took_nanos: 0} + - gte: {profile.drivers.1.cpu_nanos: 0} +# It's hard to assert much about these because they don't come back in any particular order. From 73a170bd4d9d868d18cc1ed8a9a2e0ef68ad7b06 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 27 Feb 2024 07:17:55 -0500 Subject: [PATCH 193/250] ESQL: Use faster field caps (#105067) The field capabilities has an internal-only option to produce un-merged output. This expands that option to be available to any caller inside of Elasticsearch and uses it in ES|QL to speed up queries across many indices with many fields. Across 5,000 indices with a couple thousand fields each (metricbeat) the `FROM *` query went from 600ms to 60ms. Across 50,000 indices that went from 6600ms to 600ms. 600ms is still too slow for such a simple query, but one step at a time! This is faster because field capabilities wants to present a field-centric result but ES|QL actually needs a different flavor of field-centric result with some differences smoothed away. If we take over the merging process we can use a few tools that the field caps API uses internally to be fast - mostly the sha256 of the mapping - to save on doing work that wasn't available in the other view. Also, two merges is more expensive than one. That 90% reduction in runtime doesn't banish field caps from the flamegraphs. You still see it, but it's now much less prominent. And you don't see the merging process at all. Now it's all data-node side operations field caps. Relates to #103369 --- docs/changelog/105067.yaml | 5 + .../FieldCapabilitiesIndexResponse.java | 4 +- .../fieldcaps/FieldCapabilitiesRequest.java | 4 +- .../fieldcaps/FieldCapabilitiesResponse.java | 4 +- .../xpack/esql/qa/mixed/FieldExtractorIT.java | 26 + .../xpack/esql/qa/multi_node/Clusters.java | 22 + .../xpack/esql/qa/multi_node/EsqlSpecIT.java | 8 +- .../esql/qa/multi_node/FieldExtractorIT.java | 26 + .../esql/qa/single_node/FieldExtractorIT.java | 26 + .../xpack/esql/qa/single_node/RestEsqlIT.java | 3 +- .../esql/qa/rest/FieldExtractorTestCase.java | 1456 +++++++++++++++++ .../xpack/esql/execution/PlanExecutor.java | 6 +- .../xpack/esql/plugin/EsqlPlugin.java | 4 +- .../xpack/esql/session/EsqlIndexResolver.java | 252 +++ .../xpack/esql/session/EsqlSession.java | 154 +- .../xpack/esql/analysis/AnalyzerTests.java | 15 +- .../esql/stats/PlanExecutorMetricsTests.java | 55 +- .../esql/type/EsqlDataTypeRegistryTests.java | 44 +- .../resources/empty_field_caps_response.json | 16 - .../unsignedlong/UnsignedLongFieldMapper.java | 2 +- 20 files changed, 2037 insertions(+), 95 deletions(-) create mode 100644 docs/changelog/105067.yaml create mode 100644 x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.java create mode 100644 x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/Clusters.java create mode 100644 x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/FieldExtractorIT.java create mode 100644 x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/FieldExtractorIT.java create mode 100644 x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java create mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlIndexResolver.java delete mode 100644 x-pack/plugin/esql/src/test/resources/empty_field_caps_response.json diff --git a/docs/changelog/105067.yaml b/docs/changelog/105067.yaml new file mode 100644 index 0000000000000..562e8271f5502 --- /dev/null +++ b/docs/changelog/105067.yaml @@ -0,0 +1,5 @@ +pr: 105067 +summary: "ESQL: Use faster field caps" +area: ES|QL +type: enhancement +issues: [] diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexResponse.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexResponse.java index 722808af879d6..cc72dd80dceac 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexResponse.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexResponse.java @@ -24,7 +24,7 @@ import java.util.Map; import java.util.Objects; -final class FieldCapabilitiesIndexResponse implements Writeable { +public final class FieldCapabilitiesIndexResponse implements Writeable { private static final TransportVersion MAPPING_HASH_VERSION = TransportVersions.V_8_2_0; private final String indexName; @@ -34,7 +34,7 @@ final class FieldCapabilitiesIndexResponse implements Writeable { private final boolean canMatch; private final transient TransportVersion originVersion; - FieldCapabilitiesIndexResponse( + public FieldCapabilitiesIndexResponse( String indexName, @Nullable String indexMappingHash, Map responseMap, diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java index 3a9d403ffb565..4b1c256bdeb71 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesRequest.java @@ -75,7 +75,7 @@ public FieldCapabilitiesRequest() {} *

    * Note that when using the high-level REST client, results are always merged (this flag is always considered 'true'). */ - boolean isMergeResults() { + public boolean isMergeResults() { return mergeResults; } @@ -85,7 +85,7 @@ boolean isMergeResults() { *

    * Note that when using the high-level REST client, results are always merged (this flag is always considered 'true'). */ - void setMergeResults(boolean mergeResults) { + public void setMergeResults(boolean mergeResults) { this.mergeResults = mergeResults; } diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java index 84388864166dc..4946f6ca7835d 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponse.java @@ -57,7 +57,7 @@ public FieldCapabilitiesResponse(String[] indices, Map indexResponses, List failures) { + public FieldCapabilitiesResponse(List indexResponses, List failures) { this(Strings.EMPTY_ARRAY, Collections.emptyMap(), indexResponses, failures); } @@ -117,7 +117,7 @@ public List getFailures() { /** * Returns the actual per-index field caps responses */ - List getIndexResponses() { + public List getIndexResponses() { return indexResponses; } diff --git a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.java b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.java new file mode 100644 index 0000000000000..8c1e47c29670a --- /dev/null +++ b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.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.qa.mixed; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; + +import org.elasticsearch.test.TestClustersThreadFilter; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.xpack.esql.qa.rest.FieldExtractorTestCase; +import org.junit.ClassRule; + +@ThreadLeakFilters(filters = TestClustersThreadFilter.class) +public class FieldExtractorIT extends FieldExtractorTestCase { + @ClassRule + public static ElasticsearchCluster cluster = Clusters.mixedVersionCluster(); + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } +} diff --git a/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/Clusters.java b/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/Clusters.java new file mode 100644 index 0000000000000..4aa17801fa217 --- /dev/null +++ b/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/Clusters.java @@ -0,0 +1,22 @@ +/* + * 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.qa.multi_node; + +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.test.cluster.local.distribution.DistributionType; + +public class Clusters { + public static ElasticsearchCluster testCluster() { + return ElasticsearchCluster.local() + .distribution(DistributionType.DEFAULT) + .nodes(2) + .setting("xpack.security.enabled", "false") + .setting("xpack.license.self_generated.type", "trial") + .build(); + } +} diff --git a/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlSpecIT.java b/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlSpecIT.java index d73f66ab00107..67b916a815819 100644 --- a/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlSpecIT.java +++ b/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/EsqlSpecIT.java @@ -8,19 +8,13 @@ package org.elasticsearch.xpack.esql.qa.multi_node; import org.elasticsearch.test.cluster.ElasticsearchCluster; -import org.elasticsearch.test.cluster.local.distribution.DistributionType; import org.elasticsearch.xpack.esql.qa.rest.EsqlSpecTestCase; import org.elasticsearch.xpack.ql.CsvSpecReader.CsvTestCase; import org.junit.ClassRule; public class EsqlSpecIT extends EsqlSpecTestCase { @ClassRule - public static ElasticsearchCluster cluster = ElasticsearchCluster.local() - .distribution(DistributionType.DEFAULT) - .nodes(2) - .setting("xpack.security.enabled", "false") - .setting("xpack.license.self_generated.type", "trial") - .build(); + public static ElasticsearchCluster cluster = Clusters.testCluster(); @Override protected String getTestRestCluster() { diff --git a/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/FieldExtractorIT.java b/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/FieldExtractorIT.java new file mode 100644 index 0000000000000..bcb83a31f7641 --- /dev/null +++ b/x-pack/plugin/esql/qa/server/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/multi_node/FieldExtractorIT.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.qa.multi_node; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; + +import org.elasticsearch.test.TestClustersThreadFilter; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.xpack.esql.qa.rest.FieldExtractorTestCase; +import org.junit.ClassRule; + +@ThreadLeakFilters(filters = TestClustersThreadFilter.class) +public class FieldExtractorIT extends FieldExtractorTestCase { + @ClassRule + public static ElasticsearchCluster cluster = Clusters.testCluster(); + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } +} diff --git a/x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/FieldExtractorIT.java b/x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/FieldExtractorIT.java new file mode 100644 index 0000000000000..695db7ddf4c3d --- /dev/null +++ b/x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/FieldExtractorIT.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.qa.single_node; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; + +import org.elasticsearch.test.TestClustersThreadFilter; +import org.elasticsearch.test.cluster.ElasticsearchCluster; +import org.elasticsearch.xpack.esql.qa.rest.FieldExtractorTestCase; +import org.junit.ClassRule; + +@ThreadLeakFilters(filters = TestClustersThreadFilter.class) +public class FieldExtractorIT extends FieldExtractorTestCase { + @ClassRule + public static ElasticsearchCluster cluster = Clusters.testCluster(); + + @Override + protected String getTestRestCluster() { + return cluster.getHttpAddresses(); + } +} diff --git a/x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/RestEsqlIT.java b/x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/RestEsqlIT.java index c7727d40d25f2..6743657e86874 100644 --- a/x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/RestEsqlIT.java +++ b/x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/RestEsqlIT.java @@ -185,8 +185,7 @@ public void testIncompatibleMappingsErrors() throws IOException { assertException("from test_alias | where _size is not null | limit 1", "Unknown column [_size]"); assertException( "from test_alias | where message.hash is not null | limit 1", - "Cannot use field [message.hash] due to ambiguities", - "incompatible types: [integer] in [index2], [murmur3] in [index1]" + "Cannot use field [message.hash] with unsupported type [murmur3]" ); assertException( "from index1 | where message.hash is not null | limit 1", diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java new file mode 100644 index 0000000000000..3f8caa3bdf5d4 --- /dev/null +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java @@ -0,0 +1,1456 @@ +/* + * 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.qa.rest; + +import org.apache.http.util.EntityUtils; +import org.elasticsearch.Version; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.network.NetworkAddress; +import org.elasticsearch.core.CheckedConsumer; +import org.elasticsearch.geo.GeometryTestUtils; +import org.elasticsearch.index.mapper.BlockLoader; +import org.elasticsearch.index.mapper.SourceFieldMapper; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.ListMatcher; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xcontent.json.JsonXContent; +import org.hamcrest.Matcher; + +import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Function; + +import static org.elasticsearch.test.ListMatcher.matchesList; +import static org.elasticsearch.test.MapMatcher.assertMap; +import static org.elasticsearch.test.MapMatcher.matchesMap; +import static org.elasticsearch.xpack.esql.qa.rest.RestEsqlTestCase.entityToMap; +import static org.elasticsearch.xpack.esql.qa.rest.RestEsqlTestCase.runEsqlSync; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.containsString; + +/** + * Creates indices with many different mappings and fetches values from them to make sure + * we can do it. Think of this as an integration test for {@link BlockLoader} + * implementations and an integration test for field resolution. + * This is a port of a test with the same name on the SQL side. + */ +public abstract class FieldExtractorTestCase extends ESRestTestCase { + private static final Logger logger = LogManager.getLogger(FieldExtractorTestCase.class); + + public void testTextField() throws IOException { + textTest().test(randomAlphaOfLength(20)); + } + + private Test textTest() { + return new Test("text").randomStoreUnlessSynthetic(); + } + + public void testKeywordField() throws IOException { + Integer ignoreAbove = randomBoolean() ? null : between(10, 50); + int length = between(10, 50); + + String value = randomAlphaOfLength(length); + keywordTest().ignoreAbove(ignoreAbove).test(value, ignoredByIgnoreAbove(ignoreAbove, length) ? null : value); + } + + private Test keywordTest() { + return new Test("keyword").randomDocValuesAndStoreUnlessSynthetic(); + } + + public void testConstantKeywordField() throws IOException { + boolean specifyInMapping = randomBoolean(); + boolean specifyInDocument = randomBoolean(); + + String value = randomAlphaOfLength(20); + new Test("constant_keyword").expectedType("keyword") + .value(specifyInMapping ? value : null) + .test(specifyInDocument ? value : null, specifyInMapping || specifyInDocument ? value : null); + } + + public void testWildcardField() throws IOException { + Integer ignoreAbove = randomBoolean() ? null : between(10, 50); + int length = between(10, 50); + + String value = randomAlphaOfLength(length); + new Test("wildcard").expectedType("keyword") + .ignoreAbove(ignoreAbove) + .test(value, ignoredByIgnoreAbove(ignoreAbove, length) ? null : value); + } + + public void testLong() throws IOException { + long value = randomLong(); + longTest().test(randomBoolean() ? Long.toString(value) : value, value); + } + + public void testLongWithDecimalParts() throws IOException { + long value = randomLong(); + int decimalPart = between(1, 99); + BigDecimal withDecimals = new BigDecimal(value + "." + decimalPart); + /* + * It's possible to pass the BigDecimal here without converting to a string + * but that rounds in a different way, and I'm not quite able to reproduce it + * at the time. + */ + longTest().test(withDecimals.toString(), value); + } + + public void testLongMalformed() throws IOException { + longTest().forceIgnoreMalformed().test(randomAlphaOfLength(5), null); + } + + private Test longTest() { + return new Test("long").randomIgnoreMalformedUnlessSynthetic().randomDocValuesUnlessSynthetic(); + } + + public void testInt() throws IOException { + int value = randomInt(); + intTest().test(randomBoolean() ? Integer.toString(value) : value, value); + } + + public void testIntWithDecimalParts() throws IOException { + double value = randomDoubleBetween(Integer.MIN_VALUE, Integer.MAX_VALUE, true); + intTest().test(randomBoolean() ? Double.toString(value) : value, (int) value); + } + + public void testIntMalformed() throws IOException { + intTest().forceIgnoreMalformed().test(randomAlphaOfLength(5), null); + } + + private Test intTest() { + return new Test("integer").randomIgnoreMalformedUnlessSynthetic().randomDocValuesUnlessSynthetic(); + } + + public void testShort() throws IOException { + short value = randomShort(); + shortTest().test(randomBoolean() ? Short.toString(value) : value, (int) value); + } + + public void testShortWithDecimalParts() throws IOException { + double value = randomDoubleBetween(Short.MIN_VALUE, Short.MAX_VALUE, true); + shortTest().test(randomBoolean() ? Double.toString(value) : value, (int) value); + } + + public void testShortMalformed() throws IOException { + shortTest().forceIgnoreMalformed().test(randomAlphaOfLength(5), null); + } + + private Test shortTest() { + return new Test("short").expectedType("integer").randomIgnoreMalformedUnlessSynthetic().randomDocValuesUnlessSynthetic(); + } + + public void testByte() throws IOException { + byte value = randomByte(); + byteTest().test(Byte.toString(value), (int) value); + } + + public void testByteWithDecimalParts() throws IOException { + double value = randomDoubleBetween(Byte.MIN_VALUE, Byte.MAX_VALUE, true); + byteTest().test(randomBoolean() ? Double.toString(value) : value, (int) value); + } + + public void testByteMalformed() throws IOException { + byteTest().forceIgnoreMalformed().test(randomAlphaOfLength(5), null); + } + + private Test byteTest() { + return new Test("byte").expectedType("integer").randomIgnoreMalformedUnlessSynthetic().randomDocValuesUnlessSynthetic(); + } + + public void testUnsignedLong() throws IOException { + assumeTrue( + "order of fields in error message inconsistent before 8.14", + getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0)) + ); + BigInteger value = randomUnsignedLong(); + new Test("unsigned_long").randomIgnoreMalformedUnlessSynthetic() + .randomDocValuesUnlessSynthetic() + .test( + randomBoolean() ? value.toString() : value, + value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) <= 0 ? value.longValue() : value + ); + } + + public void testUnsignedLongMalformed() throws IOException { + new Test("unsigned_long").forceIgnoreMalformed().randomDocValuesUnlessSynthetic().test(randomAlphaOfLength(5), null); + } + + public void testDouble() throws IOException { + double value = randomDouble(); + new Test("double").randomIgnoreMalformedUnlessSynthetic() + .randomDocValuesUnlessSynthetic() + .test(randomBoolean() ? Double.toString(value) : value, value); + } + + public void testFloat() throws IOException { + float value = randomFloat(); + new Test("float").expectedType("double") + .randomIgnoreMalformedUnlessSynthetic() + .randomDocValuesUnlessSynthetic() + .test(randomBoolean() ? Float.toString(value) : value, (double) value); + } + + public void testScaledFloat() throws IOException { + double value = randomBoolean() ? randomDoubleBetween(-Double.MAX_VALUE, Double.MAX_VALUE, true) : randomFloat(); + double scalingFactor = randomDoubleBetween(0, Double.MAX_VALUE, false); + new Test("scaled_float").expectedType("double") + .randomIgnoreMalformedUnlessSynthetic() + .randomDocValuesUnlessSynthetic() + .scalingFactor(scalingFactor) + .test(randomBoolean() ? Double.toString(value) : value, scaledFloatMatcher(scalingFactor, value)); + } + + private Matcher scaledFloatMatcher(double scalingFactor, double d) { + long encoded = Math.round(d * scalingFactor); + double decoded = encoded / scalingFactor; + return closeTo(decoded, Math.ulp(decoded)); + } + + public void testBoolean() throws IOException { + boolean value = randomBoolean(); + new Test("boolean").ignoreMalformed(randomBoolean()) + .randomDocValuesUnlessSynthetic() + .test(randomBoolean() ? Boolean.toString(value) : value, value); + } + + public void testIp() throws IOException { + ipTest().test(NetworkAddress.format(randomIp(randomBoolean()))); + } + + private Test ipTest() { + return new Test("ip").ignoreMalformed(randomBoolean()); + } + + public void testVersionField() throws IOException { + new Test("version").test(randomVersionString()); + } + + public void testGeoPoint() throws IOException { + assumeTrue( + "not supported until 8.13", + getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_13_0)) + ); + new Test("geo_point") + // TODO we should support loading geo_point from doc values if source isn't enabled + .sourceMode(randomValueOtherThanMany(s -> s.stored() == false, () -> randomFrom(SourceMode.values()))) + .ignoreMalformed(randomBoolean()) + .storeAndDocValues(randomBoolean(), randomBoolean()) + .test(GeometryTestUtils.randomPoint(false).toString()); + } + + public void testGeoShape() throws IOException { + assumeTrue( + "not supported until 8.13", + getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_13_0)) + ); + new Test("geo_shape") + // TODO if source isn't enabled how can we load *something*? It's just triangles, right? + .sourceMode(randomValueOtherThanMany(s -> s.stored() == false, () -> randomFrom(SourceMode.values()))) + .ignoreMalformed(randomBoolean()) + .storeAndDocValues(randomBoolean(), randomBoolean()) + // TODO pick supported random shapes + .test(GeometryTestUtils.randomPoint(false).toString()); + } + + public void testAliasToKeyword() throws IOException { + keywordTest().createAlias().test(randomAlphaOfLength(20)); + } + + public void testAliasToText() throws IOException { + textTest().createAlias().test(randomAlphaOfLength(20)); + } + + public void testAliasToInt() throws IOException { + intTest().createAlias().test(randomInt()); + } + + public void testFlattenedUnsupported() throws IOException { + new Test("flattened").createIndex("test", "flattened"); + index("test", """ + {"flattened": {"a": "foo"}}"""); + Map result = runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | LIMIT 2")); + + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("flattened", "unsupported"))) + .entry("values", List.of(matchesList().item(null))) + ); + } + + public void testEmptyMapping() throws IOException { + createIndex("test", index -> {}); + index("test", """ + {}"""); + + ResponseException e = expectThrows( + ResponseException.class, + () -> runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | SORT missing | LIMIT 3")) + ); + String err = EntityUtils.toString(e.getResponse().getEntity()); + assertThat(err, containsString("Unknown column [missing]")); + + // TODO this is broken in main too + // Map result = runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | LIMIT 2")); + // assertMap( + // result, + // matchesMap().entry("columns", List.of(columnInfo("f", "unsupported"), columnInfo("f.raw", "unsupported"))) + // .entry("values", List.of(matchesList().item(null).item(null))) + // ); + } + + /** + *

    +     * "text_field": {
    +     *   "type": "text",
    +     *   "fields": {
    +     *     "raw": {
    +     *       "type": "keyword",
    +     *       "ignore_above": 10
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testTextFieldWithKeywordSubfield() throws IOException { + String value = randomAlphaOfLength(20); + Map result = new Test("text").storeAndDocValues(randomBoolean(), null).sub("raw", keywordTest()).roundTrip(value); + + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("text_field", "text"), columnInfo("text_field.raw", "keyword"))) + .entry("values", List.of(matchesList().item(value).item(value))) + ); + } + + /** + *
    +     * "text_field": {
    +     *   "type": "text",
    +     *   "fields": {
    +     *     "int": {
    +     *       "type": "integer",
    +     *       "ignore_malformed": true/false
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testTextFieldWithIntegerSubfield() throws IOException { + int value = randomInt(); + Map result = textTest().sub("int", intTest()).roundTrip(value); + + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("text_field", "text"), columnInfo("text_field.int", "integer"))) + .entry("values", List.of(matchesList().item(Integer.toString(value)).item(value))) + ); + } + + /** + *
    +     * "text_field": {
    +     *   "type": "text",
    +     *   "fields": {
    +     *     "int": {
    +     *       "type": "integer",
    +     *       "ignore_malformed": true
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testTextFieldWithIntegerSubfieldMalformed() throws IOException { + String value = randomAlphaOfLength(5); + Map result = textTest().sourceMode(SourceMode.DEFAULT).sub("int", intTest().ignoreMalformed(true)).roundTrip(value); + + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("text_field", "text"), columnInfo("text_field.int", "integer"))) + .entry("values", List.of(matchesList().item(value).item(null))) + ); + } + + /** + *
    +     * "text_field": {
    +     *   "type": "text",
    +     *   "fields": {
    +     *     "ip": {
    +     *       "type": "ip",
    +     *       "ignore_malformed": true/false
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testTextFieldWithIpSubfield() throws IOException { + String value = NetworkAddress.format(randomIp(randomBoolean())); + Map result = textTest().sub("ip", ipTest()).roundTrip(value); + + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("text_field", "text"), columnInfo("text_field.ip", "ip"))) + .entry("values", List.of(matchesList().item(value).item(value))) + ); + } + + /** + *
    +     * "text_field": {
    +     *   "type": "text",
    +     *   "fields": {
    +     *     "ip": {
    +     *       "type": "ip",
    +     *       "ignore_malformed": true
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testTextFieldWithIpSubfieldMalformed() throws IOException { + String value = randomAlphaOfLength(10); + Map result = textTest().sourceMode(SourceMode.DEFAULT).sub("ip", ipTest().ignoreMalformed(true)).roundTrip(value); + + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("text_field", "text"), columnInfo("text_field.ip", "ip"))) + .entry("values", List.of(matchesList().item(value).item(null))) + ); + } + + /** + *
    +     * "integer_field": {
    +     *   "type": "integer",
    +     *   "ignore_malformed": true/false,
    +     *   "fields": {
    +     *     "str": {
    +     *       "type": "text/keyword"
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testIntFieldWithTextOrKeywordSubfield() throws IOException { + int value = randomInt(); + boolean text = randomBoolean(); + Map result = intTest().sub("str", text ? textTest() : keywordTest()).roundTrip(value); + + assertMap( + result, + matchesMap().entry( + "columns", + List.of(columnInfo("integer_field", "integer"), columnInfo("integer_field.str", text ? "text" : "keyword")) + ).entry("values", List.of(matchesList().item(value).item(Integer.toString(value)))) + ); + } + + /** + *
    +     * "integer_field": {
    +     *   "type": "integer",
    +     *   "ignore_malformed": true,
    +     *   "fields": {
    +     *     "str": {
    +     *       "type": "text/keyword"
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testIntFieldWithTextOrKeywordSubfieldMalformed() throws IOException { + String value = randomAlphaOfLength(5); + boolean text = randomBoolean(); + Map result = intTest().forceIgnoreMalformed().sub("str", text ? textTest() : keywordTest()).roundTrip(value); + + assertMap( + result, + matchesMap().entry( + "columns", + List.of(columnInfo("integer_field", "integer"), columnInfo("integer_field.str", text ? "text" : "keyword")) + ).entry("values", List.of(matchesList().item(null).item(value))) + ); + } + + /** + *
    +     * "ip_field": {
    +     *   "type": "ip",
    +     *   "ignore_malformed": true/false,
    +     *   "fields": {
    +     *     "str": {
    +     *       "type": "text/keyword"
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testIpFieldWithTextOrKeywordSubfield() throws IOException { + String value = NetworkAddress.format(randomIp(randomBoolean())); + boolean text = randomBoolean(); + Map result = ipTest().sub("str", text ? textTest() : keywordTest()).roundTrip(value); + + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("ip_field", "ip"), columnInfo("ip_field.str", text ? "text" : "keyword"))) + .entry("values", List.of(matchesList().item(value).item(value))) + ); + } + + /** + *
    +     * "ip_field": {
    +     *   "type": "ip",
    +     *   "ignore_malformed": true,
    +     *   "fields": {
    +     *     "str": {
    +     *       "type": "text/keyword"
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testIpFieldWithTextOrKeywordSubfieldMalformed() throws IOException { + String value = randomAlphaOfLength(5); + boolean text = randomBoolean(); + Map result = ipTest().forceIgnoreMalformed().sub("str", text ? textTest() : keywordTest()).roundTrip(value); + + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("ip_field", "ip"), columnInfo("ip_field.str", text ? "text" : "keyword"))) + .entry("values", List.of(matchesList().item(null).item(value))) + ); + } + + /** + *
    +     * "integer_field": {
    +     *   "type": "ip",
    +     *   "ignore_malformed": true/false,
    +     *   "fields": {
    +     *     "byte": {
    +     *       "type": "byte",
    +     *       "ignore_malformed": true/false
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testIntFieldWithByteSubfield() throws IOException { + byte value = randomByte(); + Map result = intTest().sub("byte", byteTest()).roundTrip(value); + + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("integer_field", "integer"), columnInfo("integer_field.byte", "integer"))) + .entry("values", List.of(matchesList().item((int) value).item((int) value))) + ); + } + + /** + *
    +     * "integer_field": {
    +     *   "type": "integer",
    +     *   "ignore_malformed": true/false,
    +     *   "fields": {
    +     *     "byte": {
    +     *       "type": "byte",
    +     *       "ignore_malformed": true
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testIntFieldWithByteSubfieldTooBig() throws IOException { + int value = randomValueOtherThanMany((Integer v) -> (Byte.MIN_VALUE <= v) && (v <= Byte.MAX_VALUE), ESTestCase::randomInt); + Map result = intTest().sourceMode(SourceMode.DEFAULT) + .sub("byte", byteTest().ignoreMalformed(true)) + .roundTrip(value); + + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("integer_field", "integer"), columnInfo("integer_field.byte", "integer"))) + .entry("values", List.of(matchesList().item(value).item(null))) + ); + } + + /** + *
    +     * "byte_field": {
    +     *   "type": "byte",
    +     *   "ignore_malformed": true/false,
    +     *   "fields": {
    +     *     "int": {
    +     *       "type": "int",
    +     *       "ignore_malformed": true/false
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testByteFieldWithIntSubfield() throws IOException { + byte value = randomByte(); + Map result = byteTest().sub("int", intTest()).roundTrip(value); + + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("byte_field", "integer"), columnInfo("byte_field.int", "integer"))) + .entry("values", List.of(matchesList().item((int) value).item((int) value))) + ); + } + + /** + *
    +     * "byte_field": {
    +     *   "type": "byte",
    +     *   "ignore_malformed": true,
    +     *   "fields": {
    +     *     "int": {
    +     *       "type": "int",
    +     *       "ignore_malformed": true/false
    +     *     }
    +     *   }
    +     * }
    +     * 
    + */ + public void testByteFieldWithIntSubfieldTooBig() throws IOException { + int value = randomValueOtherThanMany((Integer v) -> (Byte.MIN_VALUE <= v) && (v <= Byte.MAX_VALUE), ESTestCase::randomInt); + Map result = byteTest().forceIgnoreMalformed().sub("int", intTest()).roundTrip(value); + + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("byte_field", "integer"), columnInfo("byte_field.int", "integer"))) + .entry("values", List.of(matchesList().item(null).item(value))) + ); + } + + /** + * Two indices, one with: + *
    +     * "f": {
    +     *     "type": "keyword"
    +     * }
    +     * 
    + * and the other with + *
    +     * "f": {
    +     *     "type": "long"
    +     * }
    +     * 
    . + */ + public void testIncompatibleTypes() throws IOException { + keywordTest().createIndex("test1", "f"); + index("test1", """ + {"f": "f1"}"""); + longTest().createIndex("test2", "f"); + index("test2", """ + {"f": 1}"""); + + Map result = runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test*")); + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("f", "unsupported"))) + .entry("values", List.of(matchesList().item(null), matchesList().item(null))) + ); + ResponseException e = expectThrows( + ResponseException.class, + () -> runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | SORT f | LIMIT 3")) + ); + String err = EntityUtils.toString(e.getResponse().getEntity()); + assertThat( + deyaml(err), + containsString( + "Cannot use field [f] due to ambiguities being mapped as [2] incompatible types: [keyword] in [test1], [long] in [test2]" + ) + ); + } + + /** + * Two indices, one with: + *
    +     * "file": {
    +     *     "type": "keyword"
    +     * }
    +     * 
    + * and the other with + *
    +     * "other_file": {
    +     *     "type": "keyword"
    +     * }
    +     * 
    . + */ + public void testDistinctInEachIndex() throws IOException { + keywordTest().createIndex("test1", "file"); + index("test1", """ + {"file": "f1"}"""); + keywordTest().createIndex("test2", "other"); + index("test2", """ + {"other": "o2"}"""); + + Map result = runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | SORT file, other")); + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("file", "keyword"), columnInfo("other", "keyword"))) + .entry("values", List.of(matchesList().item("f1").item(null), matchesList().item(null).item("o2"))) + ); + } + + /** + * Two indices, one with: + *
    +     * "file": {
    +     *    "type": "keyword"
    +     * }
    +     * 
    + * and the other with + *
    +     * "file": {
    +     *    "type": "object",
    +     *    "properties": {
    +     *       "raw": {
    +     *          "type": "keyword"
    +     *       }
    +     *    }
    +     * }
    +     * 
    . + */ + public void testMergeKeywordAndObject() throws IOException { + assumeTrue( + "order of fields in error message inconsistent before 8.14", + getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0)) + ); + keywordTest().createIndex("test1", "file"); + index("test1", """ + {"file": "f1"}"""); + createIndex("test2", index -> { + index.startObject("properties"); + { + index.startObject("file"); + { + index.field("type", "object"); + index.startObject("properties"); + { + index.startObject("raw").field("type", "keyword").endObject(); + } + index.endObject(); + } + index.endObject(); + } + index.endObject(); + }); + index("test2", """ + {"file": {"raw": "o2"}}"""); + + ResponseException e = expectThrows( + ResponseException.class, + () -> runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | SORT file, file.raw | LIMIT 3")) + ); + String err = EntityUtils.toString(e.getResponse().getEntity()); + assertThat( + deyaml(err), + containsString( + "Cannot use field [file] due to ambiguities" + + " being mapped as [2] incompatible types: [keyword] in [test1], [object] in [test2]" + ) + ); + + Map result = runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | SORT file.raw | LIMIT 2")); + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("file", "unsupported"), columnInfo("file.raw", "keyword"))) + .entry("values", List.of(matchesList().item(null).item("o2"), matchesList().item(null).item(null))) + ); + } + + /** + * One index with an unsupported field and a supported sub-field. The supported sub-field + * is marked as unsupported because the parent is unsupported. Mapping: + *
    +     * "f": {
    +     *    "type": "ip_range"  ----- The type here doesn't matter, but it has to be one we don't support
    +     *    "fields": {
    +     *       "raw": {
    +     *          "type": "keyword"
    +     *       }
    +     *    }
    +     * }
    +     * 
    . + */ + public void testPropagateUnsupportedToSubFields() throws IOException { + createIndex("test", index -> { + index.startObject("properties"); + index.startObject("f"); + { + index.field("type", "ip_range"); + index.startObject("fields"); + { + index.startObject("raw").field("type", "keyword").endObject(); + } + index.endObject(); + } + index.endObject(); + index.endObject(); + }); + index("test", """ + {"f": "192.168.0.1/24"}"""); + + ResponseException e = expectThrows( + ResponseException.class, + () -> runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | SORT f, f.raw | LIMIT 3")) + ); + String err = EntityUtils.toString(e.getResponse().getEntity()); + assertThat(err, containsString("Cannot use field [f] with unsupported type [ip_range]")); + assertThat(err, containsString("Cannot use field [f.raw] with unsupported type [ip_range]")); + + Map result = runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | LIMIT 2")); + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("f", "unsupported"), columnInfo("f.raw", "unsupported"))) + .entry("values", List.of(matchesList().item(null).item(null))) + ); + } + + /** + * Two indices, one with: + *
    +     * "f": {
    +     *    "type": "ip_range"  ----- The type here doesn't matter, but it has to be one we don't support
    +     * }
    +     * 
    + * and the other with + *
    +     * "f": {
    +     *    "type": "object",
    +     *    "properties": {
    +     *       "raw": {
    +     *          "type": "keyword"
    +     *       }
    +     *    }
    +     * }
    +     * 
    . + */ + public void testMergeUnsupportedAndObject() throws IOException { + assumeTrue( + "order of fields in error message inconsistent before 8.14", + getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0)) + ); + createIndex("test1", index -> { + index.startObject("properties"); + index.startObject("f").field("type", "ip_range").endObject(); + index.endObject(); + }); + index("test1", """ + {"f": "192.168.0.1/24"}"""); + createIndex("test2", index -> { + index.startObject("properties"); + { + index.startObject("f"); + { + index.field("type", "object"); + index.startObject("properties"); + { + index.startObject("raw").field("type", "keyword").endObject(); + } + index.endObject(); + } + index.endObject(); + } + index.endObject(); + }); + index("test2", """ + {"f": {"raw": "o2"}}"""); + + ResponseException e = expectThrows( + ResponseException.class, + () -> runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | SORT f, f.raw | LIMIT 3")) + ); + String err = EntityUtils.toString(e.getResponse().getEntity()); + assertThat(err, containsString("Cannot use field [f] with unsupported type [ip_range]")); + assertThat(err, containsString("Cannot use field [f.raw] with unsupported type [ip_range]")); + + Map result = runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | LIMIT 2")); + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("f", "unsupported"), columnInfo("f.raw", "unsupported"))) + .entry("values", List.of(matchesList().item(null).item(null), matchesList().item(null).item(null))) + ); + } + + /** + * Two indices, one with: + *
    +     * "emp_no": {
    +     *     "type": "integer"
    +     * }
    +     * 
    + * and the other with + *
    +     * "emp_no": {
    +     *     "type": "integer",
    +     *     "doc_values": false
    +     * }
    +     * 
    . + */ + public void testIntegerDocValuesConflict() throws IOException { + assumeTrue( + "order of fields in error message inconsistent before 8.14", + getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0)) + ); + intTest().sourceMode(SourceMode.DEFAULT).storeAndDocValues(null, true).createIndex("test1", "emp_no"); + index("test1", """ + {"emp_no": 1}"""); + intTest().sourceMode(SourceMode.DEFAULT).storeAndDocValues(null, false).createIndex("test2", "emp_no"); + index("test2", """ + {"emp_no": 2}"""); + + Map result = runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | SORT emp_no | LIMIT 2")); + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("emp_no", "integer"))) + .entry("values", List.of(matchesList().item(1), matchesList().item(2))) + ); + } + + /** + * Two indices, one with: + *
    +     * "emp_no": {
    +     *     "type": "long"
    +     * }
    +     * 
    + * and the other with + *
    +     * "emp_no": {
    +     *     "type": "integer"
    +     * }
    +     * 
    . + * + * In an ideal world we'd promote the {@code integer} to an {@code long} and just go. + */ + public void testLongIntegerConflict() throws IOException { + assumeTrue( + "order of fields in error message inconsistent before 8.14", + getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0)) + ); + longTest().sourceMode(SourceMode.DEFAULT).createIndex("test1", "emp_no"); + index("test1", """ + {"emp_no": 1}"""); + intTest().sourceMode(SourceMode.DEFAULT).createIndex("test2", "emp_no"); + index("test2", """ + {"emp_no": 2}"""); + + ResponseException e = expectThrows( + ResponseException.class, + () -> runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | SORT emp_no | LIMIT 3")) + ); + String err = EntityUtils.toString(e.getResponse().getEntity()); + assertThat( + deyaml(err), + containsString( + "Cannot use field [emp_no] due to ambiguities being " + + "mapped as [2] incompatible types: [integer] in [test2], [long] in [test1]" + ) + ); + + Map result = runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | LIMIT 2")); + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("emp_no", "unsupported"))) + .entry("values", List.of(matchesList().item(null), matchesList().item(null))) + ); + } + + /** + * Two indices, one with: + *
    +     * "emp_no": {
    +     *     "type": "integer"
    +     * }
    +     * 
    + * and the other with + *
    +     * "emp_no": {
    +     *     "type": "short"
    +     * }
    +     * 
    . + * + * In an ideal world we'd promote the {@code short} to an {@code integer} and just go. + */ + public void testIntegerShortConflict() throws IOException { + assumeTrue( + "order of fields in error message inconsistent before 8.14", + getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0)) + ); + intTest().sourceMode(SourceMode.DEFAULT).createIndex("test1", "emp_no"); + index("test1", """ + {"emp_no": 1}"""); + shortTest().sourceMode(SourceMode.DEFAULT).createIndex("test2", "emp_no"); + index("test2", """ + {"emp_no": 2}"""); + + ResponseException e = expectThrows( + ResponseException.class, + () -> runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | SORT emp_no | LIMIT 3")) + ); + String err = EntityUtils.toString(e.getResponse().getEntity()); + assertThat( + deyaml(err), + containsString( + "Cannot use field [emp_no] due to ambiguities being " + + "mapped as [2] incompatible types: [integer] in [test1], [short] in [test2]" + ) + ); + + Map result = runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | LIMIT 2")); + assertMap( + result, + matchesMap().entry("columns", List.of(columnInfo("emp_no", "unsupported"))) + .entry("values", List.of(matchesList().item(null), matchesList().item(null))) + ); + } + + /** + * Two indices, one with: + *
    +     * "foo": {
    +     *   "type": "object",
    +     *   "properties": {
    +     *     "emp_no": {
    +     *       "type": "integer"
    +     *     }
    +     * }
    +     * 
    + * and the other with + *
    +     * "foo": {
    +     *   "type": "object",
    +     *   "properties": {
    +     *     "emp_no": {
    +     *       "type": "keyword"
    +     *     }
    +     * }
    +     * 
    . + */ + public void testTypeConflictInObject() throws IOException { + assumeTrue( + "order of fields in error message inconsistent before 8.14", + getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_14_0)) + ); + createIndex("test1", empNoInObject("integer")); + index("test1", """ + {"foo": {"emp_no": 1}}"""); + createIndex("test2", empNoInObject("keyword")); + index("test2", """ + {"foo": {"emp_no": "cat"}}"""); + + Map result = runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | LIMIT 3")); + assertMap(result, matchesMap().entry("columns", List.of(columnInfo("foo.emp_no", "unsupported"))).extraOk()); + + ResponseException e = expectThrows( + ResponseException.class, + () -> runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | SORT foo.emp_no | LIMIT 3")) + ); + String err = EntityUtils.toString(e.getResponse().getEntity()); + assertThat( + deyaml(err), + containsString( + "Cannot use field [foo.emp_no] due to ambiguities being " + + "mapped as [2] incompatible types: [integer] in [test1], [keyword] in [test2]" + ) + ); + } + + private CheckedConsumer empNoInObject(String empNoType) { + return index -> { + index.startObject("properties"); + { + index.startObject("foo"); + { + index.field("type", "object"); + index.startObject("properties"); + { + index.startObject("emp_no").field("type", empNoType).endObject(); + } + index.endObject(); + } + index.endObject(); + } + index.endObject(); + }; + } + + private enum SourceMode { + DEFAULT { + @Override + void sourceMapping(XContentBuilder builder) {} + + @Override + boolean stored() { + return true; + } + }, + STORED { + @Override + void sourceMapping(XContentBuilder builder) throws IOException { + builder.startObject(SourceFieldMapper.NAME).field("mode", "stored").endObject(); + } + + @Override + boolean stored() { + return true; + } + }, + /* TODO add support to this test for disabling _source + DISABLED { + @Override + void sourceMapping(XContentBuilder builder) throws IOException { + builder.startObject(SourceFieldMapper.NAME).field("mode", "disabled").endObject(); + } + + @Override + boolean stored() { + return false; + } + }, + */ + SYNTHETIC { + @Override + void sourceMapping(XContentBuilder builder) throws IOException { + builder.startObject(SourceFieldMapper.NAME).field("mode", "synthetic").endObject(); + } + + @Override + boolean stored() { + return false; + } + }; + + abstract void sourceMapping(XContentBuilder builder) throws IOException; + + abstract boolean stored(); + } + + private boolean ignoredByIgnoreAbove(Integer ignoreAbove, int length) { + return ignoreAbove != null && length > ignoreAbove; + } + + private BigInteger randomUnsignedLong() { + BigInteger big = BigInteger.valueOf(randomNonNegativeLong()).shiftLeft(1); + return big.add(randomBoolean() ? BigInteger.ONE : BigInteger.ZERO); + } + + private static String randomVersionString() { + return randomVersionNumber() + (randomBoolean() ? "" : randomPrerelease()); + } + + private static String randomVersionNumber() { + int numbers = between(1, 3); + String v = Integer.toString(between(0, 100)); + for (int i = 1; i < numbers; i++) { + v += "." + between(0, 100); + } + return v; + } + + private static String randomPrerelease() { + if (rarely()) { + return randomFrom("alpha", "beta", "prerelease", "whatever"); + } + return randomFrom("alpha", "beta", "") + randomVersionNumber(); + } + + private record StoreAndDocValues(Boolean store, Boolean docValues) {} + + private static class Test { + private final String type; + private final Map subFields = new TreeMap<>(); + + private SourceMode sourceMode; + private String expectedType; + private Function ignoreMalformed; + private Function storeAndDocValues = s -> new StoreAndDocValues(null, null); + private Double scalingFactor; + private Integer ignoreAbove; + private Object value; + private boolean createAlias; + + Test(String type) { + this.type = type; + // Default the expected return type to the field type. + this.expectedType = type; + } + + Test sourceMode(SourceMode sourceMode) { + this.sourceMode = sourceMode; + return this; + } + + Test expectedType(String expectedType) { + this.expectedType = expectedType; + return this; + } + + Test ignoreMalformed(boolean ignoreMalformed) { + this.ignoreMalformed = s -> ignoreMalformed; + return this; + } + + /** + * Enable {@code ignore_malformed} and disable synthetic _source because + * most fields don't support ignore_malformed and synthetic _source. + */ + Test forceIgnoreMalformed() { + return this.sourceMode(randomValueOtherThan(SourceMode.SYNTHETIC, () -> randomFrom(SourceMode.values()))).ignoreMalformed(true); + } + + Test randomIgnoreMalformedUnlessSynthetic() { + this.ignoreMalformed = s -> s == SourceMode.SYNTHETIC ? false : randomBoolean(); + return this; + } + + Test storeAndDocValues(Boolean store, Boolean docValues) { + this.storeAndDocValues = s -> new StoreAndDocValues(store, docValues); + return this; + } + + Test randomStoreUnlessSynthetic() { + this.storeAndDocValues = s -> new StoreAndDocValues(s == SourceMode.SYNTHETIC ? true : randomBoolean(), null); + return this; + } + + Test randomDocValuesAndStoreUnlessSynthetic() { + this.storeAndDocValues = s -> { + if (s == SourceMode.SYNTHETIC) { + boolean store = randomBoolean(); + return new StoreAndDocValues(store, store == false || randomBoolean()); + } + return new StoreAndDocValues(randomBoolean(), randomBoolean()); + }; + return this; + } + + Test randomDocValuesUnlessSynthetic() { + this.storeAndDocValues = s -> new StoreAndDocValues(null, s == SourceMode.SYNTHETIC || randomBoolean()); + return this; + } + + Test scalingFactor(double scalingFactor) { + this.scalingFactor = scalingFactor; + return this; + } + + Test ignoreAbove(Integer ignoreAbove) { + this.ignoreAbove = ignoreAbove; + return this; + } + + Test value(Object value) { + this.value = value; + return this; + } + + Test createAlias() { + this.createAlias = true; + return this; + } + + Test sub(String name, Test sub) { + this.subFields.put(name, sub); + return this; + } + + Map roundTrip(Object value) throws IOException { + String fieldName = type + "_field"; + createIndex("test", fieldName); + if (randomBoolean()) { + createIndex("test2", fieldName); + } + + if (value == null) { + logger.info("indexing empty doc"); + index("test", "{}"); + } else { + logger.info("indexing {}::{}", value, value.getClass().getName()); + index("test", Strings.toString(JsonXContent.contentBuilder().startObject().field(fieldName, value).endObject())); + } + + return fetchAll(); + } + + void test(Object value) throws IOException { + test(value, value); + } + + /** + * Round trip the value through and index configured by the parameters + * of this test and assert that it matches the {@code expectedValues} + * which can be either the expected value or a subclass of {@link Matcher}. + */ + void test(Object value, Object expectedValue) throws IOException { + Map result = roundTrip(value); + + logger.info("expecting {}", expectedValue == null ? null : expectedValue + "::" + expectedValue.getClass().getName()); + + List> columns = new ArrayList<>(); + columns.add(columnInfo(type + "_field", expectedType)); + if (createAlias) { + columns.add(columnInfo("a.b.c." + type + "_field_alias", expectedType)); + columns.add(columnInfo(type + "_field_alias", expectedType)); + } + Collections.sort(columns, Comparator.comparing(m -> (String) m.get("name"))); + + ListMatcher values = matchesList(); + values = values.item(expectedValue); + if (createAlias) { + values = values.item(expectedValue); + values = values.item(expectedValue); + } + + assertMap(result, matchesMap().entry("columns", columns).entry("values", List.of(values))); + } + + void createIndex(String name, String fieldName) throws IOException { + if (sourceMode == null) { + sourceMode(randomFrom(SourceMode.values())); + } + logger.info("source_mode: {}", sourceMode); + + FieldExtractorTestCase.createIndex(name, index -> { + sourceMode.sourceMapping(index); + index.startObject("properties"); + { + index.startObject(fieldName); + fieldMapping(index); + index.endObject(); + + if (createAlias) { + // create two aliases - one within a hierarchy, the other just a simple field w/o hierarchy + index.startObject(fieldName + "_alias"); + { + index.field("type", "alias"); + index.field("path", fieldName); + } + index.endObject(); + index.startObject("a.b.c." + fieldName + "_alias"); + { + index.field("type", "alias"); + index.field("path", fieldName); + } + index.endObject(); + } + } + index.endObject(); + }); + } + + private void fieldMapping(XContentBuilder builder) throws IOException { + builder.field("type", type); + if (ignoreMalformed != null) { + boolean v = ignoreMalformed.apply(sourceMode); + builder.field("ignore_malformed", v); + ignoreMalformed = m -> v; + } + StoreAndDocValues sd = storeAndDocValues.apply(sourceMode); + storeAndDocValues = m -> sd; + if (sd.docValues != null) { + builder.field("doc_values", sd.docValues); + } + if (sd.store != null) { + builder.field("store", sd.store); + } + if (scalingFactor != null) { + builder.field("scaling_factor", scalingFactor); + } + if (ignoreAbove != null) { + builder.field("ignore_above", ignoreAbove); + } + if (value != null) { + builder.field("value", value); + } + + if (subFields.isEmpty() == false) { + builder.startObject("fields"); + for (Map.Entry sub : subFields.entrySet()) { + builder.startObject(sub.getKey()); + if (sub.getValue().sourceMode == null) { + sub.getValue().sourceMode = sourceMode; + } else if (sub.getValue().sourceMode != sourceMode) { + throw new IllegalStateException("source_mode can't be configured on sub-fields"); + } + sub.getValue().fieldMapping(builder); + builder.endObject(); + } + builder.endObject(); + } + } + + private Map fetchAll() throws IOException { + return runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query("FROM test* | LIMIT 10")); + } + } + + private static Map columnInfo(String name, String type) { + return Map.of("name", name, "type", type); + } + + private static void index(String name, String... docs) throws IOException { + Request request = new Request("POST", "/" + name + "/_bulk"); + request.addParameter("refresh", "true"); + StringBuilder bulk = new StringBuilder(); + for (String doc : docs) { + bulk.append(String.format(Locale.ROOT, """ + {"index":{}} + %s + """, doc)); + } + request.setJsonEntity(bulk.toString()); + Response response = client().performRequest(request); + Map result = entityToMap(response.getEntity(), XContentType.JSON); + assertMap(result, matchesMap().extraOk().entry("errors", false)); + } + + private static void createIndex(String name, CheckedConsumer mapping) throws IOException { + Request request = new Request("PUT", "/" + name); + XContentBuilder index = JsonXContent.contentBuilder().prettyPrint().startObject(); + index.startObject("settings"); + { + index.field("index.number_of_replicas", 0); + index.field("index.number_of_shards", 1); + } + index.endObject(); + index.startObject("mappings"); + mapping.accept(index); + index.endObject(); + index.endObject(); + String configStr = Strings.toString(index); + logger.info("index: {} {}", name, configStr); + request.setJsonEntity(configStr); + client().performRequest(request); + } + + /** + * Yaml adds newlines and some indentation which we don't want to match. + */ + private String deyaml(String err) { + return err.replaceAll("\\\\\n\s+\\\\", ""); + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/execution/PlanExecutor.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/execution/PlanExecutor.java index e90510461551f..a07c963dc0844 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/execution/PlanExecutor.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/execution/PlanExecutor.java @@ -18,6 +18,7 @@ import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan; import org.elasticsearch.xpack.esql.planner.Mapper; import org.elasticsearch.xpack.esql.session.EsqlConfiguration; +import org.elasticsearch.xpack.esql.session.EsqlIndexResolver; import org.elasticsearch.xpack.esql.session.EsqlSession; import org.elasticsearch.xpack.esql.stats.Metrics; import org.elasticsearch.xpack.esql.stats.QueryMetric; @@ -29,14 +30,16 @@ public class PlanExecutor { private final IndexResolver indexResolver; + private final EsqlIndexResolver esqlIndexResolver; private final PreAnalyzer preAnalyzer; private final FunctionRegistry functionRegistry; private final Mapper mapper; private final Metrics metrics; private final Verifier verifier; - public PlanExecutor(IndexResolver indexResolver) { + public PlanExecutor(IndexResolver indexResolver, EsqlIndexResolver esqlIndexResolver) { this.indexResolver = indexResolver; + this.esqlIndexResolver = esqlIndexResolver; this.preAnalyzer = new PreAnalyzer(); this.functionRegistry = new EsqlFunctionRegistry(); this.mapper = new Mapper(functionRegistry); @@ -55,6 +58,7 @@ public void esql( sessionId, cfg, indexResolver, + esqlIndexResolver, enrichPolicyResolver, preAnalyzer, functionRegistry, diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java index 80667b855ccb1..d1bcac4e399e5 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java @@ -54,6 +54,7 @@ import org.elasticsearch.xpack.esql.action.RestEsqlQueryAction; import org.elasticsearch.xpack.esql.execution.PlanExecutor; import org.elasticsearch.xpack.esql.querydsl.query.SingleValueQuery; +import org.elasticsearch.xpack.esql.session.EsqlIndexResolver; import org.elasticsearch.xpack.esql.type.EsqlDataTypeRegistry; import org.elasticsearch.xpack.ql.index.IndexResolver; @@ -106,7 +107,8 @@ public Collection createComponents(PluginServices services) { services.clusterService().getClusterName().value(), EsqlDataTypeRegistry.INSTANCE, Set::of - ) + ), + new EsqlIndexResolver(services.client(), EsqlDataTypeRegistry.INSTANCE) ), new ExchangeService( services.clusterService().getSettings(), diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlIndexResolver.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlIndexResolver.java new file mode 100644 index 0000000000000..b573de7cc3435 --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlIndexResolver.java @@ -0,0 +1,252 @@ +/* + * 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.action.ActionListener; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesIndexResponse; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesRequest; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; +import org.elasticsearch.action.fieldcaps.IndexFieldCapabilities; +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.common.Strings; +import org.elasticsearch.index.mapper.TimeSeriesParams; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.ql.index.EsIndex; +import org.elasticsearch.xpack.ql.index.IndexResolution; +import org.elasticsearch.xpack.ql.index.IndexResolver; +import org.elasticsearch.xpack.ql.type.DataType; +import org.elasticsearch.xpack.ql.type.DataTypeRegistry; +import org.elasticsearch.xpack.ql.type.DateEsField; +import org.elasticsearch.xpack.ql.type.EsField; +import org.elasticsearch.xpack.ql.type.InvalidMappedField; +import org.elasticsearch.xpack.ql.type.KeywordEsField; +import org.elasticsearch.xpack.ql.type.TextEsField; +import org.elasticsearch.xpack.ql.type.UnsupportedEsField; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import static org.elasticsearch.xpack.ql.type.DataTypes.DATETIME; +import static org.elasticsearch.xpack.ql.type.DataTypes.KEYWORD; +import static org.elasticsearch.xpack.ql.type.DataTypes.OBJECT; +import static org.elasticsearch.xpack.ql.type.DataTypes.TEXT; +import static org.elasticsearch.xpack.ql.type.DataTypes.UNSUPPORTED; + +public class EsqlIndexResolver { + private final Client client; + private final DataTypeRegistry typeRegistry; + + public EsqlIndexResolver(Client client, DataTypeRegistry typeRegistry) { + this.client = client; + this.typeRegistry = typeRegistry; + } + + /** + * Resolves a pattern to one (potentially compound meaning that spawns multiple indices) mapping. + */ + public void resolveAsMergedMapping(String indexWildcard, Set fieldNames, ActionListener listener) { + client.fieldCaps( + createFieldCapsRequest(indexWildcard, fieldNames), + listener.delegateFailureAndWrap((l, response) -> l.onResponse(mergedMappings(indexWildcard, response))) + ); + } + + public IndexResolution mergedMappings(String indexPattern, FieldCapabilitiesResponse fieldCapsResponse) { + assert ThreadPool.assertCurrentThreadPool(ThreadPool.Names.SEARCH_COORDINATION); // too expensive to run this on a transport worker + if (fieldCapsResponse.getIndexResponses().isEmpty()) { + return IndexResolution.notFound(indexPattern); + } + + Map> fieldsCaps = collectFieldCaps(fieldCapsResponse); + + // Build hierarchical fields - it's easier to do it in sorted order so the object fields come first. + // TODO flattened is simpler - could we get away with that? + String[] names = fieldsCaps.keySet().toArray(new String[0]); + Arrays.sort(names); + Map rootFields = new HashMap<>(); + for (String name : names) { + Map fields = rootFields; + String fullName = name; + boolean isAlias = false; + UnsupportedEsField firstUnsupportedParent = null; + while (true) { + int nextDot = name.indexOf('.'); + if (nextDot < 0) { + break; + } + String parent = name.substring(0, nextDot); + EsField obj = fields.get(parent); + if (obj == null) { + obj = new EsField(parent, OBJECT, new HashMap<>(), false, true); + isAlias = true; + fields.put(parent, obj); + } else if (firstUnsupportedParent == null && obj instanceof UnsupportedEsField unsupportedParent) { + firstUnsupportedParent = unsupportedParent; + } + fields = obj.getProperties(); + name = name.substring(nextDot + 1); + } + // TODO we're careful to make isAlias match IndexResolver - but do we use it? + EsField field = firstUnsupportedParent == null + ? createField(fieldCapsResponse, name, fullName, fieldsCaps.get(fullName), isAlias) + : new UnsupportedEsField( + fullName, + firstUnsupportedParent.getOriginalType(), + firstUnsupportedParent.getName(), + new HashMap<>() + ); + fields.put(name, field); + } + + boolean allEmpty = true; + for (FieldCapabilitiesIndexResponse ir : fieldCapsResponse.getIndexResponses()) { + allEmpty &= ir.get().isEmpty(); + } + if (allEmpty) { + // If all the mappings are empty we return an empty set of resolved indices to line up with QL + return IndexResolution.valid(new EsIndex(indexPattern, rootFields, Set.of())); + } + + Set concreteIndices = new HashSet<>(fieldCapsResponse.getIndexResponses().size()); + for (FieldCapabilitiesIndexResponse ir : fieldCapsResponse.getIndexResponses()) { + concreteIndices.add(ir.getIndexName()); + } + return IndexResolution.valid(new EsIndex(indexPattern, rootFields, concreteIndices)); + } + + private static Map> collectFieldCaps(FieldCapabilitiesResponse fieldCapsResponse) { + Set seenHashes = new HashSet<>(); + Map> fieldsCaps = new HashMap<>(); + for (FieldCapabilitiesIndexResponse response : fieldCapsResponse.getIndexResponses()) { + if (seenHashes.add(response.getIndexMappingHash()) == false) { + continue; + } + for (IndexFieldCapabilities fc : response.get().values()) { + if (fc.isMetadatafield()) { + // ESQL builds the metadata fields if they are asked for without using the resolution. + continue; + } + List all = fieldsCaps.computeIfAbsent(fc.name(), (_key) -> new ArrayList<>()); + all.add(fc); + } + } + return fieldsCaps; + } + + private EsField createField( + FieldCapabilitiesResponse fieldCapsResponse, + String name, + String fullName, + List fcs, + boolean isAlias + ) { + IndexFieldCapabilities first = fcs.get(0); + List rest = fcs.subList(1, fcs.size()); + DataType type = typeRegistry.fromEs(first.type(), first.metricType()); + boolean aggregatable = first.isAggregatable(); + if (rest.isEmpty() == false) { + for (IndexFieldCapabilities fc : rest) { + if (first.metricType() != fc.metricType()) { + return conflictingMetricTypes(name, fullName, fieldCapsResponse); + } + } + for (IndexFieldCapabilities fc : rest) { + if (type != typeRegistry.fromEs(fc.type(), fc.metricType())) { + return conflictingTypes(name, fullName, fieldCapsResponse); + } + } + for (IndexFieldCapabilities fc : rest) { + aggregatable &= fc.isAggregatable(); + } + } + + // TODO I think we only care about unmapped fields if we're aggregating on them. do we even then? + + if (type == TEXT) { + return new TextEsField(name, new HashMap<>(), false, isAlias); + } + if (type == KEYWORD) { + int length = Short.MAX_VALUE; + // TODO: to check whether isSearchable/isAggregateable takes into account the presence of the normalizer + boolean normalized = false; + return new KeywordEsField(name, new HashMap<>(), aggregatable, length, normalized, isAlias); + } + if (type == DATETIME) { + return DateEsField.dateEsField(name, new HashMap<>(), aggregatable); + } + if (type == UNSUPPORTED) { + return unsupported(name, first); + } + + return new EsField(name, type, new HashMap<>(), aggregatable, isAlias); + } + + private UnsupportedEsField unsupported(String name, IndexFieldCapabilities fc) { + String originalType = fc.metricType() == TimeSeriesParams.MetricType.COUNTER ? "counter" : fc.type(); + return new UnsupportedEsField(name, originalType); + } + + private EsField conflictingTypes(String name, String fullName, FieldCapabilitiesResponse fieldCapsResponse) { + Map> typesToIndices = new TreeMap<>(); + for (FieldCapabilitiesIndexResponse ir : fieldCapsResponse.getIndexResponses()) { + IndexFieldCapabilities fc = ir.get().get(fullName); + if (fc != null) { + DataType type = typeRegistry.fromEs(fc.type(), fc.metricType()); + if (type == UNSUPPORTED) { + return unsupported(name, fc); + } + typesToIndices.computeIfAbsent(type.esType(), _key -> new TreeSet<>()).add(ir.getIndexName()); + } + } + StringBuilder errorMessage = new StringBuilder(); + errorMessage.append("mapped as ["); + errorMessage.append(typesToIndices.size()); + errorMessage.append("] incompatible types: "); + boolean first = true; + for (Map.Entry> e : typesToIndices.entrySet()) { + if (first) { + first = false; + } else { + errorMessage.append(", "); + } + errorMessage.append("["); + errorMessage.append(e.getKey()); + errorMessage.append("] in "); + errorMessage.append(e.getValue()); + } + return new InvalidMappedField(name, errorMessage.toString()); + } + + private EsField conflictingMetricTypes(String name, String fullName, FieldCapabilitiesResponse fieldCapsResponse) { + TreeSet indices = new TreeSet<>(); + for (FieldCapabilitiesIndexResponse ir : fieldCapsResponse.getIndexResponses()) { + IndexFieldCapabilities fc = ir.get().get(fullName); + if (fc != null) { + indices.add(ir.getIndexName()); + } + } + return new InvalidMappedField(name, "mapped as different metric types in indices: " + indices); + } + + private static FieldCapabilitiesRequest createFieldCapsRequest(String index, Set fieldNames) { + FieldCapabilitiesRequest req = new FieldCapabilitiesRequest().indices(Strings.commaDelimitedListToStringArray(index)); + req.fields(fieldNames.toArray(String[]::new)); + req.includeUnmapped(true); + // lenient because we throw our own errors looking at the response e.g. if something was not resolved + // also because this way security doesn't throw authorization exceptions but rather honors ignore_unavailable + req.indicesOptions(IndexResolver.FIELD_CAPS_INDICES_OPTIONS); + req.setMergeResults(false); + return req; + } +} 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 fa573c7731c13..683460243ecbd 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 @@ -11,6 +11,7 @@ import org.elasticsearch.action.fieldcaps.FieldCapabilities; import org.elasticsearch.common.Strings; import org.elasticsearch.common.regex.Regex; +import org.elasticsearch.core.Assertions; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; @@ -51,11 +52,14 @@ import org.elasticsearch.xpack.ql.plan.logical.Aggregate; import org.elasticsearch.xpack.ql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.ql.plan.logical.Project; +import org.elasticsearch.xpack.ql.type.DataTypes; +import org.elasticsearch.xpack.ql.type.EsField; import org.elasticsearch.xpack.ql.type.InvalidMappedField; import org.elasticsearch.xpack.ql.util.Holder; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -75,6 +79,7 @@ public class EsqlSession { private final String sessionId; private final EsqlConfiguration configuration; private final IndexResolver indexResolver; + private final EsqlIndexResolver esqlIndexResolver; private final EnrichPolicyResolver enrichPolicyResolver; private final PreAnalyzer preAnalyzer; @@ -89,6 +94,7 @@ public EsqlSession( String sessionId, EsqlConfiguration configuration, IndexResolver indexResolver, + EsqlIndexResolver esqlIndexResolver, EnrichPolicyResolver enrichPolicyResolver, PreAnalyzer preAnalyzer, FunctionRegistry functionRegistry, @@ -99,6 +105,7 @@ public EsqlSession( this.sessionId = sessionId; this.configuration = configuration; this.indexResolver = indexResolver; + this.esqlIndexResolver = esqlIndexResolver; this.enrichPolicyResolver = enrichPolicyResolver; this.preAnalyzer = preAnalyzer; this.verifier = verifier; @@ -201,18 +208,11 @@ private void preAnalyzeIndices(LogicalPlan parsed, ActionListener void preAnalyzeIndices(LogicalPlan parsed, ActionListener fieldNames, + ActionListener listener + ) { + indexResolver.resolveAsMergedMapping(indexWildcard, fieldNames, false, Map.of(), new ActionListener<>() { + @Override + public void onResponse(IndexResolution fromQl) { + esqlIndexResolver.resolveAsMergedMapping(indexWildcard, fieldNames, new ActionListener<>() { + @Override + public void onResponse(IndexResolution fromEsql) { + if (fromQl.isValid() == false) { + if (fromEsql.isValid()) { + throw new IllegalArgumentException( + "ql and esql didn't make the same resolution: validity differs " + fromQl + " != " + fromEsql + ); + } + } else { + assertSameMappings("", fromQl.get().mapping(), fromEsql.get().mapping()); + if (fromQl.get().concreteIndices().equals(fromEsql.get().concreteIndices()) == false) { + throw new IllegalArgumentException( + "ql and esql didn't make the same resolution: concrete indices differ " + + fromQl.get().concreteIndices() + + " != " + + fromEsql.get().concreteIndices() + ); + } + } + listener.onResponse(fromEsql); + } + + private void assertSameMappings(String prefix, Map fromQl, Map fromEsql) { + List qlFields = new ArrayList<>(); + qlFields.addAll(fromQl.keySet()); + Collections.sort(qlFields); + + List esqlFields = new ArrayList<>(); + esqlFields.addAll(fromEsql.keySet()); + Collections.sort(esqlFields); + if (qlFields.equals(esqlFields) == false) { + throw new IllegalArgumentException( + prefix + ": ql and esql didn't make the same resolution: fields differ \n" + qlFields + " !=\n" + esqlFields + ); + } + + for (int f = 0; f < qlFields.size(); f++) { + String name = qlFields.get(f); + EsField qlField = fromQl.get(name); + EsField esqlField = fromEsql.get(name); + + if (qlField.getProperties().isEmpty() == false || esqlField.getProperties().isEmpty() == false) { + assertSameMappings( + prefix.equals("") ? name : prefix + "." + name, + qlField.getProperties(), + esqlField.getProperties() + ); + } + + /* + * Check that the field itself is the same, skipping isAlias because + * we don't actually use it in ESQL and the EsqlIndexResolver doesn't + * produce exactly the same result. + */ + if (qlField.getDataType().equals(DataTypes.UNSUPPORTED) == false + && qlField.getName().equals(esqlField.getName()) == false + // QL uses full paths for unsupported fields. ESQL does not. This particular difference is fine. + ) { + throw new IllegalArgumentException( + prefix + + "." + + name + + ": ql and esql didn't make the same resolution: names differ [" + + qlField.getName() + + "] != [" + + esqlField.getName() + + "]" + ); + } + if (qlField.getDataType() != esqlField.getDataType()) { + throw new IllegalArgumentException( + prefix + + "." + + name + + ": ql and esql didn't make the same resolution: types differ [" + + qlField.getDataType() + + "] != [" + + esqlField.getDataType() + + "]" + ); + } + if (qlField.isAggregatable() != esqlField.isAggregatable()) { + throw new IllegalArgumentException( + prefix + + "." + + name + + ": ql and esql didn't make the same resolution: aggregability differ [" + + qlField.isAggregatable() + + "] != [" + + esqlField.isAggregatable() + + "]" + ); + } + } + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }, + EsqlSession::specificValidity, + IndexResolver.PRESERVE_PROPERTIES, + // TODO no matter what metadata fields are asked in a query, the "allowedMetadataFields" is always _index, does it make + // sense to reflect the actual list of metadata fields instead? + IndexResolver.INDEX_METADATA_FIELD + ); + } + static Set fieldNames(LogicalPlan parsed, Set enrichPolicyMatchFields) { if (false == parsed.anyMatch(plan -> plan instanceof Aggregate || plan instanceof Project)) { // no explicit columns selection, for example "from employees" 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 b9c0e9b34b552..0d406d19d3d16 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 @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.analysis; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesIndexResponse; import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.Streams; @@ -28,7 +29,7 @@ import org.elasticsearch.xpack.esql.plan.logical.Row; import org.elasticsearch.xpack.esql.plan.logical.local.EsqlProject; import org.elasticsearch.xpack.esql.plugin.EsqlPlugin; -import org.elasticsearch.xpack.esql.session.EsqlSession; +import org.elasticsearch.xpack.esql.session.EsqlIndexResolver; import org.elasticsearch.xpack.esql.type.EsqlDataTypeRegistry; import org.elasticsearch.xpack.ql.expression.Alias; import org.elasticsearch.xpack.ql.expression.Attribute; @@ -40,7 +41,6 @@ import org.elasticsearch.xpack.ql.expression.UnresolvedAttribute; import org.elasticsearch.xpack.ql.index.EsIndex; import org.elasticsearch.xpack.ql.index.IndexResolution; -import org.elasticsearch.xpack.ql.index.IndexResolver; import org.elasticsearch.xpack.ql.plan.TableIdentifier; import org.elasticsearch.xpack.ql.plan.logical.Aggregate; import org.elasticsearch.xpack.ql.plan.logical.EsRelation; @@ -1767,14 +1767,9 @@ protected List filteredWarnings() { } private static LogicalPlan analyzeWithEmptyFieldCapsResponse(String query) throws IOException { - IndexResolution resolution = IndexResolver.mergedMappings( - EsqlDataTypeRegistry.INSTANCE, - "test*", - readFieldCapsResponse("empty_field_caps_response.json"), - EsqlSession::specificValidity, - IndexResolver.PRESERVE_PROPERTIES, - IndexResolver.INDEX_METADATA_FIELD - ); + List idxResponses = List.of(new FieldCapabilitiesIndexResponse("idx", "idx", Map.of(), true)); + FieldCapabilitiesResponse caps = new FieldCapabilitiesResponse(idxResponses, List.of()); + IndexResolution resolution = new EsqlIndexResolver(null, EsqlDataTypeRegistry.INSTANCE).mergedMappings("test*", caps); var analyzer = analyzer(resolution, TEST_VERIFIER, configuration(query)); return analyze(query, analyzer); } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/stats/PlanExecutorMetricsTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/stats/PlanExecutorMetricsTests.java index 06eae5d57cf16..f90e441b8c308 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/stats/PlanExecutorMetricsTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/stats/PlanExecutorMetricsTests.java @@ -9,7 +9,9 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.fieldcaps.FieldCapabilities; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesIndexResponse; import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; +import org.elasticsearch.action.fieldcaps.IndexFieldCapabilities; import org.elasticsearch.client.internal.Client; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; @@ -21,19 +23,19 @@ import org.elasticsearch.xpack.esql.enrich.EnrichPolicyResolver; import org.elasticsearch.xpack.esql.execution.PlanExecutor; import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan; +import org.elasticsearch.xpack.esql.session.EsqlIndexResolver; import org.elasticsearch.xpack.esql.type.EsqlDataTypeRegistry; import org.elasticsearch.xpack.ql.index.IndexResolver; import org.junit.After; import org.junit.Before; import org.mockito.stubbing.Answer; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonMap; import static org.elasticsearch.xpack.esql.EsqlTestUtils.withDefaultLimitWarning; import static org.hamcrest.Matchers.instanceOf; import static org.mockito.ArgumentMatchers.any; @@ -68,11 +70,10 @@ EnrichPolicyResolver mockEnrichResolver() { } public void testFailedMetric() { - Client client = mock(Client.class); - IndexResolver idxResolver = new IndexResolver(client, randomAlphaOfLength(10), EsqlDataTypeRegistry.INSTANCE, Set::of); - var planExecutor = new PlanExecutor(idxResolver); String[] indices = new String[] { "test" }; - var enrichResolver = mockEnrichResolver(); + + Client qlClient = mock(Client.class); + IndexResolver idxResolver = new IndexResolver(qlClient, randomAlphaOfLength(10), EsqlDataTypeRegistry.INSTANCE, Set::of); // simulate a valid field_caps response so we can parse and correctly analyze de query FieldCapabilitiesResponse fieldCapabilitiesResponse = mock(FieldCapabilitiesResponse.class); when(fieldCapabilitiesResponse.getIndices()).thenReturn(indices); @@ -80,9 +81,23 @@ public void testFailedMetric() { doAnswer((Answer) invocation -> { @SuppressWarnings("unchecked") ActionListener listener = (ActionListener) invocation.getArguments()[1]; + // simulate a valid field_caps response so we can parse and correctly analyze de query listener.onResponse(fieldCapabilitiesResponse); return null; - }).when(client).fieldCaps(any(), any()); + }).when(qlClient).fieldCaps(any(), any()); + + Client esqlClient = mock(Client.class); + EsqlIndexResolver esqlIndexResolver = new EsqlIndexResolver(esqlClient, EsqlDataTypeRegistry.INSTANCE); + doAnswer((Answer) invocation -> { + @SuppressWarnings("unchecked") + ActionListener listener = (ActionListener) invocation.getArguments()[1]; + // simulate a valid field_caps response so we can parse and correctly analyze de query + listener.onResponse(new FieldCapabilitiesResponse(indexFieldCapabilities(indices), List.of())); + return null; + }).when(esqlClient).fieldCaps(any(), any()); + + var planExecutor = new PlanExecutor(idxResolver, esqlIndexResolver); + var enrichResolver = mockEnrichResolver(); var request = new EsqlQueryRequest(); // test a failed query: xyz field doesn't exist @@ -122,12 +137,30 @@ public void onFailure(Exception e) { assertEquals(1, planExecutor.metrics().stats().get("features.stats")); } + private List indexFieldCapabilities(String[] indices) { + List responses = new ArrayList<>(); + for (String idx : indices) { + responses.add( + new FieldCapabilitiesIndexResponse( + idx, + idx, + Map.ofEntries( + Map.entry("foo", new IndexFieldCapabilities("foo", "integer", false, true, true, false, null, Map.of())), + Map.entry("bar", new IndexFieldCapabilities("bar", "long", false, true, true, false, null, Map.of())) + ), + true + ) + ); + } + return responses; + } + private Map> fields(String[] indices) { - FieldCapabilities fooField = new FieldCapabilities("foo", "integer", false, true, true, indices, null, null, emptyMap()); - FieldCapabilities barField = new FieldCapabilities("bar", "long", false, true, true, indices, null, null, emptyMap()); + FieldCapabilities fooField = new FieldCapabilities("foo", "integer", false, true, true, indices, null, null, Map.of()); + FieldCapabilities barField = new FieldCapabilities("bar", "long", false, true, true, indices, null, null, Map.of()); Map> fields = new HashMap<>(); - fields.put(fooField.getName(), singletonMap(fooField.getName(), fooField)); - fields.put(barField.getName(), singletonMap(barField.getName(), barField)); + fields.put(fooField.getName(), Map.of(fooField.getName(), fooField)); + fields.put(barField.getName(), Map.of(barField.getName(), barField)); return fields; } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeRegistryTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeRegistryTests.java index e4fa78fac0dee..93f58398d267f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeRegistryTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeRegistryTests.java @@ -6,17 +6,18 @@ */ package org.elasticsearch.xpack.esql.type; -import org.elasticsearch.action.fieldcaps.FieldCapabilities; +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesIndexResponse; import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; +import org.elasticsearch.action.fieldcaps.IndexFieldCapabilities; import org.elasticsearch.index.mapper.TimeSeriesParams; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.esql.session.EsqlSession; +import org.elasticsearch.xpack.esql.session.EsqlIndexResolver; import org.elasticsearch.xpack.ql.index.IndexResolution; -import org.elasticsearch.xpack.ql.index.IndexResolver; import org.elasticsearch.xpack.ql.type.DataType; import org.elasticsearch.xpack.ql.type.DataTypes; import org.elasticsearch.xpack.ql.type.EsField; +import java.util.List; import java.util.Map; import static org.hamcrest.Matchers.equalTo; @@ -35,33 +36,20 @@ public void testLong() { } private void resolve(String esTypeName, TimeSeriesParams.MetricType metricType, DataType expected) { - String[] indices = new String[] { "idx-" + randomAlphaOfLength(5) }; - FieldCapabilities fieldCap = new FieldCapabilities( - randomAlphaOfLength(3), - esTypeName, - false, - true, - true, - false, - metricType, - indices, - null, - null, - null, - null, - Map.of() - ); - FieldCapabilitiesResponse caps = new FieldCapabilitiesResponse(indices, Map.of(fieldCap.getName(), Map.of(esTypeName, fieldCap))); - IndexResolution resolution = IndexResolver.mergedMappings( - EsqlDataTypeRegistry.INSTANCE, - "idx-*", - caps, - EsqlSession::specificValidity, - IndexResolver.PRESERVE_PROPERTIES, - null + String idx = "idx-" + randomAlphaOfLength(5); + String field = "f" + randomAlphaOfLength(3); + List idxResponses = List.of( + new FieldCapabilitiesIndexResponse( + idx, + idx, + Map.of(field, new IndexFieldCapabilities(field, esTypeName, false, true, true, false, metricType, Map.of())), + true + ) ); - EsField f = resolution.get().mapping().get(fieldCap.getName()); + FieldCapabilitiesResponse caps = new FieldCapabilitiesResponse(idxResponses, List.of()); + IndexResolution resolution = new EsqlIndexResolver(null, EsqlDataTypeRegistry.INSTANCE).mergedMappings("idx-*", caps); + EsField f = resolution.get().mapping().get(field); assertThat(f.getDataType(), equalTo(expected)); } } diff --git a/x-pack/plugin/esql/src/test/resources/empty_field_caps_response.json b/x-pack/plugin/esql/src/test/resources/empty_field_caps_response.json deleted file mode 100644 index fe8b293e3c0b9..0000000000000 --- a/x-pack/plugin/esql/src/test/resources/empty_field_caps_response.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "indices": [ - "test1", - "test2" - ], - "fields": { - "_index": { - "_index": { - "type": "_index", - "metadata_field": true, - "searchable": true, - "aggregatable": true - } - } - } -} diff --git a/x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java b/x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java index 955d658b01bab..52424956ef53e 100644 --- a/x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java +++ b/x-pack/plugin/mapper-unsigned-long/src/main/java/org/elasticsearch/xpack/unsignedlong/UnsignedLongFieldMapper.java @@ -333,7 +333,7 @@ protected Object parseSourceValue(Object value) { if (value.equals("")) { return nullValueFormatted; } - return parseUnsignedLong(value); + return unsignedToSortableSignedLong(parseUnsignedLong(value)); } }; BlockSourceReader.LeafIteratorLookup lookup = isStored() || isIndexed() From 1df32c93e37c9660c9116f57b6291d3d061c52cb Mon Sep 17 00:00:00 2001 From: Craig Taverner Date: Wed, 28 Feb 2024 16:49:09 +0100 Subject: [PATCH 194/250] Mute failing tests from #105837 (#105838) Many tests were failing, so we muted the entire class. Sometimes as many as fifteen tests failed in one run. --- .../org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.java b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.java index 8c1e47c29670a..bdb10ea65dc1b 100644 --- a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.java +++ b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.java @@ -9,11 +9,13 @@ import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.test.TestClustersThreadFilter; import org.elasticsearch.test.cluster.ElasticsearchCluster; import org.elasticsearch.xpack.esql.qa.rest.FieldExtractorTestCase; import org.junit.ClassRule; +@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105837") @ThreadLeakFilters(filters = TestClustersThreadFilter.class) public class FieldExtractorIT extends FieldExtractorTestCase { @ClassRule From 136b1a1adf1d9575701496be0053af12aab1851c Mon Sep 17 00:00:00 2001 From: Artem Prigoda Date: Wed, 28 Feb 2024 17:08:56 +0100 Subject: [PATCH 195/250] [test] Run cluster explanation if can't relocate a shard from a node (#105747) If we can't relocate a short to a different node for some reason, print out a shard allocation explanation, so we have more debug information for diagnostics. Resolves #104807 See #105443 --- .../cluster/PrevalidateShardPathIT.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateShardPathIT.java b/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateShardPathIT.java index 3a1fa8e5da272..560a525ec526c 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateShardPathIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/cluster/PrevalidateShardPathIT.java @@ -9,14 +9,18 @@ package org.elasticsearch.cluster; import org.apache.lucene.tests.util.LuceneTestCase; +import org.elasticsearch.action.admin.cluster.allocation.ClusterAllocationExplainRequest; +import org.elasticsearch.action.admin.cluster.allocation.TransportClusterAllocationExplainAction; import org.elasticsearch.action.admin.cluster.node.shutdown.NodePrevalidateShardPathResponse; import org.elasticsearch.action.admin.cluster.node.shutdown.PrevalidateShardPathRequest; import org.elasticsearch.action.admin.cluster.node.shutdown.PrevalidateShardPathResponse; import org.elasticsearch.action.admin.cluster.node.shutdown.TransportPrevalidateShardPathAction; import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.routing.ShardRouting; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.test.ESIntegTestCase; @@ -77,7 +81,31 @@ public void testCheckShards() throws Exception { assertThat(resp2.getNodes().size(), equalTo(1)); assertThat(resp2.getNodes().get(0).getNode().getId(), equalTo(node2Id)); assertTrue("There should be no failures in the response", resp.failures().isEmpty()); - assertTrue("The relocation source node should have removed the shard(s)", resp2.getNodes().get(0).getShardIds().isEmpty()); + Set node2ShardIds = resp2.getNodes().get(0).getShardIds(); + if (node2ShardIds.size() > 0) { + for (var node2Shard : clusterService().state() + .routingTable() + .allShards() + .filter(s -> s.getIndexName().equals(indexName)) + .filter(s -> node2ShardIds.contains(s.shardId())) + .filter(s -> s.currentNodeId().equals(node2Id)) + .toList()) { + var explanation = client().execute( + TransportClusterAllocationExplainAction.TYPE, + new ClusterAllocationExplainRequest().setIndex(node2Shard.getIndexName()) + .setCurrentNode(node2Shard.currentNodeId()) + .setShard(node2Shard.id()) + .setPrimary(node2Shard.primary()) + ).get(); + logger.info( + "Shard: {} is still located on relocation source node: {}. Allocation explanation: {}", + node2Shard.shardId(), + node2, + Strings.toString(ChunkedToXContent.wrapAsToXContent(explanation), false, true) + ); + } + throw new AssertionError("The relocation source node should have removed the shard(s)"); + } } catch (AssertionError e) { // Removal of shards which are no longer allocated to the node is attempted on every cluster state change in IndicesStore. // If for whatever reason the removal is not triggered (e.g. not enough nodes reported that the shards are active) or it From 067aba96cd74fd42066da7201ea02274d1b43652 Mon Sep 17 00:00:00 2001 From: Ryan Ernst Date: Wed, 28 Feb 2024 17:47:12 +0100 Subject: [PATCH 196/250] Standardize build distribution internals on os/architecture (#105842) The build handles platform specific code which may be for arm or x86. Yet there are multiple ways to describe 64bit x86, and the build converts between the two in several places. This commit consolidates on the x64 nomenclature in most places, except where necessary (eg ML still uses x86_64). relates #105715 --- distribution/archives/build.gradle | 24 ++++++++++++++---------- distribution/build.gradle | 17 +++++++++++------ distribution/packages/build.gradle | 2 +- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/distribution/archives/build.gradle b/distribution/archives/build.gradle index dcd9fbf733088..0508f29ef595a 100644 --- a/distribution/archives/build.gradle +++ b/distribution/archives/build.gradle @@ -11,7 +11,7 @@ import java.nio.file.Path apply plugin: 'elasticsearch.internal-distribution-archive-setup' -CopySpec archiveFiles(CopySpec modulesFiles, String distributionType, String platform, String architecture, boolean isTestDistro) { +CopySpec archiveFiles(String distributionType, String os, String architecture, boolean isTestDistro) { return copySpec { into("elasticsearch-${version}") { into('lib') { @@ -29,9 +29,9 @@ CopySpec archiveFiles(CopySpec modulesFiles, String distributionType, String pla into('bin') { with binFiles(distributionType, isTestDistro) } - into("darwin".equals(platform) ? 'jdk.app' : 'jdk') { + into("darwin".equals(os) ? 'jdk.app' : 'jdk') { if (isTestDistro == false) { - with jdkFiles(project, platform, architecture) + with jdkFiles(project, os, architecture) } } into('') { @@ -56,7 +56,11 @@ CopySpec archiveFiles(CopySpec modulesFiles, String distributionType, String pla with noticeFile(isTestDistro) into('modules') { - with modulesFiles + if (isTestDistro) { + with integTestModulesFiles + } else { + with modulesFiles(os, architecture) + } } } } @@ -65,42 +69,42 @@ CopySpec archiveFiles(CopySpec modulesFiles, String distributionType, String pla distribution_archives { integTestZip { content { - archiveFiles(integTestModulesFiles, 'zip', null, 'x64', true) + archiveFiles('zip', null, null, true) } } windowsZip { archiveClassifier = 'windows-x86_64' content { - archiveFiles(modulesFiles('windows-x86_64'), 'zip', 'windows', 'x64', false) + archiveFiles('zip', 'windows', 'x64', false) } } darwinTar { archiveClassifier = 'darwin-x86_64' content { - archiveFiles(modulesFiles('darwin-x86_64'), 'tar', 'darwin', 'x64', false) + archiveFiles('tar', 'darwin', 'x64', false) } } darwinAarch64Tar { archiveClassifier = 'darwin-aarch64' content { - archiveFiles(modulesFiles('darwin-aarch64'), 'tar', 'darwin', 'aarch64', false) + archiveFiles('tar', 'darwin', 'aarch64', false) } } linuxAarch64Tar { archiveClassifier = 'linux-aarch64' content { - archiveFiles(modulesFiles('linux-aarch64'), 'tar', 'linux', 'aarch64', false) + archiveFiles('tar', 'linux', 'aarch64', false) } } linuxTar { archiveClassifier = 'linux-x86_64' content { - archiveFiles(modulesFiles('linux-x86_64'), 'tar', 'linux', 'x64', false) + archiveFiles('tar', 'linux', 'x64', false) } } } diff --git a/distribution/build.gradle b/distribution/build.gradle index e45f1d09625d6..c8cc60b6facf6 100644 --- a/distribution/build.gradle +++ b/distribution/build.gradle @@ -332,10 +332,10 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { } } - modulesFiles = { platform -> + modulesFiles = { os, architecture -> copySpec { eachFile { - if (it.relativePath.segments[-2] == 'bin' || ((platform == 'darwin-x86_64' || platform == 'darwin-aarch64') && it.relativePath.segments[-2] == 'MacOS')) { + if (it.relativePath.segments[-2] == 'bin' || (os == 'darwin' && it.relativePath.segments[-2] == 'MacOS')) { // bin files, wherever they are within modules (eg platform specific) should be executable // and MacOS is an alternative to bin on macOS it.mode = 0755 @@ -344,7 +344,12 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { } } List excludePlatforms = ['linux-x86_64', 'linux-aarch64', 'windows-x86_64', 'darwin-x86_64', 'darwin-aarch64'] - if (platform != null) { + if (os != null) { + String platform = os + '-' + architecture + if (architecture == 'x64') { + // ML platform dir uses the x86_64 nomenclature + platform = os + '-x86_64' + } excludePlatforms.remove(excludePlatforms.indexOf(platform)) } else { excludePlatforms = [] @@ -430,15 +435,15 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { } } - jdkFiles = { Project project, String platform, String architecture -> + jdkFiles = { Project project, String os, String architecture -> return copySpec { - from project.jdks."bundled_${platform}_${architecture}" + from project.jdks."bundled_${os}_${architecture}" exclude "demo/**" /* * The Contents/MacOS directory interferes with notarization, and is unused by our distribution, so we exclude * it from the build. */ - if ("darwin".equals(platform)) { + if ("darwin".equals(os)) { exclude "Contents/MacOS" } eachFile { FileCopyDetails details -> diff --git a/distribution/packages/build.gradle b/distribution/packages/build.gradle index 1d0f77bd35970..1983736e4ee9e 100644 --- a/distribution/packages/build.gradle +++ b/distribution/packages/build.gradle @@ -143,7 +143,7 @@ def commonPackageConfig(String type, String architecture) { with libFiles } into('modules') { - with modulesFiles('linux-' + ((architecture == 'x64') ? 'x86_64' : architecture)) + with modulesFiles('linux', architecture) } into('jdk') { with jdkFiles(project, 'linux', architecture) From 0fb3a6ecd5bbbfdee87354e8d43119e01710a67a Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Wed, 28 Feb 2024 18:32:29 +0100 Subject: [PATCH 197/250] Cleanup logic around ML's ScrollDataExtractor and reactivate related test (#105826) Dried up the logic that is leaking buffers and causing test failures (and fixed a couple trivial items along the way) and reactivated tests. Should be easier to hunt this down now that the leak tracker is much more sensivitve and logs more context. --- .../integration/MlDistributedFailureIT.java | 1 - .../xpack/ml/datafeed/DatafeedJob.java | 4 +- .../AbstractAggregationDataExtractor.java | 13 +++++-- .../CompositeAggregationDataExtractor.java | 37 +++++-------------- .../extractor/scroll/ScrollDataExtractor.java | 33 ++++------------- .../dataframe/inference/InferenceRunner.java | 2 +- .../scroll/ScrollDataExtractorTests.java | 15 ++++---- 7 files changed, 36 insertions(+), 69 deletions(-) diff --git a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/MlDistributedFailureIT.java b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/MlDistributedFailureIT.java index 942729bb81c64..33fd7c108863b 100644 --- a/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/MlDistributedFailureIT.java +++ b/x-pack/plugin/ml/src/internalClusterTest/java/org/elasticsearch/xpack/ml/integration/MlDistributedFailureIT.java @@ -541,7 +541,6 @@ public void testClusterWithTwoMlNodes_RunsDatafeed_GivenOriginalNodeGoesDown() t }); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/103108") public void testClusterWithTwoMlNodes_StopsDatafeed_GivenJobFailsOnReassign() throws Exception { internalCluster().ensureAtMostNumDataNodes(0); logger.info("Starting dedicated master node..."); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJob.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJob.java index 3e196e1a12723..2a76b925247ff 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJob.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedJob.java @@ -75,7 +75,7 @@ class DatafeedJob { private volatile long lastDataCheckTimeMs; private volatile Tuple lastDataCheckAnnotationWithId; private volatile Long lastEndTimeMs; - private AtomicBoolean running = new AtomicBoolean(true); + private final AtomicBoolean running = new AtomicBoolean(true); private volatile boolean isIsolated; private volatile boolean haveEverSeenData; private volatile long consecutiveDelayedDataBuckets; @@ -351,7 +351,7 @@ public boolean isRunning() { return running.get(); } - private void run(long start, long end, FlushJobAction.Request flushRequest) throws IOException { + private void run(long start, long end, FlushJobAction.Request flushRequest) { if (end <= start) { return; } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AbstractAggregationDataExtractor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AbstractAggregationDataExtractor.java index 26c43e1d098c1..f561c2a0aa5ca 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AbstractAggregationDataExtractor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/AbstractAggregationDataExtractor.java @@ -22,6 +22,7 @@ import org.elasticsearch.xpack.core.ml.datafeed.SearchInterval; import org.elasticsearch.xpack.ml.datafeed.DatafeedTimingStatsReporter; import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractor; +import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorQueryContext; import org.elasticsearch.xpack.ml.datafeed.extractor.DataExtractorUtils; import java.io.ByteArrayInputStream; @@ -121,7 +122,7 @@ private InternalAggregations search() { LOGGER.debug("[{}] Executing aggregated search", context.jobId); ActionRequestBuilder searchRequest = buildSearchRequest(buildBaseSearchSource()); assert searchRequest.request().allowPartialSearchResults() == false; - SearchResponse searchResponse = executeSearchRequest(searchRequest); + SearchResponse searchResponse = executeSearchRequest(client, context.queryContext, searchRequest); try { LOGGER.debug("[{}] Search response was obtained", context.jobId); timingStatsReporter.reportSearchDuration(searchResponse.getTook()); @@ -142,9 +143,13 @@ private void initAggregationProcessor(InternalAggregations aggs) throws IOExcept aggregationToJsonProcessor.process(aggs); } - private SearchResponse executeSearchRequest(ActionRequestBuilder searchRequestBuilder) { + static SearchResponse executeSearchRequest( + Client client, + DataExtractorQueryContext context, + ActionRequestBuilder searchRequestBuilder + ) { SearchResponse searchResponse = ClientHelper.executeWithHeaders( - context.queryContext.headers, + context.headers, ClientHelper.ML_ORIGIN, client, searchRequestBuilder::get @@ -216,7 +221,7 @@ public DataSummary getSummary() { ActionRequestBuilder searchRequestBuilder = buildSearchRequest( DataExtractorUtils.getSearchSourceBuilderForSummary(context.queryContext) ); - SearchResponse searchResponse = executeSearchRequest(searchRequestBuilder); + SearchResponse searchResponse = executeSearchRequest(client, context.queryContext, searchRequestBuilder); try { LOGGER.debug("[{}] Aggregating Data summary response was obtained", context.jobId); timingStatsReporter.reportSearchDuration(searchResponse.getTook()); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/CompositeAggregationDataExtractor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/CompositeAggregationDataExtractor.java index e4712d051ef1e..874c68c0afd73 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/CompositeAggregationDataExtractor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/aggregation/CompositeAggregationDataExtractor.java @@ -16,7 +16,6 @@ import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregation; import org.elasticsearch.search.aggregations.bucket.composite.CompositeAggregationBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; -import org.elasticsearch.xpack.core.ClientHelper; import org.elasticsearch.xpack.core.ml.datafeed.DatafeedConfigUtils; import org.elasticsearch.xpack.core.ml.datafeed.SearchInterval; import org.elasticsearch.xpack.core.ml.utils.Intervals; @@ -48,9 +47,6 @@ class CompositeAggregationDataExtractor implements DataExtractor { private static final Logger LOGGER = LogManager.getLogger(CompositeAggregationDataExtractor.class); - private static final String EARLIEST_TIME = "earliest_time"; - private static final String LATEST_TIME = "latest_time"; - private volatile Map afterKey = null; private final CompositeAggregationBuilder compositeAggregationBuilder; private final Client client; @@ -90,7 +86,7 @@ public boolean isCancelled() { @Override public void cancel() { - LOGGER.debug(() -> "[" + context.jobId + "] Data extractor received cancel request"); + LOGGER.debug("[{}] Data extractor received cancel request", context.jobId); isCancelled = true; } @@ -113,7 +109,7 @@ public Result next() throws IOException { SearchInterval searchInterval = new SearchInterval(context.queryContext.start, context.queryContext.end); InternalAggregations aggs = search(); if (aggs == null) { - LOGGER.trace(() -> "[" + context.jobId + "] extraction finished"); + LOGGER.trace("[{}] extraction finished", context.jobId); hasNext = false; afterKey = null; return new Result(searchInterval, Optional.empty()); @@ -153,9 +149,9 @@ private InternalAggregations search() { } searchSourceBuilder.aggregation(compositeAggregationBuilder); ActionRequestBuilder searchRequest = requestBuilder.build(searchSourceBuilder); - SearchResponse searchResponse = executeSearchRequest(searchRequest); + SearchResponse searchResponse = AbstractAggregationDataExtractor.executeSearchRequest(client, context.queryContext, searchRequest); try { - LOGGER.trace(() -> "[" + context.jobId + "] Search composite response was obtained"); + LOGGER.trace("[{}] Search composite response was obtained", context.jobId); timingStatsReporter.reportSearchDuration(searchResponse.getTook()); InternalAggregations aggregations = searchResponse.getAggregations(); if (aggregations == null) { @@ -171,25 +167,6 @@ private InternalAggregations search() { } } - private SearchResponse executeSearchRequest(ActionRequestBuilder searchRequestBuilder) { - SearchResponse searchResponse = ClientHelper.executeWithHeaders( - context.queryContext.headers, - ClientHelper.ML_ORIGIN, - client, - searchRequestBuilder::get - ); - boolean success = false; - try { - DataExtractorUtils.checkForSkippedClusters(searchResponse); - success = true; - } finally { - if (success == false) { - searchResponse.decRef(); - } - } - return searchResponse; - } - private InputStream processAggs(InternalAggregations aggs) throws IOException { AggregationToJsonProcessor aggregationToJsonProcessor = new AggregationToJsonProcessor( context.queryContext.timeField, @@ -262,7 +239,11 @@ public DataSummary getSummary() { client, context.queryContext ); - SearchResponse searchResponse = executeSearchRequest(searchRequestBuilder); + SearchResponse searchResponse = AbstractAggregationDataExtractor.executeSearchRequest( + client, + context.queryContext, + searchRequestBuilder + ); try { LOGGER.debug("[{}] Aggregating Data summary response was obtained", context.jobId); timingStatsReporter.reportSearchDuration(searchResponse.getTook()); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractor.java index 5da89da6b3450..52ffe3893f33c 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractor.java @@ -9,6 +9,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ResourceNotFoundException; +import org.elasticsearch.action.ActionRequestBuilder; import org.elasticsearch.action.search.ClearScrollRequest; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchRequestBuilder; @@ -133,13 +134,13 @@ protected InputStream initScroll(long startTimestamp) throws IOException { } } - protected SearchResponse executeSearchRequest(SearchRequestBuilder searchRequestBuilder) { - SearchResponse searchResponse = ClientHelper.executeWithHeaders( - context.queryContext.headers, - ClientHelper.ML_ORIGIN, - client, - searchRequestBuilder::get + protected SearchResponse executeSearchRequest(ActionRequestBuilder searchRequestBuilder) { + return checkForSkippedClusters( + ClientHelper.executeWithHeaders(context.queryContext.headers, ClientHelper.ML_ORIGIN, client, searchRequestBuilder::get) ); + } + + private SearchResponse checkForSkippedClusters(SearchResponse searchResponse) { boolean success = false; try { DataExtractorUtils.checkForSkippedClusters(searchResponse); @@ -262,25 +263,7 @@ void markScrollAsErrored() { @SuppressWarnings("HiddenField") protected SearchResponse executeSearchScrollRequest(String scrollId) { - SearchResponse searchResponse = ClientHelper.executeWithHeaders( - context.queryContext.headers, - ClientHelper.ML_ORIGIN, - client, - () -> new SearchScrollRequestBuilder(client).setScroll(SCROLL_TIMEOUT).setScrollId(scrollId).get() - ); - boolean success = false; - try { - DataExtractorUtils.checkForSkippedClusters(searchResponse); - success = true; - } catch (ResourceNotFoundException e) { - clearScrollLoggingExceptions(searchResponse.getScrollId()); - throw e; - } finally { - if (success == false) { - searchResponse.decRef(); - } - } - return searchResponse; + return executeSearchRequest(new SearchScrollRequestBuilder(client).setScroll(SCROLL_TIMEOUT).setScrollId(scrollId)); } private void clearScroll() { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/inference/InferenceRunner.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/inference/InferenceRunner.java index c9ce6e0d4e3c7..637b37853363f 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/inference/InferenceRunner.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/inference/InferenceRunner.java @@ -153,7 +153,7 @@ private InferenceState restoreInferenceState() { config.getHeaders(), ClientHelper.ML_ORIGIN, client, - () -> client.search(searchRequest).actionGet() + client.search(searchRequest)::actionGet ); try { Max maxIncrementalIdAgg = searchResponse.getAggregations().get(DestinationIndex.INCREMENTAL_ID); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorTests.java index d994b14265a26..b04e9ab6d5332 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/extractor/scroll/ScrollDataExtractorTests.java @@ -9,11 +9,10 @@ import org.apache.lucene.search.TotalHits; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionFuture; +import org.elasticsearch.action.ActionRequestBuilder; import org.elasticsearch.action.search.ClearScrollRequest; -import org.elasticsearch.action.search.ClearScrollResponse; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchRequest; -import org.elasticsearch.action.search.SearchRequestBuilder; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.action.search.TransportClearScrollAction; @@ -77,7 +76,7 @@ public class ScrollDataExtractorTests extends ESTestCase { private Client client; - private List capturedSearchRequests; + private List> capturedSearchRequests; private List capturedContinueScrollIds; private ArgumentCaptor capturedClearScrollRequests; private String jobId; @@ -87,12 +86,11 @@ public class ScrollDataExtractorTests extends ESTestCase { private List scriptFields; private int scrollSize; private long initScrollStartTime; - private ActionFuture clearScrollFuture; private DatafeedTimingStatsReporter timingStatsReporter; private class TestDataExtractor extends ScrollDataExtractor { - private Queue> responses = new LinkedList<>(); + private final Queue> responses = new LinkedList<>(); private int numScrollReset; TestDataExtractor(long start, long end) { @@ -110,7 +108,7 @@ protected InputStream initScroll(long startTimestamp) throws IOException { } @Override - protected SearchResponse executeSearchRequest(SearchRequestBuilder searchRequestBuilder) { + protected SearchResponse executeSearchRequest(ActionRequestBuilder searchRequestBuilder) { capturedSearchRequests.add(searchRequestBuilder); Tuple responseOrException = responses.remove(); if (responseOrException.v2() != null) { @@ -176,9 +174,10 @@ public void setUpTests() { scriptFields = Collections.emptyList(); scrollSize = 1000; - clearScrollFuture = mock(ActionFuture.class); capturedClearScrollRequests = ArgumentCaptor.forClass(ClearScrollRequest.class); - when(client.execute(same(TransportClearScrollAction.TYPE), capturedClearScrollRequests.capture())).thenReturn(clearScrollFuture); + when(client.execute(same(TransportClearScrollAction.TYPE), capturedClearScrollRequests.capture())).thenReturn( + mock(ActionFuture.class) + ); timingStatsReporter = new DatafeedTimingStatsReporter(new DatafeedTimingStats(jobId), mock(DatafeedTimingStatsPersister.class)); } From 4bea4a7a10af48d40675b523bcd0943ae9194c90 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Thu, 29 Feb 2024 09:32:42 +0100 Subject: [PATCH 198/250] [Docs] Tiny format fix (#105820) --- docs/reference/mapping/types/dense-vector.asciidoc | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/reference/mapping/types/dense-vector.asciidoc b/docs/reference/mapping/types/dense-vector.asciidoc index d600bc5566ace..cec41eab41238 100644 --- a/docs/reference/mapping/types/dense-vector.asciidoc +++ b/docs/reference/mapping/types/dense-vector.asciidoc @@ -232,7 +232,6 @@ expense of slower indexing speed. + ^*^ This parameter can only be specified when `index` is `true`. + -+ .Properties of `index_options` [%collapsible%open] ==== From 52b027e44b0931b4a86099fe3ba8c71f0ea0cdff Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Thu, 29 Feb 2024 09:32:50 +0100 Subject: [PATCH 199/250] [DOCS] Fix bullet in get-desired-balance ref (#105819) Fix formatting --- docs/reference/cluster/get-desired-balance.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference/cluster/get-desired-balance.asciidoc b/docs/reference/cluster/get-desired-balance.asciidoc index 2628b5abca9f3..3fd87dcfedc4f 100644 --- a/docs/reference/cluster/get-desired-balance.asciidoc +++ b/docs/reference/cluster/get-desired-balance.asciidoc @@ -7,6 +7,7 @@ NOTE: {cloud-only} Exposes: + * the desired balance computation and reconciliation stats * balancing stats such as distribution of shards, disk and ingest forecasts across nodes and data tiers (based on the current cluster state) From 6bb6cab5bbe2884a1d56421ec972e74b6659662d Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 1 Mar 2024 11:13:38 +0100 Subject: [PATCH 200/250] Stop allocating/acquiring pages redundantly in RecyclerBytesStreamOutput (#105856) Acquiring a page just to figure out the recycler's page size is quite wasteful. Especially for the Netty allocator there is a non-trivial amount of underlying logic here that may even involve contention with other threads. We can easily pass the page size with the recycler. --- .../transport/netty4/NettyAllocator.java | 5 +++++ .../io/stream/RecyclerBytesStreamOutput.java | 4 +--- .../common/recycler/AbstractRecycler.java | 4 ++++ .../elasticsearch/common/recycler/Recycler.java | 10 ++++++++++ .../elasticsearch/common/recycler/Recyclers.java | 8 ++++++++ .../common/util/PageCacheRecycler.java | 14 ++++++++++++-- .../elasticsearch/transport/BytesRefRecycler.java | 5 +++++ .../io/stream/RecyclerBytesStreamOutputTests.java | 5 +++++ .../common/recycler/AbstractRecyclerTestCase.java | 7 ++++++- .../rest/action/cat/RestTableTests.java | 5 +++++ .../coordination/AbstractCoordinatorTestCase.java | 5 +++++ 11 files changed, 66 insertions(+), 6 deletions(-) diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/NettyAllocator.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/NettyAllocator.java index f5d566d977d09..863334af85144 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/NettyAllocator.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/NettyAllocator.java @@ -152,6 +152,11 @@ public void close() { } }; } + + @Override + public int pageSize() { + return PageCacheRecycler.BYTE_PAGE_SIZE; + } }; } diff --git a/server/src/main/java/org/elasticsearch/common/io/stream/RecyclerBytesStreamOutput.java b/server/src/main/java/org/elasticsearch/common/io/stream/RecyclerBytesStreamOutput.java index 80917b530202b..7be964fc1be39 100644 --- a/server/src/main/java/org/elasticsearch/common/io/stream/RecyclerBytesStreamOutput.java +++ b/server/src/main/java/org/elasticsearch/common/io/stream/RecyclerBytesStreamOutput.java @@ -43,9 +43,7 @@ public class RecyclerBytesStreamOutput extends BytesStream implements Releasable public RecyclerBytesStreamOutput(Recycler recycler) { this.recycler = recycler; - try (Recycler.V obtain = recycler.obtain()) { - pageSize = obtain.v().length; - } + this.pageSize = recycler.pageSize(); this.currentPageOffset = pageSize; } diff --git a/server/src/main/java/org/elasticsearch/common/recycler/AbstractRecycler.java b/server/src/main/java/org/elasticsearch/common/recycler/AbstractRecycler.java index b38c7195f55a2..ee4b8b9f93160 100644 --- a/server/src/main/java/org/elasticsearch/common/recycler/AbstractRecycler.java +++ b/server/src/main/java/org/elasticsearch/common/recycler/AbstractRecycler.java @@ -16,4 +16,8 @@ protected AbstractRecycler(Recycler.C c) { this.c = c; } + @Override + public int pageSize() { + return c.pageSize(); + } } diff --git a/server/src/main/java/org/elasticsearch/common/recycler/Recycler.java b/server/src/main/java/org/elasticsearch/common/recycler/Recycler.java index 316666e4267ec..999ee2f6beaab 100644 --- a/server/src/main/java/org/elasticsearch/common/recycler/Recycler.java +++ b/server/src/main/java/org/elasticsearch/common/recycler/Recycler.java @@ -30,6 +30,11 @@ interface C { /** Destroy the data. This operation allows the data structure to release any internal resources before GC. */ void destroy(T value); + + /** + * @return see {@link Recycler#pageSize()} + */ + int pageSize(); } interface V extends Releasable { @@ -44,4 +49,9 @@ interface V extends Releasable { V obtain(); + /** + * @return the page size of the recycled object if it is array backed. + */ + int pageSize(); + } diff --git a/server/src/main/java/org/elasticsearch/common/recycler/Recyclers.java b/server/src/main/java/org/elasticsearch/common/recycler/Recyclers.java index d4ddf73d4c389..200f8b055e51d 100644 --- a/server/src/main/java/org/elasticsearch/common/recycler/Recyclers.java +++ b/server/src/main/java/org/elasticsearch/common/recycler/Recyclers.java @@ -92,6 +92,10 @@ public boolean isRecycled() { }; } + @Override + public int pageSize() { + return getDelegate().pageSize(); + } }; } @@ -134,6 +138,10 @@ protected Recycler getDelegate() { return recyclers[slot()]; } + @Override + public int pageSize() { + return recyclers[slot()].pageSize(); + } }; } diff --git a/server/src/main/java/org/elasticsearch/common/util/PageCacheRecycler.java b/server/src/main/java/org/elasticsearch/common/util/PageCacheRecycler.java index a1430ae5ce784..49de2c822a99c 100644 --- a/server/src/main/java/org/elasticsearch/common/util/PageCacheRecycler.java +++ b/server/src/main/java/org/elasticsearch/common/util/PageCacheRecycler.java @@ -99,7 +99,7 @@ public PageCacheRecycler(Settings settings) { final int maxPageCount = (int) Math.min(Integer.MAX_VALUE, limit / PAGE_SIZE_IN_BYTES); final int maxBytePageCount = (int) (bytesWeight * maxPageCount / totalWeight); - bytePage = build(type, maxBytePageCount, allocatedProcessors, new AbstractRecyclerC() { + bytePage = build(type, maxBytePageCount, allocatedProcessors, new AbstractRecyclerC<>() { @Override public byte[] newInstance() { return new byte[BYTE_PAGE_SIZE]; @@ -109,10 +109,15 @@ public byte[] newInstance() { public void recycle(byte[] value) { // nothing to do } + + @Override + public int pageSize() { + return BYTE_PAGE_SIZE; + } }); final int maxObjectPageCount = (int) (objectsWeight * maxPageCount / totalWeight); - objectPage = build(type, maxObjectPageCount, allocatedProcessors, new AbstractRecyclerC() { + objectPage = build(type, maxObjectPageCount, allocatedProcessors, new AbstractRecyclerC<>() { @Override public Object[] newInstance() { return new Object[OBJECT_PAGE_SIZE]; @@ -122,6 +127,11 @@ public Object[] newInstance() { public void recycle(Object[] value) { Arrays.fill(value, null); // we need to remove the strong refs on the objects stored in the array } + + @Override + public int pageSize() { + return OBJECT_PAGE_SIZE; + } }); assert PAGE_SIZE_IN_BYTES * (maxBytePageCount + maxObjectPageCount) <= limit; diff --git a/server/src/main/java/org/elasticsearch/transport/BytesRefRecycler.java b/server/src/main/java/org/elasticsearch/transport/BytesRefRecycler.java index ca1fd984f2bf0..7b083ea77a991 100644 --- a/server/src/main/java/org/elasticsearch/transport/BytesRefRecycler.java +++ b/server/src/main/java/org/elasticsearch/transport/BytesRefRecycler.java @@ -43,4 +43,9 @@ public void close() { } }; } + + @Override + public int pageSize() { + return PageCacheRecycler.BYTE_PAGE_SIZE; + } } diff --git a/server/src/test/java/org/elasticsearch/common/io/stream/RecyclerBytesStreamOutputTests.java b/server/src/test/java/org/elasticsearch/common/io/stream/RecyclerBytesStreamOutputTests.java index fd54dd12ce189..6cc7b355d4b1c 100644 --- a/server/src/test/java/org/elasticsearch/common/io/stream/RecyclerBytesStreamOutputTests.java +++ b/server/src/test/java/org/elasticsearch/common/io/stream/RecyclerBytesStreamOutputTests.java @@ -1027,6 +1027,11 @@ public V obtain() { pagesAllocated.incrementAndGet(); return page; } + + @Override + public int pageSize() { + return pageSize; + } })) { var bytesAllocated = 0; while (bytesAllocated < Integer.MAX_VALUE) { diff --git a/server/src/test/java/org/elasticsearch/common/recycler/AbstractRecyclerTestCase.java b/server/src/test/java/org/elasticsearch/common/recycler/AbstractRecyclerTestCase.java index bd43bbbdcfbc2..5efa3e2fea300 100644 --- a/server/src/test/java/org/elasticsearch/common/recycler/AbstractRecyclerTestCase.java +++ b/server/src/test/java/org/elasticsearch/common/recycler/AbstractRecyclerTestCase.java @@ -26,7 +26,7 @@ public abstract class AbstractRecyclerTestCase extends ESTestCase { @Override public byte[] newInstance() { - byte[] value = new byte[10]; + byte[] value = new byte[pageSize()]; // "fresh" is intentionally not 0 to ensure we covered this code path Arrays.fill(value, FRESH); return value; @@ -43,6 +43,11 @@ public void destroy(byte[] value) { Arrays.fill(value, DEAD); } + @Override + public int pageSize() { + return 10; + } + }; protected void assertFresh(byte[] data) { diff --git a/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java b/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java index ff3b72463d86e..7a8c67177aade 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/cat/RestTableTests.java @@ -404,6 +404,11 @@ public void close() { } }; } + + @Override + public int pageSize() { + return pageSize; + } }; final var bodyChunks = new ArrayList(); diff --git a/test/framework/src/main/java/org/elasticsearch/cluster/coordination/AbstractCoordinatorTestCase.java b/test/framework/src/main/java/org/elasticsearch/cluster/coordination/AbstractCoordinatorTestCase.java index 1d76c1e40910e..7f39120e83c07 100644 --- a/test/framework/src/main/java/org/elasticsearch/cluster/coordination/AbstractCoordinatorTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/cluster/coordination/AbstractCoordinatorTestCase.java @@ -2033,6 +2033,11 @@ public void close() { return trackedRef; } + @Override + public int pageSize() { + return delegate.pageSize(); + } + /** * Release all tracked refs as if the node rebooted. */ From 07ae23aad3e54d0ef315ad0ecf78faf8f67689e1 Mon Sep 17 00:00:00 2001 From: Henning Andersen <33268011+henningandersen@users.noreply.github.com> Date: Fri, 1 Mar 2024 17:21:53 +0100 Subject: [PATCH 201/250] Document 429 handling generically (#105700) We only had a few mentions of 429 handling, now documenting our expectation generically. Co-authored-by: David Turner Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> --- docs/reference/api-conventions.asciidoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/reference/api-conventions.asciidoc b/docs/reference/api-conventions.asciidoc index 64cb499a9cd4e..b0fa51679d661 100644 --- a/docs/reference/api-conventions.asciidoc +++ b/docs/reference/api-conventions.asciidoc @@ -302,6 +302,14 @@ Content-Type: application/vnd.elasticsearch+json; compatible-with=7 Accept: application/vnd.elasticsearch+json; compatible-with=7 ---------------------------------------------------------------------- +[discrete] +[[api-push-back]] +=== HTTP `429 Too Many Requests` status code push back + +{es} APIs may respond with the HTTP `429 Too Many Requests` status code, indicating that the cluster is too busy +to handle the request. When this happens, consider retrying after a short delay. If the retry also receives +a `429 Too Many Requests` response, extend the delay by backing off exponentially before each subsequent retry. + [discrete] [[api-url-access-control]] === URL-based access control From 0a7c88cfd67c52be8a168be4f85b6bc63cf5edf7 Mon Sep 17 00:00:00 2001 From: Rene Groeschke Date: Fri, 1 Mar 2024 19:03:11 +0100 Subject: [PATCH 202/250] Update Gradle Enterprise plugin to 3.16.2 (#105871) --- gradle/build.versions.toml | 2 +- gradle/verification-metadata.xml | 6 +++--- plugins/examples/settings.gradle | 2 +- settings.gradle | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle/build.versions.toml b/gradle/build.versions.toml index a17f77c6b4917..bbcad622cf5e5 100644 --- a/gradle/build.versions.toml +++ b/gradle/build.versions.toml @@ -17,7 +17,7 @@ commons-codec = "commons-codec:commons-codec:1.11" commmons-io = "commons-io:commons-io:2.2" docker-compose = "com.avast.gradle:gradle-docker-compose-plugin:0.17.5" forbiddenApis = "de.thetaphi:forbiddenapis:3.6" -gradle-enterprise = "com.gradle:gradle-enterprise-gradle-plugin:3.16.1" +gradle-enterprise = "com.gradle:gradle-enterprise-gradle-plugin:3.16.2" hamcrest = "org.hamcrest:hamcrest:2.1" httpcore = "org.apache.httpcomponents:httpcore:4.4.12" httpclient = "org.apache.httpcomponents:httpclient:4.5.14" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 07b35f8b3e345..648c7260256dd 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -736,9 +736,9 @@ - - - + + + diff --git a/plugins/examples/settings.gradle b/plugins/examples/settings.gradle index f71eca0d1f966..af2596fdbafe3 100644 --- a/plugins/examples/settings.gradle +++ b/plugins/examples/settings.gradle @@ -7,7 +7,7 @@ */ plugins { - id "com.gradle.enterprise" version "3.16.1" + id "com.gradle.enterprise" version "3.16.2" } // Include all subdirectories as example projects diff --git a/settings.gradle b/settings.gradle index 5eefe21b360d6..c183971bc12ca 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id "com.gradle.enterprise" version "3.16.1" + id "com.gradle.enterprise" version "3.16.2" id 'elasticsearch.java-toolchain' } From 8cc438ea8ed936faad47a6c0a08272c8318ffb20 Mon Sep 17 00:00:00 2001 From: Stef Nestor <26751266+stefnestor@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:40:33 -0700 Subject: [PATCH 203/250] (+DOC)(ILM) Shrink recovers to specific node (#105872) --- docs/reference/indices/shrink-index.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/indices/shrink-index.asciidoc b/docs/reference/indices/shrink-index.asciidoc index 9ebc6ff5ef5a5..388344123b964 100644 --- a/docs/reference/indices/shrink-index.asciidoc +++ b/docs/reference/indices/shrink-index.asciidoc @@ -101,7 +101,8 @@ A shrink operation: disks) . Recovers the target index as though it were a closed index which - had just been re-opened. + had just been re-opened. Recovers shards to <> + `.routing.allocation.initial_recovery._id`. [[_shrinking_an_index]] From 5d809f31523e54e14c72d37fb2c748bd87e696c4 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Sun, 3 Mar 2024 02:53:51 +0100 Subject: [PATCH 204/250] ProjectOperator should not retain references to released blocks (#105848) The heap attack tests hit OOM where the circuit breaker was under-accounted. This was because the ProjectOperator retained references to released blocks. Consequently, the released block couldn't be GCed although we have decreased memory usage in the circuit breaker. Relates #10563 --- docs/changelog/105848.yaml | 5 +++++ .../compute/operator/ProjectOperator.java | 12 +++++------- 2 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 docs/changelog/105848.yaml diff --git a/docs/changelog/105848.yaml b/docs/changelog/105848.yaml new file mode 100644 index 0000000000000..18291066177f6 --- /dev/null +++ b/docs/changelog/105848.yaml @@ -0,0 +1,5 @@ +pr: 105848 +summary: '`ProjectOperator` should not retain references to released blocks' +area: ES|QL +type: bug +issues: [] diff --git a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/ProjectOperator.java b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/ProjectOperator.java index 2e61150061e1a..d318639625034 100644 --- a/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/ProjectOperator.java +++ b/x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/ProjectOperator.java @@ -30,7 +30,7 @@ public String describe() { } private final int[] projection; - private Block[] blocks; + private final Block[] blocks; /** * Creates an operator that applies the given projection, encoded as an integer list where @@ -41,6 +41,7 @@ public String describe() { */ public ProjectOperator(List projection) { this.projection = projection.stream().mapToInt(Integer::intValue).toArray(); + this.blocks = new Block[projection.size()]; } @Override @@ -49,11 +50,6 @@ protected Page process(Page page) { if (blockCount == 0) { return page; } - if (blocks == null) { - blocks = new Block[projection.length]; - } - - Arrays.fill(blocks, null); int b = 0; for (int source : projection) { if (source >= blockCount) { @@ -69,7 +65,9 @@ protected Page process(Page page) { page.releaseBlocks(); // Use positionCount explicitly to avoid re-computing - also, if the projection is empty, there may be // no more blocks left to determine the positionCount from. - return new Page(positionCount, blocks); + Page output = new Page(positionCount, blocks); + Arrays.fill(blocks, null); + return output; } @Override From c888d394e939884a4f83738b3bf2868b9f4ca7d9 Mon Sep 17 00:00:00 2001 From: David Turner Date: Sun, 3 Mar 2024 21:09:15 +0100 Subject: [PATCH 205/250] AwaitsFix for #105890 --- .../org/elasticsearch/cluster/service/MasterServiceTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/cluster/service/MasterServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/service/MasterServiceTests.java index 77b2f0112ad43..5b933f6a2cec7 100644 --- a/server/src/test/java/org/elasticsearch/cluster/service/MasterServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/service/MasterServiceTests.java @@ -2121,6 +2121,7 @@ public void onFailure(Exception e) { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105890") public void testTimeoutRejectionBehaviourAtSubmission() { final var source = randomIdentifier(); From 5d1d75a3c670ecd384213510add5c8acd7f2cd2e Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Sun, 3 Mar 2024 21:15:31 +0100 Subject: [PATCH 206/250] Avoid eager loading Netty classes in Netty4Plugin (#105889) When using the secure Netty4 transport (common case these days) we still load the Netty4Plugin in a separate classloader. Prior to this change this would result in 0.5M+ of needless class-loading by loading the `Netty4Transport` and `Netty4HttpServerTransportTests` classes. Moving the settings constants (no other changes than that in here) to the plugin makes it so that none of these are loaded and the plugin when unused consumes a trivial 30kB of meta-space. --- .../elasticsearch/ESNetty4IntegTestCase.java | 3 +- .../netty4/Netty4HttpServerTransport.java | 59 +----------- .../transport/netty4/Netty4Plugin.java | 94 +++++++++++++++++-- .../transport/netty4/Netty4Transport.java | 33 +------ .../transport/netty4/SharedGroupFactory.java | 7 +- .../Netty4HttpServerTransportTests.java | 3 +- .../netty4/SharedGroupFactoryTests.java | 5 +- .../test/NativeRealmIntegTestCase.java | 4 +- 8 files changed, 100 insertions(+), 108 deletions(-) diff --git a/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/ESNetty4IntegTestCase.java b/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/ESNetty4IntegTestCase.java index c996f55198bf6..65fbde5d42005 100644 --- a/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/ESNetty4IntegTestCase.java +++ b/modules/transport-netty4/src/internalClusterTest/java/org/elasticsearch/ESNetty4IntegTestCase.java @@ -12,7 +12,6 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.transport.netty4.Netty4Plugin; -import org.elasticsearch.transport.netty4.Netty4Transport; import java.util.Collection; import java.util.Collections; @@ -29,7 +28,7 @@ protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { Settings.Builder builder = Settings.builder().put(super.nodeSettings(nodeOrdinal, otherSettings)); // randomize netty settings if (randomBoolean()) { - builder.put(Netty4Transport.WORKER_COUNT.getKey(), random().nextInt(3) + 1); + builder.put(Netty4Plugin.WORKER_COUNT.getKey(), random().nextInt(3) + 1); } builder.put(NetworkModule.TRANSPORT_TYPE_KEY, Netty4Plugin.NETTY_TRANSPORT_NAME); builder.put(NetworkModule.HTTP_TYPE_KEY, Netty4Plugin.NETTY_HTTP_TRANSPORT_NAME); diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java index 274240a40bd46..7844f7bbb8ce2 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpServerTransport.java @@ -39,10 +39,7 @@ import org.elasticsearch.common.network.CloseableChannel; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.ClusterSettings; -import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.core.IOUtils; @@ -59,6 +56,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.netty4.AcceptChannelHandler; import org.elasticsearch.transport.netty4.NetUtils; +import org.elasticsearch.transport.netty4.Netty4Plugin; import org.elasticsearch.transport.netty4.Netty4Utils; import org.elasticsearch.transport.netty4.Netty4WriteThrottlingHandler; import org.elasticsearch.transport.netty4.NettyAllocator; @@ -73,7 +71,6 @@ import java.util.function.BiPredicate; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_CHUNK_SIZE; -import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_CONTENT_LENGTH; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_HEADER_SIZE; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_INITIAL_LINE_LENGTH; import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_READ_TIMEOUT; @@ -90,56 +87,6 @@ public class Netty4HttpServerTransport extends AbstractHttpServerTransport { private static final Logger logger = LogManager.getLogger(Netty4HttpServerTransport.class); - /* - * Size in bytes of an individual message received by io.netty.handler.codec.MessageAggregator which accumulates the content for an - * HTTP request. This number is used for estimating the maximum number of allowed buffers before the MessageAggregator's internal - * collection of buffers is resized. - * - * By default we assume the Ethernet MTU (1500 bytes) but users can override it with a system property. - */ - private static final ByteSizeValue MTU = ByteSizeValue.ofBytes(Long.parseLong(System.getProperty("es.net.mtu", "1500"))); - - private static final String SETTING_KEY_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS = "http.netty.max_composite_buffer_components"; - - public static Setting SETTING_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS = new Setting<>( - SETTING_KEY_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS, - (s) -> { - ByteSizeValue maxContentLength = SETTING_HTTP_MAX_CONTENT_LENGTH.get(s); - /* - * Netty accumulates buffers containing data from all incoming network packets that make up one HTTP request in an instance of - * io.netty.buffer.CompositeByteBuf (think of it as a buffer of buffers). Once its capacity is reached, the buffer will iterate - * over its individual entries and put them into larger buffers (see io.netty.buffer.CompositeByteBuf#consolidateIfNeeded() - * for implementation details). We want to to resize that buffer because this leads to additional garbage on the heap and also - * increases the application's native memory footprint (as direct byte buffers hold their contents off-heap). - * - * With this setting we control the CompositeByteBuf's capacity (which is by default 1024, see - * io.netty.handler.codec.MessageAggregator#DEFAULT_MAX_COMPOSITEBUFFER_COMPONENTS). To determine a proper default capacity for - * that buffer, we need to consider that the upper bound for the size of HTTP requests is determined by `maxContentLength`. The - * number of buffers that are needed depend on how often Netty reads network packets which depends on the network type (MTU). - * We assume here that Elasticsearch receives HTTP requests via an Ethernet connection which has a MTU of 1500 bytes. - * - * Note that we are *not* pre-allocating any memory based on this setting but rather determine the CompositeByteBuf's capacity. - * The tradeoff is between less (but larger) buffers that are contained in the CompositeByteBuf and more (but smaller) buffers. - * With the default max content length of 100MB and a MTU of 1500 bytes we would allow 69905 entries. - */ - long maxBufferComponentsEstimate = Math.round((double) (maxContentLength.getBytes() / MTU.getBytes())); - // clamp value to the allowed range - long maxBufferComponents = Math.max(2, Math.min(maxBufferComponentsEstimate, Integer.MAX_VALUE)); - return String.valueOf(maxBufferComponents); - // Netty's CompositeByteBuf implementation does not allow less than two components. - }, - s -> Setting.parseInt(s, 2, Integer.MAX_VALUE, SETTING_KEY_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS), - Property.NodeScope - ); - - public static final Setting SETTING_HTTP_WORKER_COUNT = Setting.intSetting("http.netty.worker_count", 0, Property.NodeScope); - - public static final Setting SETTING_HTTP_NETTY_RECEIVE_PREDICTOR_SIZE = Setting.byteSizeSetting( - "http.netty.receive_predictor_size", - new ByteSizeValue(64, ByteSizeUnit.KB), - Property.NodeScope - ); - private final int pipeliningMaxEvents; private final SharedGroupFactory sharedGroupFactory; @@ -186,11 +133,11 @@ public Netty4HttpServerTransport( this.pipeliningMaxEvents = SETTING_PIPELINING_MAX_EVENTS.get(settings); - this.maxCompositeBufferComponents = SETTING_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS.get(settings); + this.maxCompositeBufferComponents = Netty4Plugin.SETTING_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS.get(settings); this.readTimeoutMillis = Math.toIntExact(SETTING_HTTP_READ_TIMEOUT.get(settings).getMillis()); - ByteSizeValue receivePredictor = SETTING_HTTP_NETTY_RECEIVE_PREDICTOR_SIZE.get(settings); + ByteSizeValue receivePredictor = Netty4Plugin.SETTING_HTTP_NETTY_RECEIVE_PREDICTOR_SIZE.get(settings); recvByteBufAllocator = new FixedRecvByteBufAllocator(receivePredictor.bytesAsInt()); logger.debug( diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Plugin.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Plugin.java index 2934d425709f2..5fd69f8d9e537 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Plugin.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Plugin.java @@ -16,8 +16,11 @@ import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeUnit; +import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.BigArrays; import org.elasticsearch.common.util.PageCacheRecycler; +import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.http.HttpPreRequest; import org.elasticsearch.http.HttpServerTransport; @@ -38,24 +41,99 @@ import java.util.function.BiConsumer; import java.util.function.Supplier; +import static org.elasticsearch.common.settings.Setting.byteSizeSetting; +import static org.elasticsearch.common.settings.Setting.intSetting; +import static org.elasticsearch.http.HttpTransportSettings.SETTING_HTTP_MAX_CONTENT_LENGTH; + public class Netty4Plugin extends Plugin implements NetworkPlugin { public static final String NETTY_TRANSPORT_NAME = "netty4"; public static final String NETTY_HTTP_TRANSPORT_NAME = "netty4"; + public static final Setting SETTING_HTTP_WORKER_COUNT = Setting.intSetting( + "http.netty.worker_count", + 0, + Setting.Property.NodeScope + ); + public static final Setting SETTING_HTTP_NETTY_RECEIVE_PREDICTOR_SIZE = byteSizeSetting( + "http.netty.receive_predictor_size", + new ByteSizeValue(64, ByteSizeUnit.KB), + Setting.Property.NodeScope + ); + public static final Setting WORKER_COUNT = new Setting<>( + "transport.netty.worker_count", + (s) -> Integer.toString(EsExecutors.allocatedProcessors(s)), + (s) -> Setting.parseInt(s, 1, "transport.netty.worker_count"), + Setting.Property.NodeScope + ); + private static final Setting NETTY_RECEIVE_PREDICTOR_SIZE = byteSizeSetting( + "transport.netty.receive_predictor_size", + new ByteSizeValue(64, ByteSizeUnit.KB), + Setting.Property.NodeScope + ); + public static final Setting NETTY_RECEIVE_PREDICTOR_MAX = byteSizeSetting( + "transport.netty.receive_predictor_max", + NETTY_RECEIVE_PREDICTOR_SIZE, + Setting.Property.NodeScope + ); + public static final Setting NETTY_RECEIVE_PREDICTOR_MIN = byteSizeSetting( + "transport.netty.receive_predictor_min", + NETTY_RECEIVE_PREDICTOR_SIZE, + Setting.Property.NodeScope + ); + public static final Setting NETTY_BOSS_COUNT = intSetting("transport.netty.boss_count", 1, 1, Setting.Property.NodeScope); + /* + * Size in bytes of an individual message received by io.netty.handler.codec.MessageAggregator which accumulates the content for an + * HTTP request. This number is used for estimating the maximum number of allowed buffers before the MessageAggregator's internal + * collection of buffers is resized. + * + * By default we assume the Ethernet MTU (1500 bytes) but users can override it with a system property. + */ + private static final ByteSizeValue MTU = ByteSizeValue.ofBytes(Long.parseLong(System.getProperty("es.net.mtu", "1500"))); + private static final String SETTING_KEY_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS = "http.netty.max_composite_buffer_components"; + public static Setting SETTING_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS = new Setting<>( + SETTING_KEY_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS, + (s) -> { + ByteSizeValue maxContentLength = SETTING_HTTP_MAX_CONTENT_LENGTH.get(s); + /* + * Netty accumulates buffers containing data from all incoming network packets that make up one HTTP request in an instance of + * io.netty.buffer.CompositeByteBuf (think of it as a buffer of buffers). Once its capacity is reached, the buffer will iterate + * over its individual entries and put them into larger buffers (see io.netty.buffer.CompositeByteBuf#consolidateIfNeeded() + * for implementation details). We want to to resize that buffer because this leads to additional garbage on the heap and also + * increases the application's native memory footprint (as direct byte buffers hold their contents off-heap). + * + * With this setting we control the CompositeByteBuf's capacity (which is by default 1024, see + * io.netty.handler.codec.MessageAggregator#DEFAULT_MAX_COMPOSITEBUFFER_COMPONENTS). To determine a proper default capacity for + * that buffer, we need to consider that the upper bound for the size of HTTP requests is determined by `maxContentLength`. The + * number of buffers that are needed depend on how often Netty reads network packets which depends on the network type (MTU). + * We assume here that Elasticsearch receives HTTP requests via an Ethernet connection which has a MTU of 1500 bytes. + * + * Note that we are *not* pre-allocating any memory based on this setting but rather determine the CompositeByteBuf's capacity. + * The tradeoff is between less (but larger) buffers that are contained in the CompositeByteBuf and more (but smaller) buffers. + * With the default max content length of 100MB and a MTU of 1500 bytes we would allow 69905 entries. + */ + long maxBufferComponentsEstimate = Math.round((double) (maxContentLength.getBytes() / MTU.getBytes())); + // clamp value to the allowed range + long maxBufferComponents = Math.max(2, Math.min(maxBufferComponentsEstimate, Integer.MAX_VALUE)); + return String.valueOf(maxBufferComponents); + // Netty's CompositeByteBuf implementation does not allow less than two components. + }, + s -> Setting.parseInt(s, 2, Integer.MAX_VALUE, SETTING_KEY_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS), + Setting.Property.NodeScope + ); private final SetOnce groupFactory = new SetOnce<>(); @Override public List> getSettings() { return Arrays.asList( - Netty4HttpServerTransport.SETTING_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS, - Netty4HttpServerTransport.SETTING_HTTP_WORKER_COUNT, - Netty4HttpServerTransport.SETTING_HTTP_NETTY_RECEIVE_PREDICTOR_SIZE, - Netty4Transport.WORKER_COUNT, - Netty4Transport.NETTY_RECEIVE_PREDICTOR_SIZE, - Netty4Transport.NETTY_RECEIVE_PREDICTOR_MIN, - Netty4Transport.NETTY_RECEIVE_PREDICTOR_MAX, - Netty4Transport.NETTY_BOSS_COUNT + SETTING_HTTP_NETTY_MAX_COMPOSITE_BUFFER_COMPONENTS, + SETTING_HTTP_WORKER_COUNT, + SETTING_HTTP_NETTY_RECEIVE_PREDICTOR_SIZE, + WORKER_COUNT, + NETTY_RECEIVE_PREDICTOR_SIZE, + NETTY_RECEIVE_PREDICTOR_MIN, + NETTY_RECEIVE_PREDICTOR_MAX, + NETTY_BOSS_COUNT ); } diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java index 9a0d6692723e3..6d8f950ef1cf4 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/Netty4Transport.java @@ -31,10 +31,7 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.recycler.Recycler; -import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.PageCacheRecycler; import org.elasticsearch.common.util.concurrent.EsExecutors; @@ -54,8 +51,6 @@ import java.net.InetSocketAddress; import java.util.Map; -import static org.elasticsearch.common.settings.Setting.byteSizeSetting; -import static org.elasticsearch.common.settings.Setting.intSetting; import static org.elasticsearch.common.util.concurrent.ConcurrentCollections.newConcurrentMap; import static org.elasticsearch.core.Strings.format; import static org.elasticsearch.transport.RemoteClusterPortSettings.REMOTE_CLUSTER_PROFILE; @@ -70,30 +65,6 @@ public class Netty4Transport extends TcpTransport { private static final Logger logger = LogManager.getLogger(Netty4Transport.class); - public static final Setting WORKER_COUNT = new Setting<>( - "transport.netty.worker_count", - (s) -> Integer.toString(EsExecutors.allocatedProcessors(s)), - (s) -> Setting.parseInt(s, 1, "transport.netty.worker_count"), - Property.NodeScope - ); - - public static final Setting NETTY_RECEIVE_PREDICTOR_SIZE = Setting.byteSizeSetting( - "transport.netty.receive_predictor_size", - new ByteSizeValue(64, ByteSizeUnit.KB), - Property.NodeScope - ); - public static final Setting NETTY_RECEIVE_PREDICTOR_MIN = byteSizeSetting( - "transport.netty.receive_predictor_min", - NETTY_RECEIVE_PREDICTOR_SIZE, - Property.NodeScope - ); - public static final Setting NETTY_RECEIVE_PREDICTOR_MAX = byteSizeSetting( - "transport.netty.receive_predictor_max", - NETTY_RECEIVE_PREDICTOR_SIZE, - Property.NodeScope - ); - - public static final Setting NETTY_BOSS_COUNT = intSetting("transport.netty.boss_count", 1, 1, Property.NodeScope); public static final ChannelOption OPTION_TCP_KEEP_IDLE = NioChannelOption.of(NetUtils.getTcpKeepIdleSocketOption()); public static final ChannelOption OPTION_TCP_KEEP_INTERVAL = NioChannelOption.of(NetUtils.getTcpKeepIntervalSocketOption()); public static final ChannelOption OPTION_TCP_KEEP_COUNT = NioChannelOption.of(NetUtils.getTcpKeepCountSocketOption()); @@ -123,8 +94,8 @@ public Netty4Transport( this.sharedGroupFactory = sharedGroupFactory; // See AdaptiveReceiveBufferSizePredictor#DEFAULT_XXX for default values in netty..., we can use higher ones for us, even fixed one - this.receivePredictorMin = NETTY_RECEIVE_PREDICTOR_MIN.get(settings); - this.receivePredictorMax = NETTY_RECEIVE_PREDICTOR_MAX.get(settings); + this.receivePredictorMin = Netty4Plugin.NETTY_RECEIVE_PREDICTOR_MIN.get(settings); + this.receivePredictorMax = Netty4Plugin.NETTY_RECEIVE_PREDICTOR_MAX.get(settings); if (receivePredictorMax.getBytes() == receivePredictorMin.getBytes()) { recvByteBufAllocator = new FixedRecvByteBufAllocator((int) receivePredictorMax.getBytes()); } else { diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/SharedGroupFactory.java b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/SharedGroupFactory.java index 14c2c13ed7669..849597b1d9915 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/SharedGroupFactory.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/transport/netty4/SharedGroupFactory.java @@ -18,7 +18,6 @@ import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.core.AbstractRefCounted; import org.elasticsearch.http.HttpServerTransport; -import org.elasticsearch.http.netty4.Netty4HttpServerTransport; import org.elasticsearch.transport.TcpTransport; import java.util.concurrent.TimeUnit; @@ -29,7 +28,7 @@ /** * Creates and returns {@link io.netty.channel.EventLoopGroup} instances. It will return a shared group for * both {@link #getHttpGroup()} and {@link #getTransportGroup()} if - * {@link org.elasticsearch.http.netty4.Netty4HttpServerTransport#SETTING_HTTP_WORKER_COUNT} is configured to be 0. + * {@link Netty4Plugin#SETTING_HTTP_WORKER_COUNT} is configured to be 0. * If that setting is not 0, then it will return a different group in the {@link #getHttpGroup()} call. */ public final class SharedGroupFactory { @@ -45,8 +44,8 @@ public final class SharedGroupFactory { public SharedGroupFactory(Settings settings) { this.settings = settings; - this.workerCount = Netty4Transport.WORKER_COUNT.get(settings); - this.httpWorkerCount = Netty4HttpServerTransport.SETTING_HTTP_WORKER_COUNT.get(settings); + this.workerCount = Netty4Plugin.WORKER_COUNT.get(settings); + this.httpWorkerCount = Netty4Plugin.SETTING_HTTP_WORKER_COUNT.get(settings); } public Settings getSettings() { diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java index 4d44c37ac094a..5ce989fba214a 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/http/netty4/Netty4HttpServerTransportTests.java @@ -78,6 +78,7 @@ import org.elasticsearch.test.rest.FakeRestRequest; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.netty4.Netty4Plugin; import org.elasticsearch.transport.netty4.NettyAllocator; import org.elasticsearch.transport.netty4.SharedGroupFactory; import org.elasticsearch.transport.netty4.TLSConfig; @@ -889,7 +890,7 @@ public void dispatchBadRequest(final RestChannel channel, final ThreadContext th public void testMultipleValidationsOnTheSameChannel() throws InterruptedException { // ensure that there is a single channel active - final Settings settings = createBuilderWithPort().put(Netty4HttpServerTransport.SETTING_HTTP_WORKER_COUNT.getKey(), 1).build(); + final Settings settings = createBuilderWithPort().put(Netty4Plugin.SETTING_HTTP_WORKER_COUNT.getKey(), 1).build(); final Set okURIs = ConcurrentHashMap.newKeySet(); final Set nokURIs = ConcurrentHashMap.newKeySet(); final SetOnce channelSetOnce = new SetOnce<>(); diff --git a/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/SharedGroupFactoryTests.java b/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/SharedGroupFactoryTests.java index 7cd34ad02d5a9..a72e2c7b69465 100644 --- a/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/SharedGroupFactoryTests.java +++ b/modules/transport-netty4/src/test/java/org/elasticsearch/transport/netty4/SharedGroupFactoryTests.java @@ -9,7 +9,6 @@ package org.elasticsearch.transport.netty4; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.http.netty4.Netty4HttpServerTransport; import org.elasticsearch.test.ESTestCase; public final class SharedGroupFactoryTests extends ESTestCase { @@ -37,9 +36,7 @@ public void testSharedEventLoops() throws Exception { } public void testNonSharedEventLoops() throws Exception { - Settings settings = Settings.builder() - .put(Netty4HttpServerTransport.SETTING_HTTP_WORKER_COUNT.getKey(), randomIntBetween(1, 10)) - .build(); + Settings settings = Settings.builder().put(Netty4Plugin.SETTING_HTTP_WORKER_COUNT.getKey(), randomIntBetween(1, 10)).build(); SharedGroupFactory sharedGroupFactory = new SharedGroupFactory(settings); SharedGroupFactory.SharedGroup httpGroup = sharedGroupFactory.getHttpGroup(); SharedGroupFactory.SharedGroup transportGroup = sharedGroupFactory.getTransportGroup(); diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/NativeRealmIntegTestCase.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/NativeRealmIntegTestCase.java index e28fead63d4ea..f4c3b77af3abe 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/NativeRealmIntegTestCase.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/NativeRealmIntegTestCase.java @@ -11,7 +11,7 @@ import org.elasticsearch.client.RestClient; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.transport.netty4.Netty4Transport; +import org.elasticsearch.transport.netty4.Netty4Plugin; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.user.APMSystemUser; import org.elasticsearch.xpack.core.security.user.BeatsSystemUser; @@ -63,7 +63,7 @@ protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { // we are randomly running a large number of nodes in these tests so we limit the number of worker threads // since the default of 2 * CPU count might use up too much direct memory for thread-local direct buffers for each node's // transport threads - builder.put(Netty4Transport.WORKER_COUNT.getKey(), random().nextInt(3) + 1); + builder.put(Netty4Plugin.WORKER_COUNT.getKey(), random().nextInt(3) + 1); return builder.build(); } From 416de3cbc16746dbb84c7cce8f3247af79bb4df5 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 4 Mar 2024 07:36:27 +0000 Subject: [PATCH 207/250] Fix testTimeoutRejectionBehaviourAtSubmission (#105891) This test relies on the task being rejected when creating the timeout handler, but a zero timeout requires no timeout handler so the test doesn't work. Closes #105890 --- .../org/elasticsearch/cluster/service/MasterServiceTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/cluster/service/MasterServiceTests.java b/server/src/test/java/org/elasticsearch/cluster/service/MasterServiceTests.java index 5b933f6a2cec7..453d9bfecf2ab 100644 --- a/server/src/test/java/org/elasticsearch/cluster/service/MasterServiceTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/service/MasterServiceTests.java @@ -2121,12 +2121,11 @@ public void onFailure(Exception e) { } } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105890") public void testTimeoutRejectionBehaviourAtSubmission() { final var source = randomIdentifier(); final var taskDescription = randomIdentifier(); - final var timeout = TimeValue.timeValueMillis(between(0, 100000)); + final var timeout = TimeValue.timeValueMillis(between(1, 100000)); final var actionCount = new AtomicInteger(); final var deterministicTaskQueue = new DeterministicTaskQueue(); From 42786aa83b929a6ede7fac3e8226554aab6b2e64 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 4 Mar 2024 08:37:21 +0000 Subject: [PATCH 208/250] Reduce copying in `GetSnapshotsOperation#snapshots()` (#105765) No need to create a set (the values are distinct anyway), make a separate synchronized list, populate them both, and finally copy them both again into another concatenated list. We can just make the final list up front. --- .../get/TransportGetSnapshotsAction.java | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java index 4996096492354..ef4ebec8c2dfc 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java @@ -400,7 +400,7 @@ private void snapshots(String repositoryName, Collection snapshotIds if (cancellableTask.notifyIfCancelled(listener)) { return; } - final Set snapshotSet = new HashSet<>(); + final List snapshots = new ArrayList<>(snapshotIds.size()); final Set snapshotIdsToIterate = new HashSet<>(snapshotIds); // first, look at the snapshots in progress final List entries = SnapshotsService.currentSnapshots( @@ -412,46 +412,46 @@ private void snapshots(String repositoryName, Collection snapshotIds if (snapshotIdsToIterate.remove(entry.snapshot().getSnapshotId())) { final SnapshotInfo snapshotInfo = SnapshotInfo.inProgress(entry); if (predicates.test(snapshotInfo)) { - snapshotSet.add(snapshotInfo.maybeWithoutIndices(indices)); + snapshots.add(snapshotInfo.maybeWithoutIndices(indices)); } } } // then, look in the repository if there's any matching snapshots left - final List snapshotInfos; - if (snapshotIdsToIterate.isEmpty()) { - snapshotInfos = Collections.emptyList(); - } else { - snapshotInfos = Collections.synchronizedList(new ArrayList<>()); - } - final ActionListener allDoneListener = listener.safeMap(v -> { - final ArrayList snapshotList = new ArrayList<>(snapshotInfos); - snapshotList.addAll(snapshotSet); - return sortSnapshotsWithNoOffsetOrLimit(snapshotList); - }); - if (snapshotIdsToIterate.isEmpty()) { - allDoneListener.onResponse(null); - return; - } - final Repository repository; - try { - repository = repositoriesService.repository(repositoryName); - } catch (RepositoryMissingException e) { - listener.onFailure(e); - return; - } - repository.getSnapshotInfo( - new GetSnapshotInfoContext( - snapshotIdsToIterate, - ignoreUnavailable == false, - cancellableTask::isCancelled, - (context, snapshotInfo) -> { - if (predicates.test(snapshotInfo)) { - snapshotInfos.add(snapshotInfo.maybeWithoutIndices(indices)); - } - }, - allDoneListener + try ( + var listeners = new RefCountingListener( + // no need to synchronize access to snapshots: Repository#getSnapshotInfo fails fast but we're on the success path here + listener.safeMap(v -> sortSnapshotsWithNoOffsetOrLimit(snapshots)) ) - ); + ) { + if (snapshotIdsToIterate.isEmpty()) { + return; + } + + final Repository repository; + try { + repository = repositoriesService.repository(repositoryName); + } catch (RepositoryMissingException e) { + listeners.acquire().onFailure(e); + return; + } + + // only need to synchronize accesses related to reading SnapshotInfo from the repo + final List syncSnapshots = Collections.synchronizedList(snapshots); + + repository.getSnapshotInfo( + new GetSnapshotInfoContext( + snapshotIdsToIterate, + ignoreUnavailable == false, + cancellableTask::isCancelled, + (context, snapshotInfo) -> { + if (predicates.test(snapshotInfo)) { + syncSnapshots.add(snapshotInfo.maybeWithoutIndices(indices)); + } + }, + listeners.acquire() + ) + ); + } } private boolean isCurrentSnapshotsOnly() { From 1978b1f30114bf6f8e038118965f6b5bd7009394 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 4 Mar 2024 10:18:46 +0100 Subject: [PATCH 209/250] Cache set of metric names in NodesInfoMetrics (#105888) No need to compute this over and over. Saw a couple of duplicates of this in a heap dump and noticed this method popping up randomly in profiles as well => just cache this thing and use a faster immutable set for it. --- .../cluster/node/info/NodesInfoMetrics.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/info/NodesInfoMetrics.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/info/NodesInfoMetrics.java index 3e632f9bdd212..39e210571f37b 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/node/info/NodesInfoMetrics.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/node/info/NodesInfoMetrics.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.util.Arrays; +import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; @@ -21,13 +22,14 @@ * This class is a container that encapsulates the necessary information needed to indicate which node information is requested. */ public class NodesInfoMetrics implements Writeable { - private Set requestedMetrics = Metric.allMetrics(); + private final Set requestedMetrics; - public NodesInfoMetrics() {} + public NodesInfoMetrics() { + requestedMetrics = new HashSet<>(Metric.allMetrics()); + } public NodesInfoMetrics(StreamInput in) throws IOException { - requestedMetrics.clear(); - requestedMetrics.addAll(Arrays.asList(in.readStringArray())); + requestedMetrics = in.readCollectionAsImmutableSet(StreamInput::readString); } public Set requestedMetrics() { @@ -36,7 +38,7 @@ public Set requestedMetrics() { @Override public void writeTo(StreamOutput out) throws IOException { - out.writeStringArray(requestedMetrics.toArray(new String[0])); + out.writeStringCollection(requestedMetrics); } /** @@ -58,6 +60,10 @@ public enum Metric { AGGREGATIONS("aggregations"), INDICES("indices"); + private static final Set ALL_METRICS = Arrays.stream(values()) + .map(Metric::metricName) + .collect(Collectors.toUnmodifiableSet()); + private final String metricName; Metric(String name) { @@ -69,7 +75,7 @@ public String metricName() { } public static Set allMetrics() { - return Arrays.stream(values()).map(Metric::metricName).collect(Collectors.toSet()); + return ALL_METRICS; } } } From 987b778bb676e2f9790c2be665792a4e201bbb58 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 4 Mar 2024 10:46:43 +0100 Subject: [PATCH 210/250] Extract cold(ish) paths in some SearchPhases (#105884) The way these phases are currently executed often means that large parts of the phase run code never runs. Lets move it to separate methods to help the compiler and more importantly, make profiling easier to interpret. --- .../action/search/ExpandSearchPhase.java | 102 +++++++++--------- .../action/search/FetchLookupFieldsPhase.java | 4 + ...SearchScrollQueryThenFetchAsyncAction.java | 3 + 3 files changed, 59 insertions(+), 50 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/ExpandSearchPhase.java b/server/src/main/java/org/elasticsearch/action/search/ExpandSearchPhase.java index 149cdb9206b34..48c2f1890ba08 100644 --- a/server/src/main/java/org/elasticsearch/action/search/ExpandSearchPhase.java +++ b/server/src/main/java/org/elasticsearch/action/search/ExpandSearchPhase.java @@ -44,66 +44,68 @@ final class ExpandSearchPhase extends SearchPhase { * Returns true iff the search request has inner hits and needs field collapsing */ private boolean isCollapseRequest() { - final SearchRequest searchRequest = context.getRequest(); - return searchRequest.source() != null - && searchRequest.source().collapse() != null - && searchRequest.source().collapse().getInnerHits().isEmpty() == false; + final var searchSource = context.getRequest().source(); + return searchSource != null && searchSource.collapse() != null && searchSource.collapse().getInnerHits().isEmpty() == false; } @Override public void run() { - if (isCollapseRequest() && searchHits.getHits().length > 0) { - SearchRequest searchRequest = context.getRequest(); - CollapseBuilder collapseBuilder = searchRequest.source().collapse(); - final List innerHitBuilders = collapseBuilder.getInnerHits(); - MultiSearchRequest multiRequest = new MultiSearchRequest(); - if (collapseBuilder.getMaxConcurrentGroupRequests() > 0) { - multiRequest.maxConcurrentSearchRequests(collapseBuilder.getMaxConcurrentGroupRequests()); + if (isCollapseRequest() == false || searchHits.getHits().length == 0) { + onPhaseDone(); + } else { + doRun(); + } + } + + private void doRun() { + SearchRequest searchRequest = context.getRequest(); + CollapseBuilder collapseBuilder = searchRequest.source().collapse(); + final List innerHitBuilders = collapseBuilder.getInnerHits(); + MultiSearchRequest multiRequest = new MultiSearchRequest(); + if (collapseBuilder.getMaxConcurrentGroupRequests() > 0) { + multiRequest.maxConcurrentSearchRequests(collapseBuilder.getMaxConcurrentGroupRequests()); + } + for (SearchHit hit : searchHits.getHits()) { + BoolQueryBuilder groupQuery = new BoolQueryBuilder(); + Object collapseValue = hit.field(collapseBuilder.getField()).getValue(); + if (collapseValue != null) { + groupQuery.filter(QueryBuilders.matchQuery(collapseBuilder.getField(), collapseValue)); + } else { + groupQuery.mustNot(QueryBuilders.existsQuery(collapseBuilder.getField())); + } + QueryBuilder origQuery = searchRequest.source().query(); + if (origQuery != null) { + groupQuery.must(origQuery); + } + for (InnerHitBuilder innerHitBuilder : innerHitBuilders) { + CollapseBuilder innerCollapseBuilder = innerHitBuilder.getInnerCollapseBuilder(); + SearchSourceBuilder sourceBuilder = buildExpandSearchSourceBuilder(innerHitBuilder, innerCollapseBuilder).query(groupQuery) + .postFilter(searchRequest.source().postFilter()) + .runtimeMappings(searchRequest.source().runtimeMappings()); + SearchRequest groupRequest = new SearchRequest(searchRequest); + groupRequest.source(sourceBuilder); + multiRequest.add(groupRequest); } + } + context.getSearchTransport().sendExecuteMultiSearch(multiRequest, context.getTask(), ActionListener.wrap(response -> { + Iterator it = response.iterator(); for (SearchHit hit : searchHits.getHits()) { - BoolQueryBuilder groupQuery = new BoolQueryBuilder(); - Object collapseValue = hit.field(collapseBuilder.getField()).getValue(); - if (collapseValue != null) { - groupQuery.filter(QueryBuilders.matchQuery(collapseBuilder.getField(), collapseValue)); - } else { - groupQuery.mustNot(QueryBuilders.existsQuery(collapseBuilder.getField())); - } - QueryBuilder origQuery = searchRequest.source().query(); - if (origQuery != null) { - groupQuery.must(origQuery); - } for (InnerHitBuilder innerHitBuilder : innerHitBuilders) { - CollapseBuilder innerCollapseBuilder = innerHitBuilder.getInnerCollapseBuilder(); - SearchSourceBuilder sourceBuilder = buildExpandSearchSourceBuilder(innerHitBuilder, innerCollapseBuilder).query( - groupQuery - ).postFilter(searchRequest.source().postFilter()).runtimeMappings(searchRequest.source().runtimeMappings()); - SearchRequest groupRequest = new SearchRequest(searchRequest); - groupRequest.source(sourceBuilder); - multiRequest.add(groupRequest); - } - } - context.getSearchTransport().sendExecuteMultiSearch(multiRequest, context.getTask(), ActionListener.wrap(response -> { - Iterator it = response.iterator(); - for (SearchHit hit : searchHits.getHits()) { - for (InnerHitBuilder innerHitBuilder : innerHitBuilders) { - MultiSearchResponse.Item item = it.next(); - if (item.isFailure()) { - context.onPhaseFailure(this, "failed to expand hits", item.getFailure()); - return; - } - SearchHits innerHits = item.getResponse().getHits(); - if (hit.getInnerHits() == null) { - hit.setInnerHits(Maps.newMapWithExpectedSize(innerHitBuilders.size())); - } - hit.getInnerHits().put(innerHitBuilder.getName(), innerHits); - innerHits.mustIncRef(); + MultiSearchResponse.Item item = it.next(); + if (item.isFailure()) { + context.onPhaseFailure(this, "failed to expand hits", item.getFailure()); + return; } + SearchHits innerHits = item.getResponse().getHits(); + if (hit.getInnerHits() == null) { + hit.setInnerHits(Maps.newMapWithExpectedSize(innerHitBuilders.size())); + } + hit.getInnerHits().put(innerHitBuilder.getName(), innerHits); + innerHits.mustIncRef(); } - onPhaseDone(); - }, context::onFailure)); - } else { + } onPhaseDone(); - } + }, context::onFailure)); } private static SearchSourceBuilder buildExpandSearchSourceBuilder(InnerHitBuilder options, CollapseBuilder innerCollapseBuilder) { diff --git a/server/src/main/java/org/elasticsearch/action/search/FetchLookupFieldsPhase.java b/server/src/main/java/org/elasticsearch/action/search/FetchLookupFieldsPhase.java index 9c50d534ac4ce..0605e23fc343c 100644 --- a/server/src/main/java/org/elasticsearch/action/search/FetchLookupFieldsPhase.java +++ b/server/src/main/java/org/elasticsearch/action/search/FetchLookupFieldsPhase.java @@ -75,6 +75,10 @@ public void run() { context.sendSearchResponse(searchResponse, queryResults); return; } + doRun(clusters); + } + + private void doRun(List clusters) { final MultiSearchRequest multiSearchRequest = new MultiSearchRequest(); for (Cluster cluster : clusters) { // Do not prepend the clusterAlias to the targetIndex if the search request is already on the remote cluster. diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchScrollQueryThenFetchAsyncAction.java b/server/src/main/java/org/elasticsearch/action/search/SearchScrollQueryThenFetchAsyncAction.java index bad0ed488d03b..793a5bfe4e9d4 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchScrollQueryThenFetchAsyncAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchScrollQueryThenFetchAsyncAction.java @@ -73,7 +73,10 @@ public void run() { sendResponse(reducedQueryPhase, fetchResults); return; } + doRun(scoreDocs, reducedQueryPhase); + } + private void doRun(ScoreDoc[] scoreDocs, SearchPhaseController.ReducedQueryPhase reducedQueryPhase) { final List[] docIdsToLoad = SearchPhaseController.fillDocIdsToLoad(queryResults.length(), scoreDocs); final ScoreDoc[] lastEmittedDocPerShard = SearchPhaseController.getLastEmittedDocPerShard( reducedQueryPhase, From b37e6e9d55862e0095166a7756945690901d8b56 Mon Sep 17 00:00:00 2001 From: Tim Grein Date: Mon, 4 Mar 2024 11:05:32 +0100 Subject: [PATCH 211/250] [Connectors API] Guard cancel and update error sync job endpoints with state machine (#105722) --- .../430_connector_sync_job_cancel.yml | 27 +++- .../450_connector_sync_job_error.yml | 13 +- .../syncjob/ConnectorSyncJobIndexService.java | 148 +++++++++++------ ...ncJobInvalidStatusTransitionException.java | 31 ++++ .../syncjob/ConnectorSyncJobStateMachine.java | 13 ++ .../ConnectorSyncJobIndexServiceTests.java | 152 +++++++++++++++++- .../ConnectorSyncJobStateMachineTests.java | 27 ++++ 7 files changed, 350 insertions(+), 61 deletions(-) create mode 100644 x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobInvalidStatusTransitionException.java diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/430_connector_sync_job_cancel.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/430_connector_sync_job_cancel.yml index 633c1a8cecb7b..eea4ca197614d 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/430_connector_sync_job_cancel.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/430_connector_sync_job_cancel.yml @@ -13,7 +13,7 @@ setup: service_type: super-connector --- -"Cancel a Connector Sync Job": +"Cancel a pending Connector Sync Job - transition to canceled directly": - do: connector_sync_job.post: body: @@ -33,8 +33,31 @@ setup: connector_sync_job.get: connector_sync_job_id: $sync-job-id-to-cancel - - match: { status: "canceling"} + - set: { cancelation_requested_at: cancelation_requested_at } + - match: { status: "canceled"} + - match: { completed_at: $cancelation_requested_at } + - match: { canceled_at: $cancelation_requested_at } + +--- +"Cancel a canceled Connector Sync Job - invalid state transition from canceled to canceling": + - do: + connector_sync_job.post: + body: + id: test-connector + job_type: full + trigger_method: on_demand + + - set: { id: sync-job-id-to-cancel } + + - do: + connector_sync_job.cancel: + connector_sync_job_id: $sync-job-id-to-cancel + + - do: + catch: bad_request + connector_sync_job.cancel: + connector_sync_job_id: $sync-job-id-to-cancel --- "Cancel a Connector Sync Job - Connector Sync Job does not exist": diff --git a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/450_connector_sync_job_error.yml b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/450_connector_sync_job_error.yml index a565d28c3e788..78cfdb845b10e 100644 --- a/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/450_connector_sync_job_error.yml +++ b/x-pack/plugin/ent-search/qa/rest/src/yamlRestTest/resources/rest-api-spec/test/entsearch/450_connector_sync_job_error.yml @@ -13,7 +13,7 @@ setup: service_type: super-connector --- -"Set an error for a connector sync job": +"Set an error for a pending connector sync job - invalid state transition from pending to error": - do: connector_sync_job.post: body: @@ -24,21 +24,12 @@ setup: - set: { id: id } - do: + catch: bad_request connector_sync_job.error: connector_sync_job_id: $id body: error: error - - match: { result: updated } - - - do: - connector_sync_job.get: - connector_sync_job_id: $id - - - match: { error: error } - - match: { status: error } - - --- "Set an error for a Connector Sync Job - Connector Sync Job does not exist": - do: diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexService.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexService.java index 910f0605ef7aa..3ac598fd58ee8 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexService.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexService.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.application.connector.syncjob; +import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionListener; @@ -31,6 +32,7 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.TermQueryBuilder; import org.elasticsearch.index.query.TermsQueryBuilder; +import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.sort.SortOrder; @@ -219,38 +221,72 @@ public void getConnectorSyncJob(String connectorSyncJobId, ActionListener listener) { - Instant cancellationRequestedAt = Instant.now(); + try { + getConnectorSyncJob(connectorSyncJobId, listener.delegateFailure((getSyncJobListener, syncJobSearchResult) -> { + Map syncJobFieldsToUpdate; + Instant now = Instant.now(); - final UpdateRequest updateRequest = new UpdateRequest(CONNECTOR_SYNC_JOB_INDEX_NAME, connectorSyncJobId).setRefreshPolicy( - WriteRequest.RefreshPolicy.IMMEDIATE - ) - .doc( - Map.of( - ConnectorSyncJob.STATUS_FIELD.getPreferredName(), - ConnectorSyncStatus.CANCELING, - ConnectorSyncJob.CANCELATION_REQUESTED_AT_FIELD.getPreferredName(), - cancellationRequestedAt - ) - ); + ConnectorSyncStatus prevStatus = getConnectorSyncJobStatusFromSearchResult(syncJobSearchResult); - try { - client.update( - updateRequest, - new DelegatingIndexNotFoundOrDocumentMissingActionListener<>(connectorSyncJobId, listener, (l, updateResponse) -> { - if (updateResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) { - l.onFailure(new ResourceNotFoundException(connectorSyncJobId)); - return; + try { + if (ConnectorSyncStatus.PENDING.equals(prevStatus) || ConnectorSyncStatus.SUSPENDED.equals(prevStatus)) { + // A pending or suspended non-running sync job is set to `canceled` directly + // without a transition to the in-between `canceling` status + ConnectorSyncStatus nextStatus = ConnectorSyncStatus.CANCELED; + ConnectorSyncJobStateMachine.assertValidStateTransition(prevStatus, nextStatus); + + syncJobFieldsToUpdate = Map.of( + ConnectorSyncJob.STATUS_FIELD.getPreferredName(), + nextStatus, + ConnectorSyncJob.CANCELATION_REQUESTED_AT_FIELD.getPreferredName(), + now, + ConnectorSyncJob.CANCELED_AT_FIELD.getPreferredName(), + now, + ConnectorSyncJob.COMPLETED_AT_FIELD.getPreferredName(), + now + ); + } else { + ConnectorSyncStatus nextStatus = ConnectorSyncStatus.CANCELING; + ConnectorSyncJobStateMachine.assertValidStateTransition(prevStatus, nextStatus); + + syncJobFieldsToUpdate = Map.of( + ConnectorSyncJob.STATUS_FIELD.getPreferredName(), + nextStatus, + ConnectorSyncJob.CANCELATION_REQUESTED_AT_FIELD.getPreferredName(), + now + ); } - l.onResponse(updateResponse); - }) - ); + } catch (ConnectorSyncJobInvalidStatusTransitionException e) { + getSyncJobListener.onFailure(new ElasticsearchStatusException(e.getMessage(), RestStatus.BAD_REQUEST, e)); + return; + } + + final UpdateRequest updateRequest = new UpdateRequest(CONNECTOR_SYNC_JOB_INDEX_NAME, connectorSyncJobId).setRefreshPolicy( + WriteRequest.RefreshPolicy.IMMEDIATE + ).doc(syncJobFieldsToUpdate); + + client.update( + updateRequest, + new DelegatingIndexNotFoundOrDocumentMissingActionListener<>( + connectorSyncJobId, + listener, + (indexNotFoundListener, updateResponse) -> { + if (updateResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) { + indexNotFoundListener.onFailure(new ResourceNotFoundException(connectorSyncJobId)); + return; + } + indexNotFoundListener.onResponse(updateResponse); + } + ) + ); + })); } catch (Exception e) { listener.onFailure(e); } @@ -415,6 +451,12 @@ public void updateConnectorSyncJobIngestionStats( } + private ConnectorSyncStatus getConnectorSyncJobStatusFromSearchResult(ConnectorSyncJobSearchResult searchResult) { + return ConnectorSyncStatus.connectorSyncStatus( + (String) searchResult.getResultMap().get(ConnectorSyncJob.STATUS_FIELD.getPreferredName()) + ); + } + private void getSyncJobConnectorInfo(String connectorId, ActionListener listener) { try { @@ -485,29 +527,45 @@ FilteringRules transformConnectorFilteringToSyncJobRepresentation(List listener) { - final UpdateRequest updateRequest = new UpdateRequest(CONNECTOR_SYNC_JOB_INDEX_NAME, connectorSyncJobId).setRefreshPolicy( - WriteRequest.RefreshPolicy.IMMEDIATE - ) - .doc( - Map.of( - ConnectorSyncJob.ERROR_FIELD.getPreferredName(), - error, - ConnectorSyncJob.STATUS_FIELD.getPreferredName(), - ConnectorSyncStatus.ERROR + try { + getConnectorSyncJob(connectorSyncJobId, listener.delegateFailure((getSyncJobListener, syncJobSearchResult) -> { + ConnectorSyncStatus prevStatus = getConnectorSyncJobStatusFromSearchResult(syncJobSearchResult); + ConnectorSyncStatus nextStatus = ConnectorSyncStatus.ERROR; + + try { + ConnectorSyncJobStateMachine.assertValidStateTransition(prevStatus, nextStatus); + } catch (ConnectorSyncJobInvalidStatusTransitionException e) { + getSyncJobListener.onFailure(new ElasticsearchStatusException(e.getMessage(), RestStatus.BAD_REQUEST, e)); + return; + } + + final UpdateRequest updateRequest = new UpdateRequest(CONNECTOR_SYNC_JOB_INDEX_NAME, connectorSyncJobId).setRefreshPolicy( + WriteRequest.RefreshPolicy.IMMEDIATE ) - ); + .doc( + Map.of( + ConnectorSyncJob.ERROR_FIELD.getPreferredName(), + error, + ConnectorSyncJob.STATUS_FIELD.getPreferredName(), + nextStatus + ) + ); - try { - client.update( - updateRequest, - new DelegatingIndexNotFoundOrDocumentMissingActionListener<>(connectorSyncJobId, listener, (l, updateResponse) -> { - if (updateResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) { - l.onFailure(new ResourceNotFoundException(connectorSyncJobId)); - return; - } - l.onResponse(updateResponse); - }) - ); + client.update( + updateRequest, + new DelegatingIndexNotFoundOrDocumentMissingActionListener<>( + connectorSyncJobId, + listener, + (indexNotFoundListener, updateResponse) -> { + if (updateResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) { + indexNotFoundListener.onFailure(new ResourceNotFoundException(connectorSyncJobId)); + return; + } + indexNotFoundListener.onResponse(updateResponse); + } + ) + ); + })); } catch (Exception e) { listener.onFailure(e); } diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobInvalidStatusTransitionException.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobInvalidStatusTransitionException.java new file mode 100644 index 0000000000000..3ded62afa5d14 --- /dev/null +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobInvalidStatusTransitionException.java @@ -0,0 +1,31 @@ +/* + * 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.application.connector.syncjob; + +import org.elasticsearch.xpack.application.connector.ConnectorSyncStatus; + +public class ConnectorSyncJobInvalidStatusTransitionException extends Exception { + + /** + * Constructs a {@link ConnectorSyncJobInvalidStatusTransitionException} exception with a detailed message. + * + * @param current The current {@link ConnectorSyncStatus} of the {@link ConnectorSyncJob}. + * @param next The attempted next {@link ConnectorSyncStatus} of the {@link ConnectorSyncJob}. + */ + public ConnectorSyncJobInvalidStatusTransitionException(ConnectorSyncStatus current, ConnectorSyncStatus next) { + super( + "Invalid transition attempt from [" + + current + + "] to [" + + next + + "]. Such a " + + ConnectorSyncStatus.class.getSimpleName() + + " transition is not supported by the Connector Protocol." + ); + } +} diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobStateMachine.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobStateMachine.java index 7a7a05bd5e455..dc624b5bf8ba1 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobStateMachine.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobStateMachine.java @@ -47,4 +47,17 @@ public class ConnectorSyncJobStateMachine { public static boolean isValidTransition(ConnectorSyncStatus current, ConnectorSyncStatus next) { return VALID_TRANSITIONS.getOrDefault(current, Collections.emptySet()).contains(next); } + + /** + * Throws {@link ConnectorSyncJobInvalidStatusTransitionException} if a + * transition from one {@link ConnectorSyncStatus} to another is invalid. + * + * @param current The current {@link ConnectorSyncStatus} of the {@link ConnectorSyncJob}. + * @param next The proposed next {@link ConnectorSyncStatus} of the {@link ConnectorSyncJob}. + */ + public static void assertValidStateTransition(ConnectorSyncStatus current, ConnectorSyncStatus next) + throws ConnectorSyncJobInvalidStatusTransitionException { + if (isValidTransition(current, next)) return; + throw new ConnectorSyncJobInvalidStatusTransitionException(current, next); + } } diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexServiceTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexServiceTests.java index 4a7a3e76ecf42..d486a27f1b728 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexServiceTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobIndexServiceTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.application.connector.syncjob; +import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.ActionFuture; import org.elasticsearch.action.ActionListener; @@ -17,6 +18,7 @@ import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.test.ESSingleNodeTestCase; @@ -80,7 +82,6 @@ public void setup() throws Exception { } private String createConnector() throws IOException, InterruptedException, ExecutionException, TimeoutException { - Connector connector = ConnectorTestUtils.getRandomConnector(); final IndexRequest indexRequest = new IndexRequest(ConnectorIndexService.CONNECTOR_INDEX_NAME).opType(DocWriteRequest.OpType.INDEX) @@ -229,7 +230,8 @@ public void testCheckInConnectorSyncJob_WithMissingSyncJobId_ExpectException() { expectThrows(ResourceNotFoundException.class, () -> awaitCheckInConnectorSyncJob(NON_EXISTING_SYNC_JOB_ID)); } - public void testCancelConnectorSyncJob() throws Exception { + public void testCancelConnectorSyncJob_WithStatusInProgress_ExpectNextStatusCanceling() throws Exception { + // Create connector sync job PostConnectorSyncJobAction.Request syncJobRequest = ConnectorSyncJobTestUtils.getRandomPostConnectorSyncJobActionRequest( connectorOneId ); @@ -247,6 +249,10 @@ public void testCancelConnectorSyncJob() throws Exception { assertThat(cancellationRequestedAtBeforeUpdate, nullValue()); assertThat(syncStatusBeforeUpdate, not(equalTo(ConnectorSyncStatus.CANCELING))); + // Set sync job status to `in_progress` + updateConnectorSyncJobStatusWithoutStateMachineGuard(syncJobId, ConnectorSyncStatus.IN_PROGRESS); + + // Cancel sync job UpdateResponse updateResponse = awaitCancelConnectorSyncJob(syncJobId); Map syncJobSourceAfterUpdate = getConnectorSyncJobSourceById(syncJobId); @@ -263,6 +269,103 @@ public void testCancelConnectorSyncJob() throws Exception { assertFieldsExceptSyncStatusAndCancellationRequestedAtDidNotUpdate(syncJobSourceBeforeUpdate, syncJobSourceAfterUpdate); } + public void testCancelConnectorSyncJob_WithPendingState_ExpectNextStatusCanceled() throws Exception { + // Create pending sync job + PostConnectorSyncJobAction.Request syncJobRequest = ConnectorSyncJobTestUtils.getRandomPostConnectorSyncJobActionRequest( + connectorOneId + ); + PostConnectorSyncJobAction.Response response = awaitPutConnectorSyncJob(syncJobRequest); + String syncJobId = response.getId(); + Map syncJobSourceBeforeUpdate = getConnectorSyncJobSourceById(syncJobId); + ConnectorSyncStatus syncStatusBeforeUpdate = ConnectorSyncStatus.fromString( + (String) syncJobSourceBeforeUpdate.get(ConnectorSyncJob.STATUS_FIELD.getPreferredName()) + ); + Object canceledAtBeforeUpdate = syncJobSourceBeforeUpdate.get(ConnectorSyncJob.CANCELED_AT_FIELD.getPreferredName()); + + assertThat(syncJobId, notNullValue()); + assertThat(canceledAtBeforeUpdate, nullValue()); + assertThat(syncStatusBeforeUpdate, not(equalTo(ConnectorSyncStatus.CANCELED))); + + // Cancel sync job + UpdateResponse updateResponse = awaitCancelConnectorSyncJob(syncJobId); + + Map syncJobSourceAfterUpdate = getConnectorSyncJobSourceById(syncJobId); + ConnectorSyncStatus syncStatusAfterUpdate = ConnectorSyncStatus.fromString( + (String) syncJobSourceAfterUpdate.get(ConnectorSyncJob.STATUS_FIELD.getPreferredName()) + ); + Instant canceledAtAfterUpdate = Instant.parse( + (String) syncJobSourceAfterUpdate.get(ConnectorSyncJob.CANCELED_AT_FIELD.getPreferredName()) + ); + + assertThat(updateResponse.status(), equalTo(RestStatus.OK)); + assertThat(canceledAtAfterUpdate, notNullValue()); + assertThat(syncStatusAfterUpdate, equalTo(ConnectorSyncStatus.CANCELED)); + assertFieldsExceptSyncStatusAndCanceledAndCompletedTimestampsDidNotUpdate(syncJobSourceBeforeUpdate, syncJobSourceAfterUpdate); + } + + public void testCancelConnectorSyncJob_WithSuspendedState_ExpectNextStatusCanceled() throws Exception { + // Create pending sync job + PostConnectorSyncJobAction.Request syncJobRequest = ConnectorSyncJobTestUtils.getRandomPostConnectorSyncJobActionRequest( + connectorOneId + ); + PostConnectorSyncJobAction.Response response = awaitPutConnectorSyncJob(syncJobRequest); + String syncJobId = response.getId(); + Map syncJobSourceBeforeUpdate = getConnectorSyncJobSourceById(syncJobId); + ConnectorSyncStatus syncStatusBeforeUpdate = ConnectorSyncStatus.fromString( + (String) syncJobSourceBeforeUpdate.get(ConnectorSyncJob.STATUS_FIELD.getPreferredName()) + ); + Object canceledAtBeforeUpdate = syncJobSourceBeforeUpdate.get(ConnectorSyncJob.CANCELED_AT_FIELD.getPreferredName()); + + assertThat(syncJobId, notNullValue()); + assertThat(canceledAtBeforeUpdate, nullValue()); + assertThat(syncStatusBeforeUpdate, not(equalTo(ConnectorSyncStatus.CANCELED))); + + // Set sync job to suspended + updateConnectorSyncJobStatusWithoutStateMachineGuard(syncJobId, ConnectorSyncStatus.SUSPENDED); + + // Cancel sync job + UpdateResponse updateResponse = awaitCancelConnectorSyncJob(syncJobId); + + Map syncJobSourceAfterUpdate = getConnectorSyncJobSourceById(syncJobId); + ConnectorSyncStatus syncStatusAfterUpdate = ConnectorSyncStatus.fromString( + (String) syncJobSourceAfterUpdate.get(ConnectorSyncJob.STATUS_FIELD.getPreferredName()) + ); + Instant canceledAtAfterUpdate = Instant.parse( + (String) syncJobSourceAfterUpdate.get(ConnectorSyncJob.CANCELED_AT_FIELD.getPreferredName()) + ); + + assertThat(updateResponse.status(), equalTo(RestStatus.OK)); + assertThat(canceledAtAfterUpdate, notNullValue()); + assertThat(syncStatusAfterUpdate, equalTo(ConnectorSyncStatus.CANCELED)); + assertFieldsExceptSyncStatusAndCanceledAndCompletedTimestampsDidNotUpdate(syncJobSourceBeforeUpdate, syncJobSourceAfterUpdate); + } + + public void testCancelConnectorSyncJob_WithCompletedState_ExpectStatusException() throws Exception { + // Create sync job + PostConnectorSyncJobAction.Request syncJobRequest = ConnectorSyncJobTestUtils.getRandomPostConnectorSyncJobActionRequest( + connectorOneId + ); + PostConnectorSyncJobAction.Response response = awaitPutConnectorSyncJob(syncJobRequest); + String syncJobId = response.getId(); + Map syncJobSourceBeforeUpdate = getConnectorSyncJobSourceById(syncJobId); + ConnectorSyncStatus syncStatusBeforeUpdate = ConnectorSyncStatus.fromString( + (String) syncJobSourceBeforeUpdate.get(ConnectorSyncJob.STATUS_FIELD.getPreferredName()) + ); + Object cancellationRequestedAtBeforeUpdate = syncJobSourceBeforeUpdate.get( + ConnectorSyncJob.CANCELATION_REQUESTED_AT_FIELD.getPreferredName() + ); + + assertThat(syncJobId, notNullValue()); + assertThat(cancellationRequestedAtBeforeUpdate, nullValue()); + assertThat(syncStatusBeforeUpdate, not(equalTo(ConnectorSyncStatus.CANCELING))); + + // Set sync job status to `completed` + updateConnectorSyncJobStatusWithoutStateMachineGuard(syncJobId, ConnectorSyncStatus.COMPLETED); + + // Cancel sync job + assertThrows(ElasticsearchStatusException.class, () -> awaitCancelConnectorSyncJob(syncJobId)); + } + public void testCancelConnectorSyncJob_WithMissingSyncJobId_ExpectException() { expectThrows(ResourceNotFoundException.class, () -> awaitCancelConnectorSyncJob(NON_EXISTING_SYNC_JOB_ID)); } @@ -332,7 +435,7 @@ public void testListConnectorSyncJobs() throws Exception { assertTrue(secondSyncJob.getCreatedAt().isAfter(firstSyncJob.getCreatedAt())); } - public void testListConnectorSyncJobs_WithStatusPending_GivenOnePendingTwoCancelled_ExpectOnePending() throws Exception { + public void testListConnectorSyncJobs_WithStatusPending_GivenOnePendingTwoCanceled_ExpectOnePending() throws Exception { PostConnectorSyncJobAction.Request requestOne = ConnectorSyncJobTestUtils.getRandomPostConnectorSyncJobActionRequest( connectorOneId ); @@ -547,12 +650,17 @@ public void testListConnectorSyncJobs_WithNoSyncJobs_ReturnEmptyResult() throws } public void testUpdateConnectorSyncJobError() throws Exception { + // Create sync job PostConnectorSyncJobAction.Request syncJobRequest = ConnectorSyncJobTestUtils.getRandomPostConnectorSyncJobActionRequest( connectorOneId ); PostConnectorSyncJobAction.Response response = awaitPutConnectorSyncJob(syncJobRequest); String syncJobId = response.getId(); + // Set sync job to in progress + updateConnectorSyncJobStatusWithoutStateMachineGuard(syncJobId, ConnectorSyncStatus.IN_PROGRESS); + + // Set sync job error UpdateConnectorSyncJobErrorAction.Request request = ConnectorSyncJobTestUtils.getRandomUpdateConnectorSyncJobErrorActionRequest(); String errorInRequest = request.getError(); @@ -575,6 +683,18 @@ public void testUpdateConnectorSyncJobError_WithMissingSyncJobId_ExceptException ); } + public void testUpdateConnectorSyncJobError_WithStatusPending_ExpectStatusException() throws Exception { + // Create sync job + PostConnectorSyncJobAction.Request syncJobRequest = ConnectorSyncJobTestUtils.getRandomPostConnectorSyncJobActionRequest( + connectorOneId + ); + PostConnectorSyncJobAction.Response response = awaitPutConnectorSyncJob(syncJobRequest); + String syncJobId = response.getId(); + + // Try to set error + assertThrows(ElasticsearchStatusException.class, () -> awaitUpdateConnectorSyncJob(syncJobId, "some error")); + } + public void testUpdateConnectorSyncJobIngestionStats() throws Exception { PostConnectorSyncJobAction.Request syncJobRequest = ConnectorSyncJobTestUtils.getRandomPostConnectorSyncJobActionRequest( connectorOneId @@ -733,6 +853,22 @@ private static void assertFieldsExceptSyncStatusAndCancellationRequestedAtDidNot ); } + private static void assertFieldsExceptSyncStatusAndCanceledAndCompletedTimestampsDidNotUpdate( + Map syncJobSourceBeforeUpdate, + Map syncJobSourceAfterUpdate + ) { + assertFieldsDidNotUpdateExceptFieldList( + syncJobSourceBeforeUpdate, + syncJobSourceAfterUpdate, + List.of( + ConnectorSyncJob.STATUS_FIELD, + ConnectorSyncJob.CANCELED_AT_FIELD, + ConnectorSyncJob.COMPLETED_AT_FIELD, + ConnectorSyncJob.CANCELATION_REQUESTED_AT_FIELD + ) + ); + } + private static void assertFieldsExceptLastSeenDidNotUpdate( Map syncJobSourceBeforeUpdate, Map syncJobSourceAfterUpdate @@ -1006,4 +1142,14 @@ public void onFailure(Exception e) { return response; } + private String updateConnectorSyncJobStatusWithoutStateMachineGuard(String syncJobId, ConnectorSyncStatus syncStatus) throws Exception { + final UpdateRequest updateRequest = new UpdateRequest(ConnectorSyncJobIndexService.CONNECTOR_SYNC_JOB_INDEX_NAME, syncJobId) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .doc(Map.of(ConnectorSyncJob.STATUS_FIELD.getPreferredName(), syncStatus)); + + ActionFuture index = client().update(updateRequest); + + // wait 10 seconds for connector creation + return index.get(TIMEOUT_SECONDS, TimeUnit.SECONDS).getId(); + } } diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobStateMachineTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobStateMachineTests.java index b702a5ffa7eef..3e7bf80dcfb25 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobStateMachineTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/syncjob/ConnectorSyncJobStateMachineTests.java @@ -86,4 +86,31 @@ public void testTransitionToSameState() { ); } } + + public void testAssertValidStateTransition_ExpectExceptionOnInvalidTransition() { + assertThrows( + ConnectorSyncJobInvalidStatusTransitionException.class, + () -> ConnectorSyncJobStateMachine.assertValidStateTransition(ConnectorSyncStatus.PENDING, ConnectorSyncStatus.CANCELING) + ); + } + + public void testAssertValidStateTransition_ExpectNoExceptionOnValidTransition() { + ConnectorSyncStatus prevStatus = ConnectorSyncStatus.PENDING; + ConnectorSyncStatus nextStatus = ConnectorSyncStatus.CANCELED; + + try { + ConnectorSyncJobStateMachine.assertValidStateTransition(prevStatus, nextStatus); + } catch (ConnectorSyncJobInvalidStatusTransitionException e) { + fail( + "Did not expect " + + ConnectorSyncJobInvalidStatusTransitionException.class.getSimpleName() + + " to be thrown for valid state transition [" + + prevStatus + + "] -> " + + "[" + + nextStatus + + "]." + ); + } + } } From 0b664dd4d42dc3756b2e7aa39cc1556e7d139f09 Mon Sep 17 00:00:00 2001 From: Serena Chou Date: Mon, 4 Mar 2024 11:31:00 +0100 Subject: [PATCH 212/250] Update README.asciidoc (#103597) * Update README.asciidoc updating the readme with the latest blurb from PMM and a reference to RAG + a few links to search labs content. * Tweak verbiage --------- Co-authored-by: Liam Thompson <32779855+leemthompo@users.noreply.github.com> --- README.asciidoc | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index a8b3704887e5b..dc27735d3c015 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -1,20 +1,24 @@ = Elasticsearch -Elasticsearch is a distributed, RESTful search engine optimized for speed and relevance on production-scale workloads. You can use Elasticsearch to perform real-time search over massive datasets for applications including: +Elasticsearch is a distributed search and analytics engine optimized for speed and relevance on production-scale workloads. Elasticsearch is the foundation of Elastic's open Stack platform. Search in near real-time over massive datasets, perform vector searches, integrate with generative AI applications, and much more. -* Vector search +Use cases enabled by Elasticsearch include: + +* https://www.elastic.co/search-labs/blog/articles/retrieval-augmented-generation-rag[Retrieval Augmented Generation (RAG)] +* https://www.elastic.co/search-labs/blog/categories/vector-search[Vector search] * Full-text search * Logs * Metrics * Application performance monitoring (APM) * Security logs - \... and more! To learn more about Elasticsearch's features and capabilities, see our https://www.elastic.co/products/elasticsearch[product page]. +To access information on https://www.elastic.co/search-labs/blog/categories/ml-research[machine learning innovations] and the latest https://www.elastic.co/search-labs/blog/categories/lucene[Lucene contributions from Elastic], more information can be found in https://www.elastic.co/search-labs[Search Labs]. + [[get-started]] == Get started From cbb09d2676ecd0ed1ad49dd7a46f284c142015ad Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 4 Mar 2024 10:45:45 +0000 Subject: [PATCH 213/250] AwaitsFix for #101008 --- .../test/java/org/elasticsearch/index/shard/IndexShardTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java index b83334ec68fdd..97bf9f4e380fa 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardTests.java @@ -3800,6 +3800,7 @@ public void testIsSearchIdle() throws Exception { closeShards(primary); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/101008") @TestIssueLogging( issueUrl = "https://github.com/elastic/elasticsearch/issues/101008", value = "org.elasticsearch.index.shard.IndexShard:TRACE" From 294fa4d037f52c3f91328f596cbd28fbe70ea3ba Mon Sep 17 00:00:00 2001 From: Tim Grein Date: Mon, 4 Mar 2024 11:53:53 +0100 Subject: [PATCH 214/250] [Connectors API] Add more distinct test cases to PutConnectorSecretActionTests (#105809) --- .../action/PutConnectorSecretActionTests.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/PutConnectorSecretActionTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/PutConnectorSecretActionTests.java index b7c7453611bdf..7940017318336 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/PutConnectorSecretActionTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/PutConnectorSecretActionTests.java @@ -25,11 +25,34 @@ public void testValidate_WhenConnectorSecretIdIsPresent_ExpectNoValidationError( } public void testValidate_WhenConnectorSecretIdIsEmpty_ExpectValidationError() { - PutConnectorSecretRequest requestWithMissingValue = new PutConnectorSecretRequest("", ""); - ActionRequestValidationException exception = requestWithMissingValue.validate(); + PutConnectorSecretRequest requestWithEmptyId = new PutConnectorSecretRequest("", randomAlphaOfLength(10)); + ActionRequestValidationException exception = requestWithEmptyId.validate(); assertThat(exception, notNullValue()); assertThat(exception.getMessage(), containsString("[id] cannot be [null] or [\"\"]")); + } + + public void testValidate_WhenConnectorSecretIdIsNull_ExpectValidationError() { + PutConnectorSecretRequest requestWithNullId = new PutConnectorSecretRequest(null, randomAlphaOfLength(10)); + ActionRequestValidationException exception = requestWithNullId.validate(); + + assertThat(exception, notNullValue()); + assertThat(exception.getMessage(), containsString("[id] cannot be [null] or [\"\"]")); + } + + public void testValidate_WhenConnectorSecretValueIsEmpty_ExpectValidationError() { + PutConnectorSecretRequest requestWithEmptyValue = new PutConnectorSecretRequest(randomAlphaOfLength(10), ""); + ActionRequestValidationException exception = requestWithEmptyValue.validate(); + + assertThat(exception, notNullValue()); + assertThat(exception.getMessage(), containsString("[value] cannot be [null] or [\"\"]")); + } + + public void testValidate_WhenConnectorSecretValueIsNull_ExpectValidationError() { + PutConnectorSecretRequest requestWithEmptyValue = new PutConnectorSecretRequest(randomAlphaOfLength(10), null); + ActionRequestValidationException exception = requestWithEmptyValue.validate(); + + assertThat(exception, notNullValue()); assertThat(exception.getMessage(), containsString("[value] cannot be [null] or [\"\"]")); } } From 3b8177f2a1cea76ce63c18eb0bd10d0c22804caa Mon Sep 17 00:00:00 2001 From: Tim Grein Date: Mon, 4 Mar 2024 11:54:17 +0100 Subject: [PATCH 215/250] [Connectors API] Make validation error message consistent and add test case for null value. (#105810) --- .../action/PostConnectorSecretRequest.java | 16 ++++++++-------- .../action/PostConnectorSecretActionTests.java | 13 +++++++++++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/PostConnectorSecretRequest.java b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/PostConnectorSecretRequest.java index 2e565dece7eca..90672f7ca7120 100644 --- a/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/PostConnectorSecretRequest.java +++ b/x-pack/plugin/ent-search/src/main/java/org/elasticsearch/xpack/application/connector/secrets/action/PostConnectorSecretRequest.java @@ -21,15 +21,15 @@ import java.io.IOException; import java.util.Objects; +import static org.elasticsearch.action.ValidateActions.addValidationError; + public class PostConnectorSecretRequest extends ActionRequest { - public static final ParseField VALUE_FIELD = new ParseField("value"); + private static final ParseField VALUE_FIELD = new ParseField("value"); public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( "post_secret_request", - args -> { - return new PostConnectorSecretRequest((String) args[0]); - } + args -> new PostConnectorSecretRequest((String) args[0]) ); static { @@ -75,13 +75,13 @@ public void writeTo(StreamOutput out) throws IOException { @Override public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.isNullOrEmpty(this.value)) { - ActionRequestValidationException exception = new ActionRequestValidationException(); - exception.addValidationError("value is missing"); - return exception; + validationException = addValidationError("[value] of the connector secret cannot be [null] or [\"\"]", validationException); } - return null; + return validationException; } @Override diff --git a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/PostConnectorSecretActionTests.java b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/PostConnectorSecretActionTests.java index f1e1a670b2748..a11de91de739a 100644 --- a/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/PostConnectorSecretActionTests.java +++ b/x-pack/plugin/ent-search/src/test/java/org/elasticsearch/xpack/application/connector/secrets/action/PostConnectorSecretActionTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.application.connector.secrets.ConnectorSecretsTestUtils; +import static org.elasticsearch.xpack.application.connector.ConnectorTestUtils.NULL_STRING; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -24,11 +25,19 @@ public void testValidate_WhenConnectorSecretIdIsPresent_ExpectNoValidationError( assertThat(exception, nullValue()); } - public void testValidate_WhenConnectorSecretIdIsEmpty_ExpectValidationError() { + public void testValidate_WhenConnectorSecretIdIsNull_ExpectValidationError() { + PostConnectorSecretRequest requestWithNullValue = new PostConnectorSecretRequest(NULL_STRING); + ActionRequestValidationException exception = requestWithNullValue.validate(); + + assertThat(exception, notNullValue()); + assertThat(exception.getMessage(), containsString("[value] of the connector secret cannot be [null] or [\"\"]")); + } + + public void testValidate_WhenConnectorSecretIdIsBlank_ExpectValidationError() { PostConnectorSecretRequest requestWithMissingValue = new PostConnectorSecretRequest(""); ActionRequestValidationException exception = requestWithMissingValue.validate(); assertThat(exception, notNullValue()); - assertThat(exception.getMessage(), containsString("value is missing")); + assertThat(exception.getMessage(), containsString("[value] of the connector secret cannot be [null] or [\"\"]")); } } From c97160a8574679f0bec0b1b8c585339f9f400ba9 Mon Sep 17 00:00:00 2001 From: Andrei Dan Date: Mon, 4 Mar 2024 10:56:01 +0000 Subject: [PATCH 216/250] [ILM] Delete step deletes data stream with only one index (#105772) We seem to have a couple of checks to make sure we delete the data stream when the last index reaches the delete step however, these checks seem a bit contradictory. Namely, the first check makes use if `Index` equality (UUID included) and the second just checks the index name. So if a data stream with just one index (the write index) is restored from snapshot (different UUID) we would've failed the first index equality check and go through the second check `dataStream.getWriteIndex().getName().equals(indexName)` and fail the delete step (in a non-retryable way :( ) because we don't want to delete the write index of a data stream (but we really do if the data stream has only one index) This PR makes 2 changes: 1. use the index name equality everywhere in the step (we already looked up the index abstraction and the parent data stream, so we know for sure the managed index is part of the data stream) 2. do not throw exception when we got here via a write index that is NOT the last index in the data stream but report the exception so we keep retrying this step (i.e. this enables our users to simply execute a manual rollover and the index is deleted by ILM eventually on retry) --- docs/changelog/105772.yaml | 5 ++ .../xpack/core/ilm/DeleteStep.java | 8 ++- .../xpack/core/ilm/DeleteStepTests.java | 65 ++++++++++--------- .../xpack/ilm/TimeSeriesDataStreamsIT.java | 46 +++++++++++++ 4 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 docs/changelog/105772.yaml diff --git a/docs/changelog/105772.yaml b/docs/changelog/105772.yaml new file mode 100644 index 0000000000000..73680aa04e5ab --- /dev/null +++ b/docs/changelog/105772.yaml @@ -0,0 +1,5 @@ +pr: 105772 +summary: "[ILM] Delete step deletes data stream with only one index" +area: ILM+SLM +type: bug +issues: [] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/DeleteStep.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/DeleteStep.java index 755e453790257..ba6b6f9366c61 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/DeleteStep.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/DeleteStep.java @@ -41,7 +41,10 @@ public void performDuringNoSnapshot(IndexMetadata indexMetadata, ClusterState cu if (dataStream != null) { assert dataStream.getWriteIndex() != null : dataStream.getName() + " has no write index"; - if (dataStream.getIndices().size() == 1 && dataStream.getIndices().get(0).equals(indexMetadata.getIndex())) { + + // using index name equality across this if/else branch as the UUID of the index might change via restoring a data stream + // with one index from snapshot + if (dataStream.getIndices().size() == 1 && dataStream.getWriteIndex().getName().equals(indexName)) { // This is the last index in the data stream, the entire stream // needs to be deleted, because we can't have an empty data stream DeleteDataStreamAction.Request deleteReq = new DeleteDataStreamAction.Request(new String[] { dataStream.getName() }); @@ -62,7 +65,8 @@ public void performDuringNoSnapshot(IndexMetadata indexMetadata, ClusterState cu policyName ); logger.debug(errorMessage); - throw new IllegalStateException(errorMessage); + listener.onFailure(new IllegalStateException(errorMessage)); + return; } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/DeleteStepTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/DeleteStepTests.java index 32e9148de067c..5851ebe2fb3c9 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/DeleteStepTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ilm/DeleteStepTests.java @@ -21,7 +21,10 @@ import java.util.List; +import static org.elasticsearch.test.ActionListenerUtils.anyActionListener; import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; public class DeleteStepTests extends AbstractStepTestCase { @@ -76,7 +79,7 @@ public void testDeleted() throws Exception { assertEquals(indexMetadata.getIndex().getName(), request.indices()[0]); listener.onResponse(null); return null; - }).when(indicesClient).delete(Mockito.any(), Mockito.any()); + }).when(indicesClient).delete(any(), any()); DeleteStep step = createRandomInstance(); ClusterState clusterState = ClusterState.builder(emptyClusterState()) @@ -86,7 +89,7 @@ public void testDeleted() throws Exception { Mockito.verify(client, Mockito.only()).admin(); Mockito.verify(adminClient, Mockito.only()).indices(); - Mockito.verify(indicesClient, Mockito.only()).delete(Mockito.any(), Mockito.any()); + Mockito.verify(indicesClient, Mockito.only()).delete(any(), any()); } public void testExceptionThrown() { @@ -102,7 +105,7 @@ public void testExceptionThrown() { assertEquals(indexMetadata.getIndex().getName(), request.indices()[0]); listener.onFailure(exception); return null; - }).when(indicesClient).delete(Mockito.any(), Mockito.any()); + }).when(indicesClient).delete(any(), any()); DeleteStep step = createRandomInstance(); ClusterState clusterState = ClusterState.builder(emptyClusterState()) @@ -117,7 +120,13 @@ public void testExceptionThrown() { ); } - public void testPerformActionThrowsExceptionIfIndexIsTheDataStreamWriteIndex() { + public void testPerformActionCallsFailureListenerIfIndexIsTheDataStreamWriteIndex() { + doThrow( + new IllegalStateException( + "the client must not be called in this test as we should fail in the step validation phase before we call the delete API" + ) + ).when(indicesClient).delete(any(DeleteIndexRequest.class), anyActionListener()); + String policyName = "test-ilm-policy"; String dataStreamName = randomAlphaOfLength(10); @@ -149,31 +158,27 @@ public void testPerformActionThrowsExceptionIfIndexIsTheDataStreamWriteIndex() { .metadata(Metadata.builder().put(index1, false).put(sourceIndexMetadata, false).put(dataStream).build()) .build(); - IllegalStateException illegalStateException = expectThrows( - IllegalStateException.class, - () -> createRandomInstance().performDuringNoSnapshot(sourceIndexMetadata, clusterState, new ActionListener<>() { - @Override - public void onResponse(Void complete) { - fail("unexpected listener callback"); - } - - @Override - public void onFailure(Exception e) { - fail("unexpected listener callback"); - } - }) - ); - assertThat( - illegalStateException.getMessage(), - is( - "index [" - + sourceIndexMetadata.getIndex().getName() - + "] is the write index for data stream [" - + dataStreamName - + "]. stopping execution of lifecycle [test-ilm-policy] as a data stream's write index cannot be deleted. " - + "manually rolling over the index will resume the execution of the policy as the index will not be the " - + "data stream's write index anymore" - ) - ); + createRandomInstance().performDuringNoSnapshot(sourceIndexMetadata, clusterState, new ActionListener<>() { + @Override + public void onResponse(Void complete) { + fail("unexpected listener callback"); + } + + @Override + public void onFailure(Exception e) { + assertThat( + e.getMessage(), + is( + "index [" + + sourceIndexMetadata.getIndex().getName() + + "] is the write index for data stream [" + + dataStreamName + + "]. stopping execution of lifecycle [test-ilm-policy] as a data stream's write index cannot be deleted. " + + "manually rolling over the index will resume the execution of the policy as the index will not be the " + + "data stream's write index anymore" + ) + ); + } + }); } } diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesDataStreamsIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesDataStreamsIT.java index cb4685a0564ed..95735ffbe8a87 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesDataStreamsIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/TimeSeriesDataStreamsIT.java @@ -11,12 +11,14 @@ import org.elasticsearch.client.Response; import org.elasticsearch.cluster.metadata.DataStream; import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.cluster.metadata.Template; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.index.engine.EngineConfig; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.ilm.CheckNotDataStreamWriteIndexStep; import org.elasticsearch.xpack.core.ilm.DeleteAction; +import org.elasticsearch.xpack.core.ilm.DeleteStep; import org.elasticsearch.xpack.core.ilm.ForceMergeAction; import org.elasticsearch.xpack.core.ilm.FreezeAction; import org.elasticsearch.xpack.core.ilm.PhaseCompleteStep; @@ -37,6 +39,7 @@ import static org.elasticsearch.xpack.TimeSeriesRestDriver.createNewSingletonPolicy; import static org.elasticsearch.xpack.TimeSeriesRestDriver.createSnapshotRepo; import static org.elasticsearch.xpack.TimeSeriesRestDriver.explainIndex; +import static org.elasticsearch.xpack.TimeSeriesRestDriver.getBackingIndices; import static org.elasticsearch.xpack.TimeSeriesRestDriver.getOnlyIndexSettings; import static org.elasticsearch.xpack.TimeSeriesRestDriver.getStepKeyForIndex; import static org.elasticsearch.xpack.TimeSeriesRestDriver.getTemplate; @@ -45,6 +48,7 @@ import static org.elasticsearch.xpack.TimeSeriesRestDriver.waitAndGetShrinkIndexName; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.is; public class TimeSeriesDataStreamsIT extends ESRestTestCase { @@ -303,4 +307,46 @@ public void testDeleteOnlyIndexInDataStreamDeletesDataStream() throws Exception }); } + @SuppressWarnings("unchecked") + public void testDataStreamWithMultipleIndicesAndWriteIndexInDeletePhase() throws Exception { + createComposableTemplate(client(), template, dataStream + "*", new Template(null, null, null, null)); + indexDocument(client(), dataStream, true); + + createNewSingletonPolicy(client(), policyName, "delete", DeleteAction.NO_SNAPSHOT_DELETE); + // let's update the index template so the new write index (after rollover) is managed by an ILM policy that sents it to the + // delete step - note that we'll have here a data stream with generation 000001 not managed and the write index 000002 in the + // delete phase (the write index in this case, being not the only backing index must NOT be deleted). + createComposableTemplate(client(), template, dataStream + "*", getTemplate(policyName)); + + client().performRequest(new Request("POST", dataStream + "/_rollover")); + indexDocument(client(), dataStream, true); + + String secondGenerationIndex = getBackingIndices(client(), dataStream).get(1); + assertBusy(() -> { + Request explainRequest = new Request("GET", "/_data_stream/" + dataStream); + Response response = client().performRequest(explainRequest); + Map responseMap; + try (InputStream is = response.getEntity().getContent()) { + responseMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), is, true); + } + + List dataStreams = (List) responseMap.get("data_streams"); + assertThat(dataStreams.size(), is(1)); + Map dataStream = (Map) dataStreams.get(0); + + List indices = (List) dataStream.get("indices"); + // no index should be deleted + assertThat(indices.size(), is(2)); + + Map explainIndex = explainIndex(client(), secondGenerationIndex); + assertThat(explainIndex.get("failed_step"), is(DeleteStep.NAME)); + assertThat((Integer) explainIndex.get("failed_step_retry_count"), is(greaterThan(1))); + }); + + // rolling the data stream again would see 000002 not be the write index anymore and should be deleted automatically + client().performRequest(new Request("POST", dataStream + "/_rollover")); + + assertBusy(() -> assertThat(indexExists(secondGenerationIndex), is(false))); + } + } From 9e5fbf6021aeb213c87bb1071db03eed920872b3 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 4 Mar 2024 10:56:45 +0000 Subject: [PATCH 217/250] Extract repository-resolution logic (#105760) We use the same logic to resolve repositories in multiple APIs, but today this is hidden in `TransportGetRepositoriesAction`. This commit moves it out to its own class and gives it its own test suite. --- .../get/TransportGetRepositoriesAction.java | 78 +--------------- .../get/TransportGetSnapshotsAction.java | 12 +-- .../repositories/ResolvedRepositories.java | 81 ++++++++++++++++ .../rest/action/cat/RestSnapshotAction.java | 4 +- .../ResolvedRepositoriesTests.java | 93 +++++++++++++++++++ .../AbstractSnapshotIntegTestCase.java | 4 +- 6 files changed, 187 insertions(+), 85 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/repositories/ResolvedRepositories.java create mode 100644 server/src/test/java/org/elasticsearch/repositories/ResolvedRepositoriesTests.java diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/get/TransportGetRepositoriesAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/get/TransportGetRepositoriesAction.java index b31dde0f75613..bed02ef2cbc19 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/get/TransportGetRepositoriesAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/repositories/get/TransportGetRepositoriesAction.java @@ -16,29 +16,20 @@ import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.RepositoriesMetadata; -import org.elasticsearch.cluster.metadata.RepositoryMetadata; import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.repositories.RepositoryMissingException; +import org.elasticsearch.repositories.ResolvedRepositories; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - /** * Transport action for get repositories operation */ public class TransportGetRepositoriesAction extends TransportMasterNodeReadAction { - public static final String ALL_PATTERN = "_all"; - @Inject public TransportGetRepositoriesAction( TransportService transportService, @@ -60,11 +51,6 @@ public TransportGetRepositoriesAction( ); } - public static boolean isMatchAll(String[] patterns) { - return (patterns.length == 0) - || (patterns.length == 1 && (ALL_PATTERN.equalsIgnoreCase(patterns[0]) || Regex.isMatchAllPattern(patterns[0]))); - } - @Override protected ClusterBlockException checkBlock(GetRepositoriesRequest request, ClusterState state) { return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ); @@ -77,69 +63,11 @@ protected void masterOperation( ClusterState state, final ActionListener listener ) { - RepositoriesResult result = getRepositories(state, request.repositories()); + final var result = ResolvedRepositories.resolve(state, request.repositories()); if (result.hasMissingRepositories()) { listener.onFailure(new RepositoryMissingException(String.join(", ", result.missing()))); } else { - listener.onResponse(new GetRepositoriesResponse(new RepositoriesMetadata(result.metadata))); - } - } - - /** - * Get repository metadata for given repository names from given cluster state. - * - * @param state Cluster state - * @param repoNames Repository names or patterns to get metadata for - * @return a result with the repository metadata that were found in the cluster state and the missing repositories - */ - public static RepositoriesResult getRepositories(ClusterState state, String[] repoNames) { - RepositoriesMetadata repositories = RepositoriesMetadata.get(state); - if (isMatchAll(repoNames)) { - return new RepositoriesResult(repositories.repositories()); - } - final List missingRepositories = new ArrayList<>(); - final List includePatterns = new ArrayList<>(); - final List excludePatterns = new ArrayList<>(); - boolean seenWildcard = false; - for (String repositoryOrPattern : repoNames) { - if (seenWildcard && repositoryOrPattern.length() > 1 && repositoryOrPattern.startsWith("-")) { - excludePatterns.add(repositoryOrPattern.substring(1)); - } else { - if (Regex.isSimpleMatchPattern(repositoryOrPattern)) { - seenWildcard = true; - } else { - if (repositories.repository(repositoryOrPattern) == null) { - missingRepositories.add(repositoryOrPattern); - } - } - includePatterns.add(repositoryOrPattern); - } - } - final String[] excludes = excludePatterns.toArray(Strings.EMPTY_ARRAY); - final Set repositoryListBuilder = new LinkedHashSet<>(); // to keep insertion order - for (String repositoryOrPattern : includePatterns) { - for (RepositoryMetadata repository : repositories.repositories()) { - if (repositoryListBuilder.contains(repository) == false - && Regex.simpleMatch(repositoryOrPattern, repository.name()) - && Regex.simpleMatch(excludes, repository.name()) == false) { - repositoryListBuilder.add(repository); - } - } - } - return new RepositoriesResult(List.copyOf(repositoryListBuilder), missingRepositories); - } - - /** - * A holder class that consists of the repository metadata and the names of the repositories that were not found in the cluster state. - */ - public record RepositoriesResult(List metadata, List missing) { - - RepositoriesResult(List repositoryMetadata) { - this(repositoryMetadata, List.of()); - } - - boolean hasMissingRepositories() { - return missing.isEmpty() == false; + listener.onResponse(new GetRepositoriesResponse(new RepositoriesMetadata(result.repositoryMetadata()))); } } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java index ef4ebec8c2dfc..18db17b1449e8 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java @@ -10,7 +10,6 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.admin.cluster.repositories.get.TransportGetRepositoriesAction; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.RefCountingListener; import org.elasticsearch.action.support.master.TransportMasterNodeAction; @@ -33,6 +32,7 @@ import org.elasticsearch.repositories.Repository; import org.elasticsearch.repositories.RepositoryData; import org.elasticsearch.repositories.RepositoryMissingException; +import org.elasticsearch.repositories.ResolvedRepositories; import org.elasticsearch.search.sort.SortOrder; import org.elasticsearch.snapshots.Snapshot; import org.elasticsearch.snapshots.SnapshotId; @@ -111,7 +111,7 @@ protected void masterOperation( new GetSnapshotsOperation( (CancellableTask) task, - TransportGetRepositoriesAction.getRepositories(state, request.repositories()), + ResolvedRepositories.resolve(state, request.repositories()), request.isSingleRepositoryRequest() == false, request.snapshots(), request.ignoreUnavailable(), @@ -172,7 +172,7 @@ private class GetSnapshotsOperation { GetSnapshotsOperation( CancellableTask cancellableTask, - TransportGetRepositoriesAction.RepositoriesResult repositoriesResult, + ResolvedRepositories resolvedRepositories, boolean isMultiRepoRequest, String[] snapshots, boolean ignoreUnavailable, @@ -188,7 +188,7 @@ private class GetSnapshotsOperation { boolean indices ) { this.cancellableTask = cancellableTask; - this.repositories = repositoriesResult.metadata(); + this.repositories = resolvedRepositories.repositoryMetadata(); this.isMultiRepoRequest = isMultiRepoRequest; this.snapshots = snapshots; this.ignoreUnavailable = ignoreUnavailable; @@ -203,7 +203,7 @@ private class GetSnapshotsOperation { this.verbose = verbose; this.indices = indices; - for (final var missingRepo : repositoriesResult.missing()) { + for (final var missingRepo : resolvedRepositories.missing()) { failuresByRepository.put(missingRepo, new RepositoryMissingException(missingRepo)); } } @@ -326,7 +326,7 @@ private void loadSnapshotInfos( } final Set toResolve = new HashSet<>(); - if (TransportGetRepositoriesAction.isMatchAll(snapshots)) { + if (ResolvedRepositories.isMatchAll(snapshots)) { toResolve.addAll(allSnapshotIds.values()); } else { final List includePatterns = new ArrayList<>(); diff --git a/server/src/main/java/org/elasticsearch/repositories/ResolvedRepositories.java b/server/src/main/java/org/elasticsearch/repositories/ResolvedRepositories.java new file mode 100644 index 0000000000000..ab4821ad942b0 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/repositories/ResolvedRepositories.java @@ -0,0 +1,81 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.repositories; + +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.RepositoriesMetadata; +import org.elasticsearch.cluster.metadata.RepositoryMetadata; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.regex.Regex; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * The result of calling {@link #resolve(ClusterState, String[])} to resolve a description of some snapshot repositories (from a path + * component of a request to the get-repositories or get-snapshots APIs) against the known repositories in the cluster state: the + * {@link RepositoryMetadata} for the extant repositories that match the description, together with a list of the parts of the description + * that failed to match any known repository. + * + * @param repositoryMetadata The {@link RepositoryMetadata} for the repositories that matched the description. + * @param missing The parts of the description which matched no repositories. + */ +public record ResolvedRepositories(List repositoryMetadata, List missing) { + + public static final String ALL_PATTERN = "_all"; + + public static boolean isMatchAll(String[] patterns) { + return patterns.length == 0 + || (patterns.length == 1 && (ALL_PATTERN.equalsIgnoreCase(patterns[0]) || Regex.isMatchAllPattern(patterns[0]))); + } + + public static ResolvedRepositories resolve(ClusterState state, String[] patterns) { + final var repositories = RepositoriesMetadata.get(state); + if (isMatchAll(patterns)) { + return new ResolvedRepositories(repositories.repositories(), List.of()); + } + + final List missingRepositories = new ArrayList<>(); + final List includePatterns = new ArrayList<>(); + final List excludePatterns = new ArrayList<>(); + boolean seenWildcard = false; + for (final var pattern : patterns) { + if (seenWildcard && pattern.length() > 1 && pattern.startsWith("-")) { + excludePatterns.add(pattern.substring(1)); + } else { + if (Regex.isSimpleMatchPattern(pattern)) { + seenWildcard = true; + } else { + if (repositories.repository(pattern) == null) { + missingRepositories.add(pattern); + } + } + includePatterns.add(pattern); + } + } + final var excludes = excludePatterns.toArray(Strings.EMPTY_ARRAY); + final Set repositoryListBuilder = new LinkedHashSet<>(); // to keep insertion order + for (String repositoryOrPattern : includePatterns) { + for (RepositoryMetadata repository : repositories.repositories()) { + if (repositoryListBuilder.contains(repository) == false + && Regex.simpleMatch(repositoryOrPattern, repository.name()) + && Regex.simpleMatch(excludes, repository.name()) == false) { + repositoryListBuilder.add(repository); + } + } + } + return new ResolvedRepositories(List.copyOf(repositoryListBuilder), missingRepositories); + } + + public boolean hasMissingRepositories() { + return missing.isEmpty() == false; + } +} diff --git a/server/src/main/java/org/elasticsearch/rest/action/cat/RestSnapshotAction.java b/server/src/main/java/org/elasticsearch/rest/action/cat/RestSnapshotAction.java index 3f13205aad6b4..9b4c6534a452f 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/cat/RestSnapshotAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/cat/RestSnapshotAction.java @@ -9,7 +9,6 @@ package org.elasticsearch.rest.action.cat; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.action.admin.cluster.repositories.get.TransportGetRepositoriesAction; import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse; import org.elasticsearch.client.internal.node.NodeClient; @@ -17,6 +16,7 @@ import org.elasticsearch.common.Table; import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.core.TimeValue; +import org.elasticsearch.repositories.ResolvedRepositories; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.Scope; @@ -50,7 +50,7 @@ public String getName() { @Override protected RestChannelConsumer doCatRequest(final RestRequest request, NodeClient client) { - final String[] matchAll = { TransportGetRepositoriesAction.ALL_PATTERN }; + final String[] matchAll = { ResolvedRepositories.ALL_PATTERN }; GetSnapshotsRequest getSnapshotsRequest = new GetSnapshotsRequest().repositories(request.paramAsStringArray("repository", matchAll)) .snapshots(matchAll); diff --git a/server/src/test/java/org/elasticsearch/repositories/ResolvedRepositoriesTests.java b/server/src/test/java/org/elasticsearch/repositories/ResolvedRepositoriesTests.java new file mode 100644 index 0000000000000..04859d2847522 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/repositories/ResolvedRepositoriesTests.java @@ -0,0 +1,93 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.repositories; + +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.RepositoriesMetadata; +import org.elasticsearch.cluster.metadata.RepositoryMetadata; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; +import org.hamcrest.Matchers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class ResolvedRepositoriesTests extends ESTestCase { + + public void testAll() { + runMatchAllTest(); + runMatchAllTest("*"); + runMatchAllTest("_all"); + } + + private static void runMatchAllTest(String... patterns) { + final var state = clusterStateWithRepositories(randomList(1, 4, ESTestCase::randomIdentifier).toArray(String[]::new)); + final var result = getRepositories(state, patterns); + assertEquals(RepositoriesMetadata.get(state).repositories(), result.repositoryMetadata()); + assertThat(result.missing(), Matchers.empty()); + assertFalse(result.hasMissingRepositories()); + } + + public void testMatchingName() { + final var state = clusterStateWithRepositories(randomList(1, 4, ESTestCase::randomIdentifier).toArray(String[]::new)); + final var name = randomFrom(RepositoriesMetadata.get(state).repositories()).name(); + final var result = getRepositories(state, name); + assertEquals(List.of(RepositoriesMetadata.get(state).repository(name)), result.repositoryMetadata()); + assertThat(result.missing(), Matchers.empty()); + assertFalse(result.hasMissingRepositories()); + } + + public void testMismatchingName() { + final var state = clusterStateWithRepositories(randomList(1, 4, ESTestCase::randomIdentifier).toArray(String[]::new)); + final var notAName = randomValueOtherThanMany( + n -> RepositoriesMetadata.get(state).repositories().stream().anyMatch(m -> n.equals(m.name())), + ESTestCase::randomIdentifier + ); + final var result = getRepositories(state, notAName); + assertEquals(List.of(), result.repositoryMetadata()); + assertEquals(List.of(notAName), result.missing()); + assertTrue(result.hasMissingRepositories()); + } + + public void testWildcards() { + final var state = clusterStateWithRepositories("test-match-1", "test-match-2", "test-exclude", "other-repo"); + + runWildcardTest(state, List.of("test-match-1", "test-match-2", "test-exclude"), "test-*"); + runWildcardTest(state, List.of("test-match-1", "test-match-2"), "test-*1", "test-*2"); + runWildcardTest(state, List.of("test-match-2", "test-match-1"), "test-*2", "test-*1"); + runWildcardTest(state, List.of("test-match-1", "test-match-2"), "test-*", "-*-exclude"); + runWildcardTest(state, List.of(), "no-*-repositories"); + runWildcardTest(state, List.of("test-match-1", "test-match-2", "other-repo"), "test-*", "-*-exclude", "other-repo"); + runWildcardTest(state, List.of("other-repo", "test-match-1", "test-match-2"), "other-repo", "test-*", "-*-exclude"); + } + + private static void runWildcardTest(ClusterState clusterState, List expectedNames, String... patterns) { + final var result = getRepositories(clusterState, patterns); + final var description = Strings.format("%s should yield %s", Arrays.toString(patterns), expectedNames); + assertFalse(description, result.hasMissingRepositories()); + assertEquals(description, expectedNames, result.repositoryMetadata().stream().map(RepositoryMetadata::name).toList()); + } + + private static ResolvedRepositories getRepositories(ClusterState clusterState, String... patterns) { + return ResolvedRepositories.resolve(clusterState, patterns); + } + + private static ClusterState clusterStateWithRepositories(String... repoNames) { + final var repositories = new ArrayList(repoNames.length); + for (final var repoName : repoNames) { + repositories.add(new RepositoryMetadata(repoName, "test", Settings.EMPTY)); + } + return ClusterState.EMPTY_STATE.copyAndUpdateMetadata( + b -> b.putCustom(RepositoriesMetadata.TYPE, new RepositoriesMetadata(repositories)) + ); + } + +} diff --git a/test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java index 3744011b5b9f6..46b18887241dd 100644 --- a/test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/snapshots/AbstractSnapshotIntegTestCase.java @@ -12,7 +12,6 @@ import org.elasticsearch.action.ActionFuture; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRunnable; -import org.elasticsearch.action.admin.cluster.repositories.get.TransportGetRepositoriesAction; import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest; import org.elasticsearch.action.index.IndexRequestBuilder; @@ -42,6 +41,7 @@ import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.repositories.Repository; import org.elasticsearch.repositories.RepositoryData; +import org.elasticsearch.repositories.ResolvedRepositories; import org.elasticsearch.repositories.ShardGenerations; import org.elasticsearch.repositories.blobstore.BlobStoreRepository; import org.elasticsearch.repositories.blobstore.BlobStoreTestUtil; @@ -799,7 +799,7 @@ public static Map randomUserMetadata() { } public static String[] matchAllPattern() { - return randomBoolean() ? new String[] { "*" } : new String[] { TransportGetRepositoriesAction.ALL_PATTERN }; + return randomBoolean() ? new String[] { "*" } : new String[] { ResolvedRepositories.ALL_PATTERN }; } public RepositoryMetadata getRepositoryMetadata(String repo) { From 3addbed878cff2a108daa6f07d14b9ee56df7dcb Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 4 Mar 2024 10:57:37 +0000 Subject: [PATCH 218/250] Restrict scope of `GetSnapshotInfoContext` (#105721) `GetSnapshotInfoContext` is kind of a strange thing to expose in the `Repository` interface. It's a concrete class with fairly specific semantics which are tied to the `BlobStoreRepository` implementation, and yet callers must always construct one before calling `getSnapshotInfo`. In practice all callers call it like this: repository.getSnapshotInfo(new GetSnapshotInfoContext(...)); This commit simplifies these calls to just pass the arguments directly, and moves `GetSnapshotInfoContext` into the `o.e.r.blobstore` package where it becomes package-private. --- .../get/TransportGetSnapshotsAction.java | 19 ++--- .../TransportSnapshotsStatusAction.java | 73 +++++++++---------- .../repositories/FilterRepository.java | 13 +++- .../repositories/InvalidRepository.java | 13 +++- .../repositories/Repository.java | 25 +++++-- .../repositories/UnknownTypeRepository.java | 13 +++- .../blobstore/BlobStoreRepository.java | 27 +++++-- .../GetSnapshotInfoContext.java | 17 +++-- .../RepositoriesServiceTests.java | 11 ++- .../index/shard/RestoreOnlyRepository.java | 14 +++- .../blobstore/BlobStoreTestUtil.java | 47 +++++------- .../xpack/ccr/repository/CcrRepository.java | 33 ++++++--- ...TransportSLMGetExpiredSnapshotsAction.java | 19 ++--- ...portSLMGetExpiredSnapshotsActionTests.java | 29 +++++--- 14 files changed, 208 insertions(+), 145 deletions(-) rename server/src/main/java/org/elasticsearch/repositories/{ => blobstore}/GetSnapshotInfoContext.java (92%) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java index 18db17b1449e8..9b74fb77c44b3 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java @@ -26,7 +26,6 @@ import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.common.util.concurrent.ListenableFuture; import org.elasticsearch.core.Nullable; -import org.elasticsearch.repositories.GetSnapshotInfoContext; import org.elasticsearch.repositories.IndexId; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.repositories.Repository; @@ -438,19 +437,11 @@ private void snapshots(String repositoryName, Collection snapshotIds // only need to synchronize accesses related to reading SnapshotInfo from the repo final List syncSnapshots = Collections.synchronizedList(snapshots); - repository.getSnapshotInfo( - new GetSnapshotInfoContext( - snapshotIdsToIterate, - ignoreUnavailable == false, - cancellableTask::isCancelled, - (context, snapshotInfo) -> { - if (predicates.test(snapshotInfo)) { - syncSnapshots.add(snapshotInfo.maybeWithoutIndices(indices)); - } - }, - listeners.acquire() - ) - ); + repository.getSnapshotInfo(snapshotIdsToIterate, ignoreUnavailable == false, cancellableTask::isCancelled, snapshotInfo -> { + if (predicates.test(snapshotInfo)) { + syncSnapshots.add(snapshotInfo.maybeWithoutIndices(indices)); + } + }, listeners.acquire()); } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/TransportSnapshotsStatusAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/TransportSnapshotsStatusAction.java index 4be6c6af3d7db..973ae9098047f 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/TransportSnapshotsStatusAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/TransportSnapshotsStatusAction.java @@ -29,7 +29,6 @@ import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; -import org.elasticsearch.repositories.GetSnapshotInfoContext; import org.elasticsearch.repositories.IndexId; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.repositories.Repository; @@ -318,45 +317,39 @@ private void loadRepositoryData( delegate.onResponse(new SnapshotsStatusResponse(Collections.unmodifiableList(builder))); } else { final List threadSafeBuilder = Collections.synchronizedList(builder); - repositoriesService.repository(repositoryName) - .getSnapshotInfo(new GetSnapshotInfoContext(snapshotIdsToLoad, true, task::isCancelled, (context, snapshotInfo) -> { - List shardStatusBuilder = new ArrayList<>(); - final Map shardStatuses; - try { - shardStatuses = snapshotShards(repositoryName, repositoryData, task, snapshotInfo); - } catch (Exception e) { - // stops all further fetches of snapshotInfo since context is fail-fast - context.onFailure(e); - return; - } - for (final var shardStatus : shardStatuses.entrySet()) { - IndexShardSnapshotStatus.Copy lastSnapshotStatus = shardStatus.getValue(); - shardStatusBuilder.add(new SnapshotIndexShardStatus(shardStatus.getKey(), lastSnapshotStatus)); - } - final SnapshotsInProgress.State state = switch (snapshotInfo.state()) { - case FAILED -> SnapshotsInProgress.State.FAILED; - case SUCCESS, PARTIAL -> - // Translating both PARTIAL and SUCCESS to SUCCESS for now - // TODO: add the differentiation on the metadata level in the next major release - SnapshotsInProgress.State.SUCCESS; - default -> throw new IllegalArgumentException("Unexpected snapshot state " + snapshotInfo.state()); - }; - final long startTime = snapshotInfo.startTime(); - final long endTime = snapshotInfo.endTime(); - assert endTime >= startTime || (endTime == 0L && snapshotInfo.state().completed() == false) - : "Inconsistent timestamps found in SnapshotInfo [" + snapshotInfo + "]"; - threadSafeBuilder.add( - new SnapshotStatus( - new Snapshot(repositoryName, snapshotInfo.snapshotId()), - state, - Collections.unmodifiableList(shardStatusBuilder), - snapshotInfo.includeGlobalState(), - startTime, - // Use current time to calculate overall runtime for in-progress snapshots that have endTime == 0 - (endTime == 0 ? threadPool.absoluteTimeInMillis() : endTime) - startTime - ) - ); - }, delegate.map(v -> new SnapshotsStatusResponse(List.copyOf(threadSafeBuilder))))); + repositoriesService.repository(repositoryName).getSnapshotInfo(snapshotIdsToLoad, true, task::isCancelled, snapshotInfo -> { + List shardStatusBuilder = new ArrayList<>(); + final Map shardStatuses; + shardStatuses = snapshotShards(repositoryName, repositoryData, task, snapshotInfo); + // an exception here stops further fetches of snapshotInfo since context is fail-fast + for (final var shardStatus : shardStatuses.entrySet()) { + IndexShardSnapshotStatus.Copy lastSnapshotStatus = shardStatus.getValue(); + shardStatusBuilder.add(new SnapshotIndexShardStatus(shardStatus.getKey(), lastSnapshotStatus)); + } + final SnapshotsInProgress.State state = switch (snapshotInfo.state()) { + case FAILED -> SnapshotsInProgress.State.FAILED; + case SUCCESS, PARTIAL -> + // Translating both PARTIAL and SUCCESS to SUCCESS for now + // TODO: add the differentiation on the metadata level in the next major release + SnapshotsInProgress.State.SUCCESS; + default -> throw new IllegalArgumentException("Unexpected snapshot state " + snapshotInfo.state()); + }; + final long startTime = snapshotInfo.startTime(); + final long endTime = snapshotInfo.endTime(); + assert endTime >= startTime || (endTime == 0L && snapshotInfo.state().completed() == false) + : "Inconsistent timestamps found in SnapshotInfo [" + snapshotInfo + "]"; + threadSafeBuilder.add( + new SnapshotStatus( + new Snapshot(repositoryName, snapshotInfo.snapshotId()), + state, + Collections.unmodifiableList(shardStatusBuilder), + snapshotInfo.includeGlobalState(), + startTime, + // Use current time to calculate overall runtime for in-progress snapshots that have endTime == 0 + (endTime == 0 ? threadPool.absoluteTimeInMillis() : endTime) - startTime + ) + ); + }, delegate.map(v -> new SnapshotsStatusResponse(List.copyOf(threadSafeBuilder)))); } })); } diff --git a/server/src/main/java/org/elasticsearch/repositories/FilterRepository.java b/server/src/main/java/org/elasticsearch/repositories/FilterRepository.java index c88bbcfa91b98..37f1850c1fb2d 100644 --- a/server/src/main/java/org/elasticsearch/repositories/FilterRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/FilterRepository.java @@ -16,6 +16,7 @@ import org.elasticsearch.common.component.Lifecycle; import org.elasticsearch.common.component.LifecycleListener; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; @@ -23,11 +24,13 @@ import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.snapshots.SnapshotDeleteListener; import org.elasticsearch.snapshots.SnapshotId; +import org.elasticsearch.snapshots.SnapshotInfo; import java.io.IOException; import java.util.Collection; import java.util.Set; import java.util.concurrent.Executor; +import java.util.function.BooleanSupplier; public class FilterRepository implements Repository { @@ -47,8 +50,14 @@ public RepositoryMetadata getMetadata() { } @Override - public void getSnapshotInfo(GetSnapshotInfoContext context) { - in.getSnapshotInfo(context); + public void getSnapshotInfo( + Collection snapshotIds, + boolean abortOnFailure, + BooleanSupplier isCancelled, + CheckedConsumer consumer, + ActionListener listener + ) { + in.getSnapshotInfo(snapshotIds, abortOnFailure, isCancelled, consumer, listener); } @Override diff --git a/server/src/main/java/org/elasticsearch/repositories/InvalidRepository.java b/server/src/main/java/org/elasticsearch/repositories/InvalidRepository.java index 6bd967d84c89b..948ae747e11a9 100644 --- a/server/src/main/java/org/elasticsearch/repositories/InvalidRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/InvalidRepository.java @@ -15,6 +15,7 @@ import org.elasticsearch.cluster.metadata.RepositoryMetadata; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.component.AbstractLifecycleComponent; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; @@ -22,10 +23,12 @@ import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.snapshots.SnapshotDeleteListener; import org.elasticsearch.snapshots.SnapshotId; +import org.elasticsearch.snapshots.SnapshotInfo; import java.io.IOException; import java.util.Collection; import java.util.concurrent.Executor; +import java.util.function.BooleanSupplier; /** * Represents a repository that exists in the cluster state but could not be instantiated on a node, typically due to invalid configuration. @@ -54,8 +57,14 @@ public RepositoryMetadata getMetadata() { } @Override - public void getSnapshotInfo(GetSnapshotInfoContext context) { - throw createCreationException(); + public void getSnapshotInfo( + Collection snapshotIds, + boolean abortOnFailure, + BooleanSupplier isCancelled, + CheckedConsumer consumer, + ActionListener listener + ) { + listener.onFailure(createCreationException()); } @Override diff --git a/server/src/main/java/org/elasticsearch/repositories/Repository.java b/server/src/main/java/org/elasticsearch/repositories/Repository.java index 5782dedf3cfbc..a90b0a217285c 100644 --- a/server/src/main/java/org/elasticsearch/repositories/Repository.java +++ b/server/src/main/java/org/elasticsearch/repositories/Repository.java @@ -15,6 +15,7 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.component.LifecycleComponent; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.core.Nullable; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.shard.ShardId; @@ -31,6 +32,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.Executor; +import java.util.function.BooleanSupplier; import java.util.function.Function; /** @@ -70,11 +72,24 @@ default Repository create(RepositoryMetadata metadata, Function snapshotIds, + boolean abortOnFailure, + BooleanSupplier isCancelled, + CheckedConsumer consumer, + ActionListener listener + ); /** * Reads a single snapshot description from the repository @@ -83,7 +98,7 @@ default Repository create(RepositoryMetadata metadata, Function listener) { - getSnapshotInfo(new GetSnapshotInfoContext(List.of(snapshotId), true, () -> false, (context, snapshotInfo) -> { + getSnapshotInfo(List.of(snapshotId), true, () -> false, snapshotInfo -> { assert Repository.assertSnapshotMetaThread(); listener.onResponse(snapshotInfo); }, new ActionListener<>() { @@ -96,7 +111,7 @@ public void onResponse(Void o) { public void onFailure(Exception e) { listener.onFailure(e); } - })); + }); } /** diff --git a/server/src/main/java/org/elasticsearch/repositories/UnknownTypeRepository.java b/server/src/main/java/org/elasticsearch/repositories/UnknownTypeRepository.java index 30f167d8c5cf6..7821c865e166c 100644 --- a/server/src/main/java/org/elasticsearch/repositories/UnknownTypeRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/UnknownTypeRepository.java @@ -15,6 +15,7 @@ import org.elasticsearch.cluster.metadata.RepositoryMetadata; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.component.AbstractLifecycleComponent; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; @@ -22,10 +23,12 @@ import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.snapshots.SnapshotDeleteListener; import org.elasticsearch.snapshots.SnapshotId; +import org.elasticsearch.snapshots.SnapshotInfo; import java.io.IOException; import java.util.Collection; import java.util.concurrent.Executor; +import java.util.function.BooleanSupplier; /** * This class represents a repository that could not be initialized due to unknown type. @@ -52,8 +55,14 @@ public RepositoryMetadata getMetadata() { } @Override - public void getSnapshotInfo(GetSnapshotInfoContext context) { - throw createUnknownTypeException(); + public void getSnapshotInfo( + Collection snapshotIds, + boolean abortOnFailure, + BooleanSupplier isCancelled, + CheckedConsumer consumer, + ActionListener listener + ) { + listener.onFailure(createUnknownTypeException()); } @Override diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index b8b0498d95125..52cfa2fd5275f 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -97,7 +97,6 @@ import org.elasticsearch.indices.recovery.RecoverySettings; import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.repositories.FinalizeSnapshotContext; -import org.elasticsearch.repositories.GetSnapshotInfoContext; import org.elasticsearch.repositories.IndexId; import org.elasticsearch.repositories.IndexMetaDataGenerations; import org.elasticsearch.repositories.RepositoriesService; @@ -151,6 +150,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -1778,7 +1778,20 @@ public void onAfter() { } @Override - public void getSnapshotInfo(GetSnapshotInfoContext context) { + public void getSnapshotInfo( + Collection snapshotIds, + boolean abortOnFailure, + BooleanSupplier isCancelled, + CheckedConsumer consumer, + ActionListener listener + ) { + final var context = new GetSnapshotInfoContext(snapshotIds, abortOnFailure, isCancelled, (ctx, sni) -> { + try { + consumer.accept(sni); + } catch (Exception e) { + ctx.onFailure(e); + } + }, listener); // put snapshot info downloads into a task queue instead of pushing them all into the queue to not completely monopolize the // snapshot meta pool for a single request final int workers = Math.min(threadPool.info(ThreadPool.Names.SNAPSHOT_META).getMax(), context.snapshotIds().size()); @@ -2617,9 +2630,11 @@ public String toString() { if (snapshotIdsWithMissingDetails.isEmpty() == false) { final Map extraDetailsMap = new ConcurrentHashMap<>(); getSnapshotInfo( - new GetSnapshotInfoContext(snapshotIdsWithMissingDetails, false, () -> false, (context, snapshotInfo) -> { - extraDetailsMap.put(snapshotInfo.snapshotId(), SnapshotDetails.fromSnapshotInfo(snapshotInfo)); - }, ActionListener.runAfter(new ActionListener<>() { + snapshotIdsWithMissingDetails, + false, + () -> false, + snapshotInfo -> extraDetailsMap.put(snapshotInfo.snapshotId(), SnapshotDetails.fromSnapshotInfo(snapshotInfo)), + ActionListener.runAfter(new ActionListener<>() { @Override public void onResponse(Void aVoid) { logger.info( @@ -2636,7 +2651,7 @@ public void onResponse(Void aVoid) { public void onFailure(Exception e) { logger.warn("Failure when trying to load missing details from snapshot metadata", e); } - }, () -> filterRepositoryDataStep.onResponse(repositoryData.withExtraDetails(extraDetailsMap)))) + }, () -> filterRepositoryDataStep.onResponse(repositoryData.withExtraDetails(extraDetailsMap))) ); } else { filterRepositoryDataStep.onResponse(repositoryData); diff --git a/server/src/main/java/org/elasticsearch/repositories/GetSnapshotInfoContext.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/GetSnapshotInfoContext.java similarity index 92% rename from server/src/main/java/org/elasticsearch/repositories/GetSnapshotInfoContext.java rename to server/src/main/java/org/elasticsearch/repositories/blobstore/GetSnapshotInfoContext.java index ec8777e71ba9b..96782bca31a15 100644 --- a/server/src/main/java/org/elasticsearch/repositories/GetSnapshotInfoContext.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/GetSnapshotInfoContext.java @@ -5,12 +5,13 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -package org.elasticsearch.repositories; +package org.elasticsearch.repositories.blobstore; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.util.concurrent.CountDown; +import org.elasticsearch.repositories.Repository; import org.elasticsearch.snapshots.SnapshotId; import org.elasticsearch.snapshots.SnapshotInfo; import org.elasticsearch.threadpool.ThreadPool; @@ -21,9 +22,9 @@ import java.util.function.BooleanSupplier; /** - * Describes the context of fetching one or more {@link SnapshotInfo} via {@link Repository#getSnapshotInfo(GetSnapshotInfoContext)}. + * Describes the context of fetching one or more {@link SnapshotInfo} via {@link Repository#getSnapshotInfo}. */ -public final class GetSnapshotInfoContext implements ActionListener { +final class GetSnapshotInfoContext implements ActionListener { private static final Logger logger = LogManager.getLogger(GetSnapshotInfoContext.class); @@ -59,7 +60,7 @@ public final class GetSnapshotInfoContext implements ActionListener snapshotIds, boolean abortOnFailure, BooleanSupplier isCancelled, @@ -77,28 +78,28 @@ public GetSnapshotInfoContext( this.doneListener = listener; } - public List snapshotIds() { + List snapshotIds() { return snapshotIds; } /** * @return true if fetching {@link SnapshotInfo} should be stopped after encountering any exception */ - public boolean abortOnFailure() { + boolean abortOnFailure() { return abortOnFailure; } /** * @return true if fetching {@link SnapshotInfo} has been cancelled */ - public boolean isCancelled() { + boolean isCancelled() { return isCancelled.getAsBoolean(); } /** * @return true if fetching {@link SnapshotInfo} is either complete or should be stopped because of an error */ - public boolean done() { + boolean done() { return counter.isCountedDown(); } diff --git a/server/src/test/java/org/elasticsearch/repositories/RepositoriesServiceTests.java b/server/src/test/java/org/elasticsearch/repositories/RepositoriesServiceTests.java index 45e4bb09c1616..5a736b4e1e9dd 100644 --- a/server/src/test/java/org/elasticsearch/repositories/RepositoriesServiceTests.java +++ b/server/src/test/java/org/elasticsearch/repositories/RepositoriesServiceTests.java @@ -30,6 +30,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.MockBigArrays; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; @@ -39,6 +40,7 @@ import org.elasticsearch.repositories.blobstore.MeteredBlobStoreRepository; import org.elasticsearch.snapshots.SnapshotDeleteListener; import org.elasticsearch.snapshots.SnapshotId; +import org.elasticsearch.snapshots.SnapshotInfo; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.Transport; @@ -51,6 +53,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Executor; +import java.util.function.BooleanSupplier; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.isA; @@ -332,7 +335,13 @@ public RepositoryMetadata getMetadata() { } @Override - public void getSnapshotInfo(GetSnapshotInfoContext context) { + public void getSnapshotInfo( + Collection snapshotIds, + boolean abortOnFailure, + BooleanSupplier isCancelled, + CheckedConsumer consumer, + ActionListener listener + ) { throw new UnsupportedOperationException(); } diff --git a/test/framework/src/main/java/org/elasticsearch/index/shard/RestoreOnlyRepository.java b/test/framework/src/main/java/org/elasticsearch/index/shard/RestoreOnlyRepository.java index 181b6c82379ed..26e887338158d 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/shard/RestoreOnlyRepository.java +++ b/test/framework/src/main/java/org/elasticsearch/index/shard/RestoreOnlyRepository.java @@ -14,10 +14,10 @@ import org.elasticsearch.cluster.metadata.RepositoryMetadata; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.component.AbstractLifecycleComponent; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.snapshots.IndexShardSnapshotStatus; import org.elasticsearch.repositories.FinalizeSnapshotContext; -import org.elasticsearch.repositories.GetSnapshotInfoContext; import org.elasticsearch.repositories.IndexId; import org.elasticsearch.repositories.IndexMetaDataGenerations; import org.elasticsearch.repositories.Repository; @@ -29,10 +29,12 @@ import org.elasticsearch.repositories.SnapshotShardContext; import org.elasticsearch.snapshots.SnapshotDeleteListener; import org.elasticsearch.snapshots.SnapshotId; +import org.elasticsearch.snapshots.SnapshotInfo; import java.util.Collection; import java.util.Collections; import java.util.concurrent.Executor; +import java.util.function.BooleanSupplier; import static java.util.Collections.emptyList; import static org.elasticsearch.repositories.RepositoryData.EMPTY_REPO_GEN; @@ -61,8 +63,14 @@ public RepositoryMetadata getMetadata() { } @Override - public void getSnapshotInfo(GetSnapshotInfoContext context) { - throw new UnsupportedOperationException(); + public void getSnapshotInfo( + Collection snapshotIds, + boolean abortOnFailure, + BooleanSupplier isCancelled, + CheckedConsumer consumer, + ActionListener listener + ) { + listener.onFailure(new UnsupportedOperationException()); } @Override diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java index 79e4a8da713c5..d31bd16b07fcc 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreTestUtil.java @@ -35,7 +35,6 @@ import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.snapshots.blobstore.BlobStoreIndexShardSnapshots; -import org.elasticsearch.repositories.GetSnapshotInfoContext; import org.elasticsearch.repositories.IndexId; import org.elasticsearch.repositories.RepositoryData; import org.elasticsearch.repositories.ShardGeneration; @@ -254,34 +253,26 @@ private static void assertSnapshotUUIDs( } // Assert that for each snapshot, the relevant metadata was written to index and shard folders final List snapshotInfos = Collections.synchronizedList(new ArrayList<>()); - repository.getSnapshotInfo( - new GetSnapshotInfoContext( - List.copyOf(snapshotIds), - true, - () -> false, - (ctx, sni) -> snapshotInfos.add(sni), - new ActionListener<>() { - @Override - public void onResponse(Void unused) { - try { - assertSnapshotInfosConsistency(repository, repositoryData, indices, snapshotInfos); - } catch (Exception e) { - listener.onResponse(new AssertionError(e)); - return; - } catch (AssertionError e) { - listener.onResponse(e); - return; - } - listener.onResponse(null); - } - - @Override - public void onFailure(Exception e) { - listener.onResponse(new AssertionError(e)); - } + repository.getSnapshotInfo(List.copyOf(snapshotIds), true, () -> false, snapshotInfos::add, new ActionListener<>() { + @Override + public void onResponse(Void unused) { + try { + assertSnapshotInfosConsistency(repository, repositoryData, indices, snapshotInfos); + } catch (Exception e) { + listener.onResponse(new AssertionError(e)); + return; + } catch (AssertionError e) { + listener.onResponse(e); + return; } - ) - ); + listener.onResponse(null); + } + + @Override + public void onFailure(Exception e) { + listener.onResponse(new AssertionError(e)); + } + }); } private static void assertSnapshotInfosConsistency( diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/repository/CcrRepository.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/repository/CcrRepository.java index c13e513ef5164..2702a2e28546c 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/repository/CcrRepository.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/repository/CcrRepository.java @@ -43,6 +43,7 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.core.IOUtils; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.TimeValue; @@ -64,7 +65,6 @@ import org.elasticsearch.indices.recovery.MultiFileWriter; import org.elasticsearch.indices.recovery.RecoveryState; import org.elasticsearch.repositories.FinalizeSnapshotContext; -import org.elasticsearch.repositories.GetSnapshotInfoContext; import org.elasticsearch.repositories.IndexId; import org.elasticsearch.repositories.IndexMetaDataGenerations; import org.elasticsearch.repositories.Repository; @@ -106,6 +106,7 @@ import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; import java.util.function.LongConsumer; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -183,28 +184,36 @@ private RemoteClusterClient getRemoteClusterClient() { } @Override - public void getSnapshotInfo(GetSnapshotInfoContext context) { - final List snapshotIds = context.snapshotIds(); + public void getSnapshotInfo( + Collection snapshotIds, + boolean abortOnFailure, + BooleanSupplier isCancelled, + CheckedConsumer consumer, + ActionListener listener + ) { assert snapshotIds.size() == 1 && SNAPSHOT_ID.equals(snapshotIds.iterator().next()) : "RemoteClusterRepository only supports " + SNAPSHOT_ID + " as the SnapshotId but saw " + snapshotIds; try { csDeduplicator.execute( - new ThreadedActionListener<>(threadPool.executor(ThreadPool.Names.SNAPSHOT_META), context.map(response -> { + new ThreadedActionListener<>(threadPool.executor(ThreadPool.Names.SNAPSHOT_META), listener.map(response -> { Metadata responseMetadata = response.metadata(); Map indicesMap = responseMetadata.indices(); - return new SnapshotInfo( - new Snapshot(this.metadata.name(), SNAPSHOT_ID), - List.copyOf(indicesMap.keySet()), - List.copyOf(responseMetadata.dataStreams().keySet()), - List.of(), - response.getNodes().getMaxDataNodeCompatibleIndexVersion(), - SnapshotState.SUCCESS + consumer.accept( + new SnapshotInfo( + new Snapshot(this.metadata.name(), SNAPSHOT_ID), + List.copyOf(indicesMap.keySet()), + List.copyOf(responseMetadata.dataStreams().keySet()), + List.of(), + response.getNodes().getMaxDataNodeCompatibleIndexVersion(), + SnapshotState.SUCCESS + ) ); + return null; })) ); } catch (Exception e) { assert false : e; - context.onFailure(e); + listener.onFailure(e); } } diff --git a/x-pack/plugin/slm/src/main/java/org/elasticsearch/xpack/slm/TransportSLMGetExpiredSnapshotsAction.java b/x-pack/plugin/slm/src/main/java/org/elasticsearch/xpack/slm/TransportSLMGetExpiredSnapshotsAction.java index b1ec8f3a28f1b..cf3a114fc5803 100644 --- a/x-pack/plugin/slm/src/main/java/org/elasticsearch/xpack/slm/TransportSLMGetExpiredSnapshotsAction.java +++ b/x-pack/plugin/slm/src/main/java/org/elasticsearch/xpack/slm/TransportSLMGetExpiredSnapshotsAction.java @@ -25,7 +25,6 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.core.Tuple; -import org.elasticsearch.repositories.GetSnapshotInfoContext; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.repositories.Repository; import org.elasticsearch.repositories.RepositoryData; @@ -194,16 +193,14 @@ static void getSnapshotDetailsByPolicy( snapshotsWithMissingDetails ); repository.getSnapshotInfo( - new GetSnapshotInfoContext( - snapshotsWithMissingDetails, - false, - () -> false, - (ignored, snapshotInfo) -> snapshotDetailsByPolicy.add( - snapshotInfo.snapshotId(), - RepositoryData.SnapshotDetails.fromSnapshotInfo(snapshotInfo) - ), - new ThreadedActionListener<>(executor, listener.map(ignored -> snapshotDetailsByPolicy)) - ) + snapshotsWithMissingDetails, + false, + () -> false, + snapshotInfo -> snapshotDetailsByPolicy.add( + snapshotInfo.snapshotId(), + RepositoryData.SnapshotDetails.fromSnapshotInfo(snapshotInfo) + ), + new ThreadedActionListener<>(executor, listener.map(ignored -> snapshotDetailsByPolicy)) ); } } diff --git a/x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/TransportSLMGetExpiredSnapshotsActionTests.java b/x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/TransportSLMGetExpiredSnapshotsActionTests.java index 1a49ad114f33f..573edc6e517bf 100644 --- a/x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/TransportSLMGetExpiredSnapshotsActionTests.java +++ b/x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/TransportSLMGetExpiredSnapshotsActionTests.java @@ -16,10 +16,10 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.DeterministicTaskQueue; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.IndexVersion; -import org.elasticsearch.repositories.GetSnapshotInfoContext; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.repositories.Repository; import org.elasticsearch.repositories.RepositoryData; @@ -28,7 +28,6 @@ import org.elasticsearch.snapshots.Snapshot; import org.elasticsearch.snapshots.SnapshotId; import org.elasticsearch.snapshots.SnapshotInfo; -import org.elasticsearch.snapshots.SnapshotMissingException; import org.elasticsearch.snapshots.SnapshotState; import org.elasticsearch.snapshots.SnapshotsService; import org.elasticsearch.tasks.Task; @@ -39,6 +38,7 @@ import org.elasticsearch.xpack.core.slm.SnapshotLifecyclePolicy; import org.elasticsearch.xpack.core.slm.SnapshotRetentionConfiguration; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -51,6 +51,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.oneOf; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -308,20 +309,26 @@ private static Repository createMockRepository(ThreadPool threadPool, List { - final GetSnapshotInfoContext getSnapshotInfoContext = invocation.getArgument(0); - final Set snapshotIds = new HashSet<>(getSnapshotInfoContext.snapshotIds()); + final Collection snapshotIdCollection = invocation.getArgument(0); + assertFalse("should not abort on failure", invocation.getArgument(1)); + final CheckedConsumer consumer = invocation.getArgument(3); + final ActionListener listener = invocation.getArgument(4); + + final Set snapshotIds = new HashSet<>(snapshotIdCollection); for (SnapshotInfo snapshotInfo : snapshotInfos) { if (snapshotIds.remove(snapshotInfo.snapshotId())) { - threadPool.generic().execute(ActionRunnable.supply(getSnapshotInfoContext, () -> snapshotInfo)); + threadPool.generic().execute(() -> { + try { + consumer.accept(snapshotInfo); + } catch (Exception e) { + fail(e); + } + }); } } - for (SnapshotId snapshotId : snapshotIds) { - threadPool.generic().execute(ActionRunnable.supply(getSnapshotInfoContext, () -> { - throw new SnapshotMissingException(REPO_NAME, snapshotId, null); - })); - } + listener.onResponse(null); return null; - }).when(repository).getSnapshotInfo(any()); + }).when(repository).getSnapshotInfo(any(), anyBoolean(), any(), any(), any()); doAnswer(invocation -> new RepositoryMetadata(REPO_NAME, "test", Settings.EMPTY)).when(repository).getMetadata(); From 35ff28025185f42e0c2b0a217151fb461e1eb7a1 Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 4 Mar 2024 11:59:39 +0000 Subject: [PATCH 219/250] Simplify TransportGetSnapshotsAction#sortSnapshots (#105895) No need to copy all the matching snapshots into a separate list before applying the size limit, we can allocate the final list straight away. --- .../get/TransportGetSnapshotsAction.java | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java index 9b74fb77c44b3..f7b5fec8a2dd5 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java @@ -510,7 +510,27 @@ private SnapshotsInRepo sortSnapshotsWithNoOffsetOrLimit(List snap return sortSnapshots(snapshotInfos.stream(), snapshotInfos.size(), 0, GetSnapshotsRequest.NO_LIMIT); } - private SnapshotsInRepo sortSnapshots(Stream infos, int totalCount, int offset, int size) { + private SnapshotsInRepo sortSnapshots(Stream snapshotInfoStream, int totalCount, int offset, int size) { + final var resultsStream = snapshotInfoStream.filter(buildAfterPredicate()).sorted(buildComparator()).skip(offset); + if (size == GetSnapshotsRequest.NO_LIMIT) { + return new SnapshotsInRepo(resultsStream.toList(), totalCount, 0); + } else { + final var allocateSize = Math.min(size, 1000); // ignore excessively-large sizes in request params + final var results = new ArrayList(allocateSize); + var remaining = 0; + for (var iterator = resultsStream.iterator(); iterator.hasNext();) { + final var snapshotInfo = iterator.next(); + if (results.size() < size) { + results.add(snapshotInfo); + } else { + remaining += 1; + } + } + return new SnapshotsInRepo(results, totalCount, remaining); + } + } + + private Comparator buildComparator() { final Comparator comparator = switch (sortBy) { case START_TIME -> BY_START_TIME; case NAME -> BY_NAME; @@ -520,26 +540,16 @@ private SnapshotsInRepo sortSnapshots(Stream infos, int totalCount case FAILED_SHARDS -> BY_FAILED_SHARDS_COUNT; case REPOSITORY -> BY_REPOSITORY; }; - - if (after != null) { - assert offset == 0 : "can't combine after and offset but saw [" + after + "] and offset [" + offset + "]"; - infos = infos.filter(buildAfterPredicate()); - } - infos = infos.sorted(order == SortOrder.DESC ? comparator.reversed() : comparator).skip(offset); - final List allSnapshots = infos.toList(); - final List snapshots; - if (size != GetSnapshotsRequest.NO_LIMIT) { - snapshots = allSnapshots.stream().limit(size + 1).toList(); - } else { - snapshots = allSnapshots; - } - final List resultSet = size != GetSnapshotsRequest.NO_LIMIT && size < snapshots.size() - ? snapshots.subList(0, size) - : snapshots; - return new SnapshotsInRepo(resultSet, totalCount, allSnapshots.size() - resultSet.size()); + return order == SortOrder.DESC ? comparator.reversed() : comparator; } private Predicate buildAfterPredicate() { + if (after == null) { + // TODO use constant when https://github.com/elastic/elasticsearch/pull/105881 merged + return snapshotInfo -> true; + } + assert offset == 0 : "can't combine after and offset but saw [" + after + "] and offset [" + offset + "]"; + final String snapshotName = after.snapshotName(); final String repoName = after.repoName(); final String value = after.value(); From 7ddf51918033e73a3a183ff23dcbc097b09d06b8 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 4 Mar 2024 13:08:19 +0100 Subject: [PATCH 220/250] Cleanup duplicate code in DenseVectorFieldMapper (#105887) Cleaning up some obvious duplication and dead abstract method. --- .../vectors/DenseVectorFieldMapper.java | 98 ++++++------------- 1 file changed, 31 insertions(+), 67 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java index 598a6383bfdaa..47efa0ca49771 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java @@ -250,8 +250,7 @@ public void readAndWriteValue(ByteBuffer byteBuffer, XContentBuilder b) throws I b.value(byteBuffer.get()); } - @Override - KnnByteVectorField createKnnVectorField(String name, byte[] vector, VectorSimilarityFunction function) { + private KnnByteVectorField createKnnVectorField(String name, byte[] vector, VectorSimilarityFunction function) { if (vector == null) { throw new IllegalArgumentException("vector value must not be null"); } @@ -261,11 +260,6 @@ KnnByteVectorField createKnnVectorField(String name, byte[] vector, VectorSimila return new KnnByteVectorField(name, vector, denseVectorFieldType); } - @Override - KnnFloatVectorField createKnnVectorField(String name, float[] vector, VectorSimilarityFunction function) { - throw new IllegalArgumentException("cannot create a float vector field from byte"); - } - @Override IndexFieldData.Builder fielddataBuilder(DenseVectorFieldType denseVectorFieldType, FieldDataContext fieldDataContext) { return new VectorIndexFieldData.Builder( @@ -452,8 +446,7 @@ public void readAndWriteValue(ByteBuffer byteBuffer, XContentBuilder b) throws I b.value(byteBuffer.getFloat()); } - @Override - KnnFloatVectorField createKnnVectorField(String name, float[] vector, VectorSimilarityFunction function) { + private KnnFloatVectorField createKnnVectorField(String name, float[] vector, VectorSimilarityFunction function) { if (vector == null) { throw new IllegalArgumentException("vector value must not be null"); } @@ -463,11 +456,6 @@ KnnFloatVectorField createKnnVectorField(String name, float[] vector, VectorSimi return new KnnFloatVectorField(name, vector, denseVectorFieldType); } - @Override - KnnByteVectorField createKnnVectorField(String name, byte[] vector, VectorSimilarityFunction function) { - throw new IllegalArgumentException("cannot create a byte vector field from float"); - } - @Override IndexFieldData.Builder fielddataBuilder(DenseVectorFieldType denseVectorFieldType, FieldDataContext fieldDataContext) { return new VectorIndexFieldData.Builder( @@ -615,10 +603,6 @@ ByteBuffer createByteBuffer(IndexVersion indexVersion, int numBytes) { public abstract void readAndWriteValue(ByteBuffer byteBuffer, XContentBuilder b) throws IOException; - abstract KnnFloatVectorField createKnnVectorField(String name, float[] vector, VectorSimilarityFunction function); - - abstract KnnByteVectorField createKnnVectorField(String name, byte[] vector, VectorSimilarityFunction function); - abstract IndexFieldData.Builder fielddataBuilder(DenseVectorFieldType denseVectorFieldType, FieldDataContext fieldDataContext); abstract void parseKnnVectorAndIndex(DocumentParserContext context, DenseVectorFieldMapper fieldMapper) throws IOException; @@ -1175,31 +1159,7 @@ public Query createKnnQuery( } public Query createExactKnnQuery(float[] queryVector) { - if (isIndexed() == false) { - throw new IllegalArgumentException( - "to perform knn search on field [" + name() + "], its mapping must have [index] set to [true]" - ); - } - if (queryVector.length != dims) { - throw new IllegalArgumentException( - "the query vector has a different dimension [" + queryVector.length + "] than the index vectors [" + dims + "]" - ); - } - elementType.checkVectorBounds(queryVector); - if (similarity == VectorSimilarity.DOT_PRODUCT || similarity == VectorSimilarity.COSINE) { - float squaredMagnitude = VectorUtil.dotProduct(queryVector, queryVector); - elementType.checkVectorMagnitude(similarity, ElementType.errorFloatElementsAppender(queryVector), squaredMagnitude); - if (similarity == VectorSimilarity.COSINE - && ElementType.FLOAT.equals(elementType) - && indexVersionCreated.onOrAfter(NORMALIZE_COSINE) - && isNotUnitVector(squaredMagnitude)) { - float length = (float) Math.sqrt(squaredMagnitude); - queryVector = Arrays.copyOf(queryVector, queryVector.length); - for (int i = 0; i < queryVector.length; i++) { - queryVector[i] /= length; - } - } - } + queryVector = validateAndNormalize(queryVector); VectorSimilarityFunction vectorSimilarityFunction = similarity.vectorSimilarityFunction(indexVersionCreated, elementType); return switch (elementType) { case BYTE -> { @@ -1242,12 +1202,38 @@ public Query createKnnQuery( Float similarityThreshold, BitSetProducer parentFilter ) { + queryVector = validateAndNormalize(queryVector); + Query knnQuery = switch (elementType) { + case BYTE -> { + byte[] bytes = new byte[queryVector.length]; + for (int i = 0; i < queryVector.length; i++) { + bytes[i] = (byte) queryVector[i]; + } + yield parentFilter != null + ? new ESDiversifyingChildrenByteKnnVectorQuery(name(), bytes, filter, numCands, parentFilter) + : new ESKnnByteVectorQuery(name(), bytes, numCands, filter); + } + case FLOAT -> parentFilter != null + ? new ESDiversifyingChildrenFloatKnnVectorQuery(name(), queryVector, filter, numCands, parentFilter) + : new ESKnnFloatVectorQuery(name(), queryVector, numCands, filter); + }; + + if (similarityThreshold != null) { + knnQuery = new VectorSimilarityQuery( + knnQuery, + similarityThreshold, + similarity.score(similarityThreshold, elementType, dims) + ); + } + return knnQuery; + } + + private float[] validateAndNormalize(float[] queryVector) { if (isIndexed() == false) { throw new IllegalArgumentException( "to perform knn search on field [" + name() + "], its mapping must have [index] set to [true]" ); } - if (queryVector.length != dims) { throw new IllegalArgumentException( "the query vector has a different dimension [" + queryVector.length + "] than the index vectors [" + dims + "]" @@ -1268,29 +1254,7 @@ && isNotUnitVector(squaredMagnitude)) { } } } - Query knnQuery = switch (elementType) { - case BYTE -> { - byte[] bytes = new byte[queryVector.length]; - for (int i = 0; i < queryVector.length; i++) { - bytes[i] = (byte) queryVector[i]; - } - yield parentFilter != null - ? new ESDiversifyingChildrenByteKnnVectorQuery(name(), bytes, filter, numCands, parentFilter) - : new ESKnnByteVectorQuery(name(), bytes, numCands, filter); - } - case FLOAT -> parentFilter != null - ? new ESDiversifyingChildrenFloatKnnVectorQuery(name(), queryVector, filter, numCands, parentFilter) - : new ESKnnFloatVectorQuery(name(), queryVector, numCands, filter); - }; - - if (similarityThreshold != null) { - knnQuery = new VectorSimilarityQuery( - knnQuery, - similarityThreshold, - similarity.score(similarityThreshold, elementType, dims) - ); - } - return knnQuery; + return queryVector; } VectorSimilarity getSimilarity() { From e55bdba027689dc34118f2013a2d2376ca3a768d Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Mon, 4 Mar 2024 13:22:41 +0100 Subject: [PATCH 221/250] Update docs/changelog/105578.yaml --- docs/changelog/105578.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/changelog/105578.yaml b/docs/changelog/105578.yaml index d7420096cb178..1ffa0128c1d0a 100644 --- a/docs/changelog/105578.yaml +++ b/docs/changelog/105578.yaml @@ -1,13 +1,13 @@ pr: 105578 summary: Upgrade to Lucene 9.10.0 area: Search -type: feature -issues: - - 104556 +type: enhancement +issues: [] highlight: - title: "New Lucene 9.10 release" + title: New Lucene 9.10 release body: |- - https://github.com/apache/lucene/pull/13090: Prevent humongous allocations in ScalarQuantizer when building quantiles. - https://github.com/apache/lucene/pull/12962: Speedup concurrent multi-segment HNSW graph search - https://github.com/apache/lucene/pull/13033: Range queries on numeric/date/ip fields now exit earlier on segments whose values don't intersect with the query range. This should especially help when there are other required clauses in the `bool` query and when the range filter is narrow, e.g. filtering on the last 5 minutes. - https://github.com/apache/lucene/pull/13026: `bool` queries that mix `filter` and `should` clauses will now propagate minimum competitive scores through the `should` clauses. This should yield speedups when sorting by descending score. + notable: true From b96f8f0ce6a6b5b2b652c981c106a632c0f659a6 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 4 Mar 2024 13:37:36 +0100 Subject: [PATCH 222/250] Dry up some painless code (#105885) Just some Intellij automatic fixes I found while researching other things, no need to have this duplication. --- .../painless/ContextDocGenerator.java | 126 ++++-------------- .../painless/ContextGeneratorCommon.java | 32 +++-- .../org/elasticsearch/painless/api/CIDR.java | 45 +------ .../common/network/CIDRUtils.java | 4 +- 4 files changed, 52 insertions(+), 155 deletions(-) diff --git a/modules/lang-painless/src/doc/java/org/elasticsearch/painless/ContextDocGenerator.java b/modules/lang-painless/src/doc/java/org/elasticsearch/painless/ContextDocGenerator.java index af702bd2e2fe3..d99a085d784b5 100644 --- a/modules/lang-painless/src/doc/java/org/elasticsearch/painless/ContextDocGenerator.java +++ b/modules/lang-painless/src/doc/java/org/elasticsearch/painless/ContextDocGenerator.java @@ -27,6 +27,7 @@ import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -176,7 +177,7 @@ private static void printSharedIndexPage( PrintStream sharedIndexStream = new PrintStream( Files.newOutputStream(sharedIndexPath, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), false, - StandardCharsets.UTF_8.name() + StandardCharsets.UTF_8 ) ) { @@ -205,7 +206,7 @@ private static void printContextIndexPage( PrintStream contextIndexStream = new PrintStream( Files.newOutputStream(contextIndexPath, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), false, - StandardCharsets.UTF_8.name() + StandardCharsets.UTF_8 ) ) { @@ -306,7 +307,7 @@ private static void printSharedPackagesPages( PrintStream sharedPackagesStream = new PrintStream( Files.newOutputStream(sharedClassesPath, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), false, - StandardCharsets.UTF_8.name() + StandardCharsets.UTF_8 ) ) { @@ -329,7 +330,7 @@ private static void printContextPackagesPages( PrintStream contextPackagesStream = new PrintStream( Files.newOutputStream(contextPackagesPath, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), false, - StandardCharsets.UTF_8.name() + StandardCharsets.UTF_8 ) ) { @@ -413,7 +414,7 @@ private static void printRootIndexPage(Path rootDir, List c PrintStream rootIndexStream = new PrintStream( Files.newOutputStream(rootIndexPath, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), false, - StandardCharsets.UTF_8.name() + StandardCharsets.UTF_8 ) ) { @@ -598,18 +599,7 @@ private static String getConstructorJavaDocLink(PainlessContextConstructorInfo c javaDocLink.append(constructorInfo.getDeclaring().replace('.', '/')); javaDocLink.append(".html#("); - for (int parameterIndex = 0; parameterIndex < constructorInfo.getParameters().size(); ++parameterIndex) { - - javaDocLink.append(getLinkType(constructorInfo.getParameters().get(parameterIndex))); - - if (parameterIndex + 1 < constructorInfo.getParameters().size()) { - javaDocLink.append(","); - } - } - - javaDocLink.append(")"); - - return javaDocLink.toString(); + return collectParameters(javaDocLink, constructorInfo.getParameters()); } private static String getMethodJavaDocLink(PainlessContextMethodInfo methodInfo) { @@ -621,11 +611,15 @@ private static String getMethodJavaDocLink(PainlessContextMethodInfo methodInfo) javaDocLink.append(methodInfo.getName()); javaDocLink.append("("); - for (int parameterIndex = 0; parameterIndex < methodInfo.getParameters().size(); ++parameterIndex) { + return collectParameters(javaDocLink, methodInfo.getParameters()); + } - javaDocLink.append(getLinkType(methodInfo.getParameters().get(parameterIndex))); + private static String collectParameters(StringBuilder javaDocLink, List parameters) { + for (int parameterIndex = 0; parameterIndex < parameters.size(); ++parameterIndex) { - if (parameterIndex + 1 < methodInfo.getParameters().size()) { + javaDocLink.append(getLinkType(parameters.get(parameterIndex))); + + if (parameterIndex + 1 < parameters.size()) { javaDocLink.append(","); } } @@ -708,32 +702,19 @@ private static List sortStaticInfos(Set staticExcludes, List(staticInfos); staticInfos.removeIf(staticExcludes::contains); - staticInfos.sort((si1, si2) -> { - String sv1; - String sv2; - - if (si1 instanceof PainlessContextMethodInfo) { - sv1 = ((PainlessContextMethodInfo) si1).getSortValue(); - } else if (si1 instanceof PainlessContextClassBindingInfo) { - sv1 = ((PainlessContextClassBindingInfo) si1).getSortValue(); - } else if (si1 instanceof PainlessContextInstanceBindingInfo) { - sv1 = ((PainlessContextInstanceBindingInfo) si1).getSortValue(); - } else { - throw new IllegalArgumentException("unexpected static info type"); - } - - if (si2 instanceof PainlessContextMethodInfo) { - sv2 = ((PainlessContextMethodInfo) si2).getSortValue(); - } else if (si2 instanceof PainlessContextClassBindingInfo) { - sv2 = ((PainlessContextClassBindingInfo) si2).getSortValue(); - } else if (si2 instanceof PainlessContextInstanceBindingInfo) { - sv2 = ((PainlessContextInstanceBindingInfo) si2).getSortValue(); + staticInfos.sort(Comparator.comparing(si -> { + String sv; + if (si instanceof PainlessContextMethodInfo) { + sv = ((PainlessContextMethodInfo) si).getSortValue(); + } else if (si instanceof PainlessContextClassBindingInfo) { + sv = ((PainlessContextClassBindingInfo) si).getSortValue(); + } else if (si instanceof PainlessContextInstanceBindingInfo) { + sv = ((PainlessContextInstanceBindingInfo) si).getSortValue(); } else { throw new IllegalArgumentException("unexpected static info type"); } - - return sv1.compareTo(sv2); - }); + return sv; + })); return staticInfos; } @@ -742,48 +723,9 @@ private static List sortClassInfos( Set classExcludes, List classInfos ) { - classInfos = new ArrayList<>(classInfos); - classInfos.removeIf( - v -> "void".equals(v.getName()) - || "boolean".equals(v.getName()) - || "byte".equals(v.getName()) - || "short".equals(v.getName()) - || "char".equals(v.getName()) - || "int".equals(v.getName()) - || "long".equals(v.getName()) - || "float".equals(v.getName()) - || "double".equals(v.getName()) - || "org.elasticsearch.painless.lookup.def".equals(v.getName()) - || isInternalClass(v.getName()) - || classExcludes.contains(v) - ); - - classInfos.sort((c1, c2) -> { - String n1 = c1.getName(); - String n2 = c2.getName(); - boolean i1 = c1.isImported(); - boolean i2 = c2.isImported(); - - String p1 = n1.substring(0, n1.lastIndexOf('.')); - String p2 = n2.substring(0, n2.lastIndexOf('.')); - - int compare = p1.compareTo(p2); - - if (compare == 0) { - if (i1 && i2) { - compare = n1.substring(n1.lastIndexOf('.') + 1).compareTo(n2.substring(n2.lastIndexOf('.') + 1)); - } else if (i1 == false && i2 == false) { - compare = n1.compareTo(n2); - } else { - compare = Boolean.compare(i1, i2) * -1; - } - } - - return compare; - }); - - return classInfos; + classInfos.removeIf(v -> ContextGeneratorCommon.isExcludedClassInfo(v) || classExcludes.contains(v)); + return ContextGeneratorCommon.sortFilteredClassInfos(classInfos); } private static Map getDisplayNames(List classInfos) { @@ -802,19 +744,5 @@ private static Map getDisplayNames(List getDisplayNames(Collection sortClassInfos(Collection unsortedClassInfos) { - List classInfos = new ArrayList<>(unsortedClassInfos); - classInfos.removeIf( - v -> "void".equals(v.getName()) - || "boolean".equals(v.getName()) - || "byte".equals(v.getName()) - || "short".equals(v.getName()) - || "char".equals(v.getName()) - || "int".equals(v.getName()) - || "long".equals(v.getName()) - || "float".equals(v.getName()) - || "double".equals(v.getName()) - || "org.elasticsearch.painless.lookup.def".equals(v.getName()) - || isInternalClass(v.getName()) - ); + classInfos.removeIf(ContextGeneratorCommon::isExcludedClassInfo); + return sortFilteredClassInfos(classInfos); + } + + static boolean isExcludedClassInfo(PainlessContextClassInfo v) { + return "void".equals(v.getName()) + || "boolean".equals(v.getName()) + || "byte".equals(v.getName()) + || "short".equals(v.getName()) + || "char".equals(v.getName()) + || "int".equals(v.getName()) + || "long".equals(v.getName()) + || "float".equals(v.getName()) + || "double".equals(v.getName()) + || "org.elasticsearch.painless.lookup.def".equals(v.getName()) + || isInternalClass(v.getName()); + } + static List sortFilteredClassInfos(List classInfos) { classInfos.sort((c1, c2) -> { String n1 = c1.getName(); String n2 = c2.getName(); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/api/CIDR.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/api/CIDR.java index 8ce32e182cb18..c3e39b5905cdc 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/api/CIDR.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/api/CIDR.java @@ -8,10 +8,10 @@ package org.elasticsearch.painless.api; +import org.elasticsearch.common.network.CIDRUtils; import org.elasticsearch.common.network.InetAddresses; import org.elasticsearch.core.Tuple; -import java.net.InetAddress; import java.util.Arrays; /** @@ -28,7 +28,7 @@ public class CIDR { */ public CIDR(String cidr) { if (cidr.contains("/")) { - final Tuple range = getLowerUpper(InetAddresses.parseCidr(cidr)); + final Tuple range = CIDRUtils.getLowerUpper(InetAddresses.parseCidr(cidr)); lower = range.v1(); upper = range.v2(); } else { @@ -51,48 +51,13 @@ public boolean contains(String addressToCheck) { return isBetween(parsedAddress, lower, upper); } - private static Tuple getLowerUpper(Tuple cidr) { - final InetAddress value = cidr.v1(); - final Integer prefixLength = cidr.v2(); - - if (prefixLength < 0 || prefixLength > 8 * value.getAddress().length) { - throw new IllegalArgumentException( - "illegal prefixLength '" + prefixLength + "'. Must be 0-32 for IPv4 ranges, 0-128 for IPv6 ranges" - ); - } - - byte[] lower = value.getAddress(); - byte[] upper = value.getAddress(); - // Borrowed from Lucene - for (int i = prefixLength; i < 8 * lower.length; i++) { - int m = 1 << (7 - (i & 7)); - lower[i >> 3] &= (byte) ~m; - upper[i >> 3] |= (byte) m; - } - return new Tuple<>(lower, upper); - } - private static boolean isBetween(byte[] addr, byte[] lower, byte[] upper) { if (addr.length != lower.length) { - addr = encode(addr); - lower = encode(lower); - upper = encode(upper); + addr = CIDRUtils.encode(addr); + lower = CIDRUtils.encode(lower); + upper = CIDRUtils.encode(upper); } return Arrays.compareUnsigned(lower, addr) <= 0 && Arrays.compareUnsigned(upper, addr) >= 0; } - // Borrowed from Lucene to make this consistent IP fields matching for the mix of IPv4 and IPv6 values - // Modified signature to avoid extra conversions - private static byte[] encode(byte[] address) { - final byte[] IPV4_PREFIX = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1 }; - if (address.length == 4) { - byte[] mapped = new byte[16]; - System.arraycopy(IPV4_PREFIX, 0, mapped, 0, IPV4_PREFIX.length); - System.arraycopy(address, 0, mapped, IPV4_PREFIX.length, address.length); - address = mapped; - } else if (address.length != 16) { - throw new UnsupportedOperationException("Only IPv4 and IPv6 addresses are supported"); - } - return address; - } } diff --git a/server/src/main/java/org/elasticsearch/common/network/CIDRUtils.java b/server/src/main/java/org/elasticsearch/common/network/CIDRUtils.java index ea4d6da9b7bec..3b5a9ae1589f8 100644 --- a/server/src/main/java/org/elasticsearch/common/network/CIDRUtils.java +++ b/server/src/main/java/org/elasticsearch/common/network/CIDRUtils.java @@ -48,7 +48,7 @@ public static boolean isInRange(byte[] addr, String cidrAddress) { return isBetween(addr, lower, upper); } - private static Tuple getLowerUpper(Tuple cidr) { + public static Tuple getLowerUpper(Tuple cidr) { final InetAddress value = cidr.v1(); final Integer prefixLength = cidr.v2(); @@ -81,7 +81,7 @@ private static boolean isBetween(byte[] addr, byte[] lower, byte[] upper) { // Borrowed from Lucene to make this consistent IP fields matching for the mix of IPv4 and IPv6 values // Modified signature to avoid extra conversions - private static byte[] encode(byte[] address) { + public static byte[] encode(byte[] address) { if (address.length == 4) { byte[] mapped = new byte[16]; System.arraycopy(IPV4_PREFIX, 0, mapped, 0, IPV4_PREFIX.length); From 9c1a0797d18795f449461433911f2a9558a9ec7b Mon Sep 17 00:00:00 2001 From: Niels Bauman <33722607+nielsbauman@users.noreply.github.com> Date: Mon, 4 Mar 2024 13:59:21 +0100 Subject: [PATCH 223/250] Make Health API more resilient to multi-version clusters (#105789) First check whether the full cluster supports a specific indicator (feature) before we mark an indicator as "unknown" when (meta) data is missing from the cluster state. --- docs/changelog/105789.yaml | 6 +++ .../rest-api-spec/test/health/10_basic.yml | 6 +-- .../elasticsearch/health/HealthFeatures.java | 10 +++- .../node/DiskHealthIndicatorService.java | 17 ++++++- .../ShardsCapacityHealthIndicatorService.java | 15 +++++- .../elasticsearch/node/NodeConstruction.java | 6 +-- ...sitoryIntegrityHealthIndicatorService.java | 23 +++++++-- .../node/DiskHealthIndicatorServiceTests.java | 46 ++++++++++++----- ...dsCapacityHealthIndicatorServiceTests.java | 51 +++++++++++++++---- ...yIntegrityHealthIndicatorServiceTests.java | 19 +++++-- 10 files changed, 157 insertions(+), 42 deletions(-) create mode 100644 docs/changelog/105789.yaml diff --git a/docs/changelog/105789.yaml b/docs/changelog/105789.yaml new file mode 100644 index 0000000000000..02a6936fa3294 --- /dev/null +++ b/docs/changelog/105789.yaml @@ -0,0 +1,6 @@ +pr: 105789 +summary: Make Health API more resilient to multi-version clusters +area: Health +type: bug +issues: + - 90183 diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/10_basic.yml index 5e6ca8247997c..1dc35c165b4e0 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/10_basic.yml @@ -1,10 +1,8 @@ --- "cluster health basic test": - skip: - version: all - reason: "AwaitsFix https://github.com/elastic/elasticsearch/issues/90183" - # version: "- 8.3.99" - # reason: "health was only added in 8.2.0, and master_is_stable in 8.4.0" + version: "- 8.3.99" + reason: "health was only added in 8.2.0, and master_is_stable in 8.4.0" - do: health_report: { } diff --git a/server/src/main/java/org/elasticsearch/health/HealthFeatures.java b/server/src/main/java/org/elasticsearch/health/HealthFeatures.java index 3a5d11f862efc..4b3bcf7e7278f 100644 --- a/server/src/main/java/org/elasticsearch/health/HealthFeatures.java +++ b/server/src/main/java/org/elasticsearch/health/HealthFeatures.java @@ -13,13 +13,21 @@ import org.elasticsearch.features.NodeFeature; import java.util.Map; +import java.util.Set; public class HealthFeatures implements FeatureSpecification { public static final NodeFeature SUPPORTS_HEALTH = new NodeFeature("health.supports_health"); + public static final NodeFeature SUPPORTS_SHARDS_CAPACITY_INDICATOR = new NodeFeature("health.shards_capacity_indicator"); + public static final NodeFeature SUPPORTS_EXTENDED_REPOSITORY_INDICATOR = new NodeFeature("health.extended_repository_indicator"); + + @Override + public Set getFeatures() { + return Set.of(SUPPORTS_EXTENDED_REPOSITORY_INDICATOR); + } @Override public Map getHistoricalFeatures() { - return Map.of(SUPPORTS_HEALTH, Version.V_8_5_0); + return Map.of(SUPPORTS_HEALTH, Version.V_8_5_0, SUPPORTS_SHARDS_CAPACITY_INDICATOR, Version.V_8_8_0); } } diff --git a/server/src/main/java/org/elasticsearch/health/node/DiskHealthIndicatorService.java b/server/src/main/java/org/elasticsearch/health/node/DiskHealthIndicatorService.java index 2805aa88a7e54..3304b71b4ca31 100644 --- a/server/src/main/java/org/elasticsearch/health/node/DiskHealthIndicatorService.java +++ b/server/src/main/java/org/elasticsearch/health/node/DiskHealthIndicatorService.java @@ -17,7 +17,9 @@ import org.elasticsearch.cluster.routing.RoutingNodes; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.features.FeatureService; import org.elasticsearch.health.Diagnosis; +import org.elasticsearch.health.HealthFeatures; import org.elasticsearch.health.HealthIndicatorDetails; import org.elasticsearch.health.HealthIndicatorImpact; import org.elasticsearch.health.HealthIndicatorResult; @@ -71,9 +73,11 @@ public class DiskHealthIndicatorService implements HealthIndicatorService { private static final String IMPACT_CLUSTER_FUNCTIONALITY_UNAVAILABLE_ID = "cluster_functionality_unavailable"; private final ClusterService clusterService; + private final FeatureService featureService; - public DiskHealthIndicatorService(ClusterService clusterService) { + public DiskHealthIndicatorService(ClusterService clusterService, FeatureService featureService) { this.clusterService = clusterService; + this.featureService = featureService; } @Override @@ -83,8 +87,18 @@ public String name() { @Override public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResourcesCount, HealthInfo healthInfo) { + ClusterState clusterState = clusterService.state(); Map diskHealthInfoMap = healthInfo.diskInfoByNode(); if (diskHealthInfoMap == null || diskHealthInfoMap.isEmpty()) { + if (featureService.clusterHasFeature(clusterState, HealthFeatures.SUPPORTS_HEALTH) == false) { + return createIndicator( + HealthStatus.GREEN, + "No disk usage data available. The cluster currently has mixed versions (an upgrade may be in progress).", + HealthIndicatorDetails.EMPTY, + List.of(), + List.of() + ); + } /* * If there is no disk health info, that either means that a new health node was just elected, or something is seriously * wrong with health data collection on the health node. Either way, we immediately return UNKNOWN. If there are at least @@ -98,7 +112,6 @@ public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResources Collections.emptyList() ); } - ClusterState clusterState = clusterService.state(); logNodesMissingHealthInfo(diskHealthInfoMap, clusterState); DiskHealthAnalyzer diskHealthAnalyzer = new DiskHealthAnalyzer(diskHealthInfoMap, clusterState); diff --git a/server/src/main/java/org/elasticsearch/health/node/ShardsCapacityHealthIndicatorService.java b/server/src/main/java/org/elasticsearch/health/node/ShardsCapacityHealthIndicatorService.java index 1852e504b61db..16e18b69d5c1d 100644 --- a/server/src/main/java/org/elasticsearch/health/node/ShardsCapacityHealthIndicatorService.java +++ b/server/src/main/java/org/elasticsearch/health/node/ShardsCapacityHealthIndicatorService.java @@ -12,7 +12,9 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.TriFunction; import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.features.FeatureService; import org.elasticsearch.health.Diagnosis; +import org.elasticsearch.health.HealthFeatures; import org.elasticsearch.health.HealthIndicatorDetails; import org.elasticsearch.health.HealthIndicatorImpact; import org.elasticsearch.health.HealthIndicatorResult; @@ -90,9 +92,11 @@ public class ShardsCapacityHealthIndicatorService implements HealthIndicatorServ ); private final ClusterService clusterService; + private final FeatureService featureService; - public ShardsCapacityHealthIndicatorService(ClusterService clusterService) { + public ShardsCapacityHealthIndicatorService(ClusterService clusterService, FeatureService featureService) { this.clusterService = clusterService; + this.featureService = featureService; } @Override @@ -105,6 +109,15 @@ public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResources var state = clusterService.state(); var healthMetadata = HealthMetadata.getFromClusterState(state); if (healthMetadata == null || healthMetadata.getShardLimitsMetadata() == null) { + if (featureService.clusterHasFeature(state, HealthFeatures.SUPPORTS_SHARDS_CAPACITY_INDICATOR) == false) { + return createIndicator( + HealthStatus.GREEN, + "No shard limits configured yet. The cluster currently has mixed versions (an upgrade may be in progress).", + HealthIndicatorDetails.EMPTY, + List.of(), + List.of() + ); + } return unknownIndicator(); } diff --git a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java index 9323ec63c0d2d..19a6d200189f2 100644 --- a/server/src/main/java/org/elasticsearch/node/NodeConstruction.java +++ b/server/src/main/java/org/elasticsearch/node/NodeConstruction.java @@ -1196,9 +1196,9 @@ private Module loadDiagnosticServices( var serverHealthIndicatorServices = Stream.of( new StableMasterHealthIndicatorService(coordinationDiagnosticsService, clusterService), - new RepositoryIntegrityHealthIndicatorService(clusterService), - new DiskHealthIndicatorService(clusterService), - new ShardsCapacityHealthIndicatorService(clusterService) + new RepositoryIntegrityHealthIndicatorService(clusterService, featureService), + new DiskHealthIndicatorService(clusterService, featureService), + new ShardsCapacityHealthIndicatorService(clusterService, featureService) ); var pluginHealthIndicatorServices = pluginsService.filterPlugins(HealthPlugin.class) .flatMap(plugin -> plugin.getHealthIndicatorServices().stream()); diff --git a/server/src/main/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorService.java b/server/src/main/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorService.java index 0b460b5cb2fb7..67afddcb70664 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorService.java @@ -12,7 +12,9 @@ import org.elasticsearch.cluster.metadata.RepositoriesMetadata; import org.elasticsearch.cluster.metadata.RepositoryMetadata; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.features.FeatureService; import org.elasticsearch.health.Diagnosis; +import org.elasticsearch.health.HealthFeatures; import org.elasticsearch.health.HealthIndicatorDetails; import org.elasticsearch.health.HealthIndicatorImpact; import org.elasticsearch.health.HealthIndicatorResult; @@ -59,6 +61,8 @@ public class RepositoryIntegrityHealthIndicatorService implements HealthIndicato public static final String NO_REPOS_CONFIGURED = "No snapshot repositories configured."; public static final String ALL_REPOS_HEALTHY = "All repositories are healthy."; public static final String NO_REPO_HEALTH_INFO = "No repository health info."; + public static final String MIXED_VERSIONS = + "No repository health info. The cluster currently has mixed versions (an upgrade may be in progress)."; public static final List IMPACTS = List.of( new HealthIndicatorImpact( @@ -95,9 +99,11 @@ public class RepositoryIntegrityHealthIndicatorService implements HealthIndicato ); private final ClusterService clusterService; + private final FeatureService featureService; - public RepositoryIntegrityHealthIndicatorService(ClusterService clusterService) { + public RepositoryIntegrityHealthIndicatorService(ClusterService clusterService, FeatureService featureService) { this.clusterService = clusterService; + this.featureService = featureService; } @Override @@ -128,7 +134,7 @@ public HealthIndicatorResult calculate(boolean verbose, int maxAffectedResources /** * Analyzer for the cluster's repositories health; aids in constructing a {@link HealthIndicatorResult}. */ - static class RepositoryHealthAnalyzer { + class RepositoryHealthAnalyzer { private final ClusterState clusterState; private final int totalRepositories; private final List corruptedRepositories; @@ -137,6 +143,7 @@ static class RepositoryHealthAnalyzer { private final Set invalidRepositories = new HashSet<>(); private final Set nodesWithInvalidRepos = new HashSet<>(); private final HealthStatus healthStatus; + private boolean clusterHasFeature = true; private RepositoryHealthAnalyzer( ClusterState clusterState, @@ -167,7 +174,15 @@ private RepositoryHealthAnalyzer( || invalidRepositories.isEmpty() == false) { healthStatus = YELLOW; } else if (repositoriesHealthByNode.isEmpty()) { - healthStatus = UNKNOWN; + clusterHasFeature = featureService.clusterHasFeature( + clusterState, + HealthFeatures.SUPPORTS_EXTENDED_REPOSITORY_INDICATOR + ) == false; + if (clusterHasFeature) { + healthStatus = GREEN; + } else { + healthStatus = UNKNOWN; + } } else { healthStatus = GREEN; } @@ -179,7 +194,7 @@ public HealthStatus getHealthStatus() { public String getSymptom() { if (healthStatus == GREEN) { - return ALL_REPOS_HEALTHY; + return clusterHasFeature ? ALL_REPOS_HEALTHY : MIXED_VERSIONS; } else if (healthStatus == UNKNOWN) { return NO_REPO_HEALTH_INFO; } diff --git a/server/src/test/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceTests.java index a622c1ff600d6..0d38aaf5b3e4a 100644 --- a/server/src/test/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/health/node/DiskHealthIndicatorServiceTests.java @@ -26,7 +26,9 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.features.FeatureService; import org.elasticsearch.health.Diagnosis; +import org.elasticsearch.health.HealthFeatures; import org.elasticsearch.health.HealthIndicatorImpact; import org.elasticsearch.health.HealthIndicatorResult; import org.elasticsearch.health.HealthStatus; @@ -39,6 +41,8 @@ import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentType; +import org.junit.Before; +import org.mockito.Mockito; import java.io.IOException; import java.util.Collection; @@ -66,6 +70,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.iterableWithSize; import static org.hamcrest.Matchers.startsWith; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -98,10 +103,20 @@ public class DiskHealthIndicatorServiceTests extends ESTestCase { DiscoveryNodeRole.TRANSFORM_ROLE ); + private FeatureService featureService; + + @Before + public void setUp() throws Exception { + super.setUp(); + + featureService = Mockito.mock(FeatureService.class); + Mockito.when(featureService.clusterHasFeature(any(), any())).thenReturn(true); + } + public void testServiceBasics() { Set discoveryNodes = createNodesWithAllRoles(); ClusterService clusterService = createClusterService(discoveryNodes, false); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); { HealthStatus expectedStatus = HealthStatus.UNKNOWN; HealthInfo healthInfo = HealthInfo.EMPTY_HEALTH_INFO; @@ -125,7 +140,7 @@ public void testServiceBasics() { public void testIndicatorYieldsGreenWhenNodeHasUnknownStatus() { Set discoveryNodes = createNodesWithAllRoles(); ClusterService clusterService = createClusterService(discoveryNodes, false); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); HealthStatus expectedStatus = HealthStatus.GREEN; HealthInfo healthInfo = createHealthInfoWithOneUnhealthyNode(HealthStatus.UNKNOWN, discoveryNodes); @@ -136,7 +151,7 @@ public void testIndicatorYieldsGreenWhenNodeHasUnknownStatus() { public void testGreen() throws IOException { Set discoveryNodes = createNodesWithAllRoles(); ClusterService clusterService = createClusterService(discoveryNodes, false); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); HealthStatus expectedStatus = HealthStatus.GREEN; HealthInfo healthInfo = createHealthInfoWithOneUnhealthyNode(expectedStatus, discoveryNodes); HealthIndicatorResult result = diskHealthIndicatorService.calculate(true, healthInfo); @@ -171,7 +186,7 @@ public void testYellowMixedNodes() throws IOException { final var clusterService = createClusterService(Set.of(), allNodes, indexNameToNodeIdsMap); HealthStatus expectedStatus = HealthStatus.YELLOW; HealthInfo healthInfo = createHealthInfo(new HealthInfoConfig(expectedStatus, allNodes.size(), allNodes)); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); HealthIndicatorResult result = diskHealthIndicatorService.calculate(true, healthInfo); assertThat(result.status(), equalTo(expectedStatus)); assertThat(result.symptom(), containsString("with roles: [data")); @@ -249,7 +264,7 @@ public void testRedNoBlockedIndicesAndRedAllRoleNodes() throws IOException { indexNameToNodeIdsMap.put(indexName, new HashSet<>(randomNonEmptySubsetOf(affectedNodeIds))); } ClusterService clusterService = createClusterService(Set.of(), discoveryNodes, indexNameToNodeIdsMap); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); Map diskInfoByNode = new HashMap<>(); for (DiscoveryNode discoveryNode : discoveryNodes) { if (affectedNodeIds.contains(discoveryNode.getId())) { @@ -313,7 +328,7 @@ public void testRedNoBlockedIndicesAndRedAllRoleNodes() throws IOException { public void testRedWithBlockedIndicesAndGreenNodes() throws IOException { Set discoveryNodes = createNodesWithAllRoles(); ClusterService clusterService = createClusterService(discoveryNodes, true); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); HealthStatus expectedStatus = HealthStatus.RED; HealthInfo healthInfo = createHealthInfoWithOneUnhealthyNode(HealthStatus.GREEN, discoveryNodes); @@ -358,7 +373,7 @@ public void testRedWithBlockedIndicesAndGreenNodes() throws IOException { public void testRedWithBlockedIndicesAndYellowNodes() throws IOException { Set discoveryNodes = createNodesWithAllRoles(); ClusterService clusterService = createClusterService(discoveryNodes, true); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); HealthStatus expectedStatus = HealthStatus.RED; int numberOfYellowNodes = randomIntBetween(1, discoveryNodes.size()); HealthInfo healthInfo = createHealthInfo(new HealthInfoConfig(HealthStatus.YELLOW, numberOfYellowNodes, discoveryNodes)); @@ -437,7 +452,7 @@ public void testRedBlockedIndicesAndRedAllRolesNodes() throws IOException { } } ClusterService clusterService = createClusterService(blockedIndices, discoveryNodes, indexNameToNodeIdsMap); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); HealthIndicatorResult result = diskHealthIndicatorService.calculate(true, healthInfo); assertThat(result.status(), equalTo(expectedStatus)); assertThat( @@ -476,7 +491,7 @@ public void testRedNodesWithoutAnyBlockedIndices() throws IOException { indexNameToNodeIdsMap.put(indexName, nonRedNodeIds); } ClusterService clusterService = createClusterService(Set.of(), discoveryNodes, indexNameToNodeIdsMap); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); HealthIndicatorResult result = diskHealthIndicatorService.calculate(true, healthInfo); assertThat(result.status(), equalTo(expectedStatus)); assertThat(result.impacts().size(), equalTo(3)); @@ -512,7 +527,7 @@ public void testMissingHealthInfo() { Set discoveryNodesInClusterState = new HashSet<>(discoveryNodes); discoveryNodesInClusterState.add(DiscoveryNodeUtils.create(randomAlphaOfLength(30), UUID.randomUUID().toString())); ClusterService clusterService = createClusterService(discoveryNodesInClusterState, false); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); { HealthInfo healthInfo = HealthInfo.EMPTY_HEALTH_INFO; HealthIndicatorResult result = diskHealthIndicatorService.calculate(true, healthInfo); @@ -544,7 +559,7 @@ public void testUnhealthyMasterNodes() { Set roles = Set.of(DiscoveryNodeRole.MASTER_ROLE, otherRole); Set discoveryNodes = createNodes(roles); ClusterService clusterService = createClusterService(discoveryNodes, false); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); HealthStatus expectedStatus = randomFrom(HealthStatus.RED, HealthStatus.YELLOW); int numberOfProblemNodes = randomIntBetween(1, discoveryNodes.size()); HealthInfo healthInfo = createHealthInfo(new HealthInfoConfig(expectedStatus, numberOfProblemNodes, discoveryNodes)); @@ -599,7 +614,7 @@ public void testUnhealthyNonDataNonMasterNodes() { Set roles = new HashSet<>(randomNonEmptySubsetOf(OTHER_ROLES)); Set nodes = createNodes(roles); ClusterService clusterService = createClusterService(nodes, false); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); HealthStatus expectedStatus = randomFrom(HealthStatus.RED, HealthStatus.YELLOW); int numberOfProblemNodes = randomIntBetween(1, nodes.size()); HealthInfo healthInfo = createHealthInfo(new HealthInfoConfig(expectedStatus, numberOfProblemNodes, nodes)); @@ -655,7 +670,7 @@ public void testBlockedIndexWithRedNonDataNodesAndYellowDataNodes() { Set masterNodes = createNodes(masterRole); Set otherNodes = createNodes(otherRoles); ClusterService clusterService = createClusterService(Sets.union(Sets.union(dataNodes, masterNodes), otherNodes), true); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); int numberOfRedMasterNodes = randomIntBetween(1, masterNodes.size()); int numberOfRedOtherNodes = randomIntBetween(1, otherNodes.size()); int numberOfYellowDataNodes = randomIntBetween(1, dataNodes.size()); @@ -877,7 +892,7 @@ public void testLimitNumberOfAffectedResources() { Set masterNodes = createNodes(20, masterRole); Set otherNodes = createNodes(10, otherRoles); ClusterService clusterService = createClusterService(Sets.union(Sets.union(dataNodes, masterNodes), otherNodes), true); - DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService); + DiskHealthIndicatorService diskHealthIndicatorService = new DiskHealthIndicatorService(clusterService, featureService); int numberOfRedMasterNodes = masterNodes.size(); int numberOfRedOtherNodes = otherNodes.size(); int numberOfYellowDataNodes = dataNodes.size(); @@ -1055,9 +1070,11 @@ static ClusterState createClusterState( Collection nodes, Map> indexNameToNodeIdsMap ) { + Map> features = new HashMap<>(); DiscoveryNodes.Builder nodesBuilder = DiscoveryNodes.builder(); for (DiscoveryNode node : nodes) { nodesBuilder = nodesBuilder.add(node); + features.put(node.getId(), Set.of(HealthFeatures.SUPPORTS_HEALTH.id())); } nodesBuilder.localNodeId(randomFrom(nodes).getId()); nodesBuilder.masterNodeId(randomFrom(nodes).getId()); @@ -1093,6 +1110,7 @@ static ClusterState createClusterState( state.metadata(metadata.generateClusterUuidIfNeeded().build()); state.routingTable(routingTable.build()); state.blocks(clusterBlocksBuilder); + state.nodeFeatures(features); return state.build(); } diff --git a/server/src/test/java/org/elasticsearch/health/node/ShardsCapacityHealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/health/node/ShardsCapacityHealthIndicatorServiceTests.java index f6e856079012d..c57f19999a915 100644 --- a/server/src/test/java/org/elasticsearch/health/node/ShardsCapacityHealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/health/node/ShardsCapacityHealthIndicatorServiceTests.java @@ -19,6 +19,8 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; +import org.elasticsearch.features.FeatureService; +import org.elasticsearch.health.HealthFeatures; import org.elasticsearch.health.HealthStatus; import org.elasticsearch.health.metadata.HealthMetadata; import org.elasticsearch.index.IndexVersion; @@ -36,6 +38,7 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; +import org.mockito.Mockito; import java.io.IOException; import java.util.List; @@ -60,6 +63,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; public class ShardsCapacityHealthIndicatorServiceTests extends ESTestCase { @@ -68,6 +72,7 @@ public class ShardsCapacityHealthIndicatorServiceTests extends ESTestCase { private static ThreadPool threadPool; private ClusterService clusterService; + private FeatureService featureService; private DiscoveryNode dataNode; private DiscoveryNode frozenNode; @@ -86,6 +91,9 @@ public void setUp() throws Exception { .build(); clusterService = ClusterServiceUtils.createClusterService(threadPool); + + featureService = Mockito.mock(FeatureService.class); + Mockito.when(featureService.clusterHasFeature(any(), any())).thenReturn(true); } @After @@ -113,7 +121,7 @@ public void testNoShardsCapacityMetadata() throws IOException { createIndexInDataNode(100) ) ); - var target = new ShardsCapacityHealthIndicatorService(clusterService); + var target = new ShardsCapacityHealthIndicatorService(clusterService, featureService); var indicatorResult = target.calculate(true, HealthInfo.EMPTY_HEALTH_INFO); assertEquals(indicatorResult.status(), HealthStatus.UNKNOWN); @@ -127,7 +135,10 @@ public void testIndicatorYieldsGreenInCaseThereIsRoom() throws IOException { int maxShardsPerNode = randomValidMaxShards(); int maxShardsPerNodeFrozen = randomValidMaxShards(); var clusterService = createClusterService(maxShardsPerNode, maxShardsPerNodeFrozen, createIndexInDataNode(maxShardsPerNode / 4)); - var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService).calculate(true, HealthInfo.EMPTY_HEALTH_INFO); + var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService, featureService).calculate( + true, + HealthInfo.EMPTY_HEALTH_INFO + ); assertEquals(indicatorResult.status(), HealthStatus.GREEN); assertTrue(indicatorResult.impacts().isEmpty()); @@ -151,7 +162,10 @@ public void testIndicatorYieldsYellowInCaseThereIsNotEnoughRoom() throws IOExcep // Only data_nodes does not have enough space int maxShardsPerNodeFrozen = randomValidMaxShards(); var clusterService = createClusterService(25, maxShardsPerNodeFrozen, createIndexInDataNode(4)); - var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService).calculate(true, HealthInfo.EMPTY_HEALTH_INFO); + var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService, featureService).calculate( + true, + HealthInfo.EMPTY_HEALTH_INFO + ); assertEquals(indicatorResult.status(), YELLOW); assertEquals(indicatorResult.symptom(), "Cluster is close to reaching the configured maximum number of shards for data nodes."); @@ -174,7 +188,10 @@ public void testIndicatorYieldsYellowInCaseThereIsNotEnoughRoom() throws IOExcep // Only frozen_nodes does not have enough space int maxShardsPerNode = randomValidMaxShards(); var clusterService = createClusterService(maxShardsPerNode, 25, createIndexInFrozenNode(4)); - var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService).calculate(true, HealthInfo.EMPTY_HEALTH_INFO); + var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService, featureService).calculate( + true, + HealthInfo.EMPTY_HEALTH_INFO + ); assertEquals(indicatorResult.status(), YELLOW); assertEquals( @@ -199,7 +216,10 @@ public void testIndicatorYieldsYellowInCaseThereIsNotEnoughRoom() throws IOExcep { // Both data and frozen nodes does not have enough space var clusterService = createClusterService(25, 25, createIndexInDataNode(4), createIndexInFrozenNode(4)); - var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService).calculate(true, HealthInfo.EMPTY_HEALTH_INFO); + var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService, featureService).calculate( + true, + HealthInfo.EMPTY_HEALTH_INFO + ); assertEquals(indicatorResult.status(), YELLOW); assertEquals( @@ -230,7 +250,10 @@ public void testIndicatorYieldsRedInCaseThereIsNotEnoughRoom() throws IOExceptio // Only data_nodes does not have enough space int maxShardsPerNodeFrozen = randomValidMaxShards(); var clusterService = createClusterService(25, maxShardsPerNodeFrozen, createIndexInDataNode(11)); - var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService).calculate(true, HealthInfo.EMPTY_HEALTH_INFO); + var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService, featureService).calculate( + true, + HealthInfo.EMPTY_HEALTH_INFO + ); assertEquals(indicatorResult.status(), RED); assertEquals(indicatorResult.symptom(), "Cluster is close to reaching the configured maximum number of shards for data nodes."); @@ -253,7 +276,10 @@ public void testIndicatorYieldsRedInCaseThereIsNotEnoughRoom() throws IOExceptio // Only frozen_nodes does not have enough space int maxShardsPerNode = randomValidMaxShards(); var clusterService = createClusterService(maxShardsPerNode, 25, createIndexInFrozenNode(11)); - var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService).calculate(true, HealthInfo.EMPTY_HEALTH_INFO); + var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService, featureService).calculate( + true, + HealthInfo.EMPTY_HEALTH_INFO + ); assertEquals(indicatorResult.status(), RED); assertEquals( @@ -278,7 +304,10 @@ public void testIndicatorYieldsRedInCaseThereIsNotEnoughRoom() throws IOExceptio { // Both data and frozen nodes does not have enough space var clusterService = createClusterService(25, 25, createIndexInDataNode(11), createIndexInFrozenNode(11)); - var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService).calculate(true, HealthInfo.EMPTY_HEALTH_INFO); + var indicatorResult = new ShardsCapacityHealthIndicatorService(clusterService, featureService).calculate( + true, + HealthInfo.EMPTY_HEALTH_INFO + ); assertEquals(indicatorResult.status(), RED); assertEquals( @@ -397,7 +426,11 @@ private ClusterState createClusterState( metadata.put(idxMetadata); } - return ClusterState.builder(clusterState).metadata(metadata).build(); + var features = Set.of(HealthFeatures.SUPPORTS_SHARDS_CAPACITY_INDICATOR.id()); + return ClusterState.builder(clusterState) + .metadata(metadata) + .nodeFeatures(Map.of(dataNode.getId(), features, frozenNode.getId(), features)) + .build(); } private static IndexMetadata.Builder createIndexInDataNode(int shards) { diff --git a/server/src/test/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceTests.java b/server/src/test/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceTests.java index 0dfe27ee6dc50..572375d64d8b8 100644 --- a/server/src/test/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceTests.java +++ b/server/src/test/java/org/elasticsearch/snapshots/RepositoryIntegrityHealthIndicatorServiceTests.java @@ -18,8 +18,10 @@ import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.features.FeatureService; import org.elasticsearch.health.Diagnosis; import org.elasticsearch.health.Diagnosis.Resource.Type; +import org.elasticsearch.health.HealthFeatures; import org.elasticsearch.health.HealthIndicatorDetails; import org.elasticsearch.health.HealthIndicatorResult; import org.elasticsearch.health.SimpleHealthIndicatorDetails; @@ -27,12 +29,14 @@ import org.elasticsearch.health.node.RepositoriesHealthInfo; import org.elasticsearch.test.ESTestCase; import org.junit.Before; +import org.mockito.Mockito; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import static org.elasticsearch.cluster.node.DiscoveryNode.DISCOVERY_NODE_COMPARATOR; @@ -47,6 +51,7 @@ import static org.elasticsearch.snapshots.RepositoryIntegrityHealthIndicatorService.NAME; import static org.elasticsearch.snapshots.RepositoryIntegrityHealthIndicatorService.UNKNOWN_DEFINITION; import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -55,6 +60,7 @@ public class RepositoryIntegrityHealthIndicatorServiceTests extends ESTestCase { private DiscoveryNode node1; private DiscoveryNode node2; private HealthInfo healthInfo; + private FeatureService featureService; @Before public void setUp() throws Exception { @@ -74,6 +80,9 @@ public void setUp() throws Exception { ) ) ); + + featureService = Mockito.mock(FeatureService.class); + Mockito.when(featureService.clusterHasFeature(any(), any())).thenReturn(true); } public void testIsGreenWhenAllRepositoriesAreHealthy() { @@ -349,11 +358,13 @@ public void testMappedFieldsForTelemetry() { } private ClusterState createClusterStateWith(RepositoriesMetadata metadata) { - var builder = ClusterState.builder(new ClusterName("test-cluster")); + var features = Set.of(HealthFeatures.SUPPORTS_EXTENDED_REPOSITORY_INDICATOR.id()); + var builder = ClusterState.builder(new ClusterName("test-cluster")) + .nodes(DiscoveryNodes.builder().add(node1).add(node2).build()) + .nodeFeatures(Map.of(node1.getId(), features, node2.getId(), features)); if (metadata != null) { builder.metadata(Metadata.builder().putCustom(RepositoriesMetadata.TYPE, metadata)); } - builder.nodes(DiscoveryNodes.builder().add(node1).add(node2).build()); return builder.build(); } @@ -361,10 +372,10 @@ private static RepositoryMetadata createRepositoryMetadata(String name, boolean return new RepositoryMetadata(name, "uuid", "s3", Settings.EMPTY, corrupted ? CORRUPTED_REPO_GEN : EMPTY_REPO_GEN, EMPTY_REPO_GEN); } - private static RepositoryIntegrityHealthIndicatorService createRepositoryIntegrityHealthIndicatorService(ClusterState clusterState) { + private RepositoryIntegrityHealthIndicatorService createRepositoryIntegrityHealthIndicatorService(ClusterState clusterState) { var clusterService = mock(ClusterService.class); when(clusterService.state()).thenReturn(clusterState); - return new RepositoryIntegrityHealthIndicatorService(clusterService); + return new RepositoryIntegrityHealthIndicatorService(clusterService, featureService); } private SimpleHealthIndicatorDetails createDetails(int total, int corruptedCount, List corrupted, int unknown, int invalid) { From fc8e2b789730c01dd9e9d994b82a9a29beabaef7 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Mon, 4 Mar 2024 14:01:21 +0100 Subject: [PATCH 224/250] Introduce Predicate Utilities for always true/false use-cases (#105881) Just a suggetion. I think this would save us a bit of memory here and there. We have loads of places where the always true lambdas are used with `Predicate.or/and`. Found this initially when looking into field caps performance where we used to heavily compose these but many spots in security and index name resolution gain from these predicates. The better toString also helps in some cases at least when debugging. --- .../org/elasticsearch/core/Predicates.java | 92 +++++++++++++++++++ .../ingest/common/KeyValueProcessor.java | 3 +- .../TransportClusterSearchShardsAction.java | 5 +- .../state/TransportClusterStateAction.java | 3 +- .../alias/get/TransportGetAliasesAction.java | 3 +- .../org/elasticsearch/action/bulk/Retry.java | 5 +- .../org/elasticsearch/action/bulk/Retry2.java | 5 +- .../action/fieldcaps/FieldCapabilities.java | 5 +- .../action/fieldcaps/ResponseRewriter.java | 3 +- .../action/search/TransportSearchAction.java | 5 +- .../org/elasticsearch/bootstrap/ESPolicy.java | 6 +- .../cluster/ClusterStateObserver.java | 7 +- .../metadata/IndexNameExpressionResolver.java | 15 ++- .../cluster/routing/IndexRoutingTable.java | 6 +- .../cluster/routing/RoutingTable.java | 5 +- .../common/network/NetworkUtils.java | 3 +- .../org/elasticsearch/common/regex/Regex.java | 5 +- .../elasticsearch/env/NodeEnvironment.java | 5 +- .../index/shard/ShardSplittingQuery.java | 8 +- .../indices/IndicesQueryCache.java | 3 +- .../elasticsearch/indices/SystemIndices.java | 5 +- .../elasticsearch/plugins/MapperPlugin.java | 3 +- .../plugins/internal/RestExtension.java | 5 +- .../aggregations/bucket/terms/LongTerms.java | 3 +- .../snapshots/SnapshotsService.java | 3 +- .../transport/ProxyConnectionStrategy.java | 3 +- .../transport/TransportService.java | 5 +- .../put/UpdateSettingsRequestTests.java | 3 +- .../cluster/metadata/MetadataTests.java | 3 +- .../decider/AllocationDecidersTests.java | 3 +- .../index/reindex/ReindexRequestTests.java | 5 +- .../index/mapper/MockFieldFilterPlugin.java | 3 +- .../test/AbstractSerializationTestCase.java | 3 +- .../test/AbstractXContentTestCase.java | 3 +- .../test/InternalTestCluster.java | 7 +- .../test/rest/yaml/section/VersionRange.java | 5 +- .../ReactiveStorageDeciderService.java | 5 +- .../ccr/action/TransportFollowInfoAction.java | 3 +- .../elasticsearch/xpack/core/ml/MlTasks.java | 7 +- .../authc/support/UserRoleMapper.java | 3 +- .../mapper/expressiondsl/ExpressionModel.java | 3 +- .../restriction/WorkflowsRestriction.java | 5 +- .../core/security/support/Automatons.java | 6 ++ .../core/security/support/StringMatcher.java | 9 +- .../security/support/StringMatcherTests.java | 3 +- .../xpack/ml/MlDailyMaintenanceService.java | 5 +- .../TransportEvaluateDataFrameAction.java | 3 +- .../TransportInternalInferModelAction.java | 5 +- .../action/TransportSetUpgradeModeAction.java | 7 +- .../MlMemoryAutoscalingDecider.java | 3 +- .../xpack/ml/utils/VoidChainTaskExecutor.java | 4 +- .../TransportGetServiceAccountAction.java | 3 +- .../audit/logfile/LoggingAuditTrail.java | 3 +- .../authc/service/FileTokensTool.java | 3 +- .../transform/transforms/TransformTask.java | 5 +- 55 files changed, 246 insertions(+), 93 deletions(-) create mode 100644 libs/core/src/main/java/org/elasticsearch/core/Predicates.java diff --git a/libs/core/src/main/java/org/elasticsearch/core/Predicates.java b/libs/core/src/main/java/org/elasticsearch/core/Predicates.java new file mode 100644 index 0000000000000..47ac9ef258d68 --- /dev/null +++ b/libs/core/src/main/java/org/elasticsearch/core/Predicates.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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.core; + +import java.util.function.Predicate; + +/** + * Utilities around predicates. + */ +public enum Predicates { + ; + + @SuppressWarnings("rawtypes") + private static final Predicate NEVER = new Predicate() { + @Override + public boolean test(Object o) { + return false; + } + + @Override + public Predicate and(Predicate other) { + return this; + } + + @Override + public Predicate negate() { + return ALWAYS; + } + + @Override + public Predicate or(Predicate other) { + return other; + } + + @Override + public String toString() { + return "Predicate[NEVER]"; + } + }; + + @SuppressWarnings("rawtypes") + private static final Predicate ALWAYS = new Predicate() { + @Override + public boolean test(Object o) { + return true; + } + + @Override + public Predicate and(Predicate other) { + return other; + } + + @Override + public Predicate negate() { + return NEVER; + } + + @Override + public Predicate or(Predicate other) { + return this; + } + + @Override + public String toString() { + return "Predicate[ALWAYS]"; + } + }; + + /** + * @return a predicate that accepts all input values + * @param type of the predicate + */ + @SuppressWarnings("unchecked") + public static Predicate always() { + return (Predicate) ALWAYS; + } + + /** + * @return a predicate that rejects all input values + * @param type of the predicate + */ + @SuppressWarnings("unchecked") + public static Predicate never() { + return (Predicate) NEVER; + } +} diff --git a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/KeyValueProcessor.java b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/KeyValueProcessor.java index 8c90beed4d01c..0c6e37f675e1d 100644 --- a/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/KeyValueProcessor.java +++ b/modules/ingest-common/src/main/java/org/elasticsearch/ingest/common/KeyValueProcessor.java @@ -11,6 +11,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.core.Predicates; import org.elasticsearch.ingest.AbstractProcessor; import org.elasticsearch.ingest.ConfigurationUtils; import org.elasticsearch.ingest.IngestDocument; @@ -100,7 +101,7 @@ private static Consumer buildExecution( final Predicate keyFilter; if (includeKeys == null) { if (excludeKeys == null) { - keyFilter = key -> true; + keyFilter = Predicates.always(); } else { keyFilter = key -> excludeKeys.contains(key) == false; } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/TransportClusterSearchShardsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/TransportClusterSearchShardsAction.java index ccfd192246c0a..826fa453e0402 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/TransportClusterSearchShardsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/shards/TransportClusterSearchShardsAction.java @@ -21,6 +21,7 @@ import org.elasticsearch.cluster.routing.ShardRouting; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.core.Predicates; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.indices.IndicesService; import org.elasticsearch.search.internal.AliasFilter; @@ -85,8 +86,8 @@ protected void masterOperation( final String[] aliases = indexNameExpressionResolver.indexAliases( clusterState, index, - aliasMetadata -> true, - dataStreamAlias -> true, + Predicates.always(), + Predicates.always(), true, indicesAndAliases ); diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/state/TransportClusterStateAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/state/TransportClusterStateAction.java index 29bffa3949258..c6431c7a593cd 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/state/TransportClusterStateAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/state/TransportClusterStateAction.java @@ -28,6 +28,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.cluster.version.CompatibilityVersions; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.Index; @@ -89,7 +90,7 @@ protected void masterOperation( final CancellableTask cancellableTask = (CancellableTask) task; final Predicate acceptableClusterStatePredicate = request.waitForMetadataVersion() == null - ? clusterState -> true + ? Predicates.always() : clusterState -> clusterState.metadata().version() >= request.waitForMetadataVersion(); final Predicate acceptableClusterStateOrFailedPredicate = request.local() diff --git a/server/src/main/java/org/elasticsearch/action/admin/indices/alias/get/TransportGetAliasesAction.java b/server/src/main/java/org/elasticsearch/action/admin/indices/alias/get/TransportGetAliasesAction.java index 3e8e6fbfde75c..4f7525c700fc2 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/indices/alias/get/TransportGetAliasesAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/indices/alias/get/TransportGetAliasesAction.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.logging.DeprecationCategory; import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.indices.SystemIndices; import org.elasticsearch.indices.SystemIndices.SystemIndexAccessLevel; @@ -160,7 +161,7 @@ private static void checkSystemIndexAccess( ) { final Predicate systemIndexAccessAllowPredicate; if (systemIndexAccessLevel == SystemIndexAccessLevel.NONE) { - systemIndexAccessAllowPredicate = indexName -> false; + systemIndexAccessAllowPredicate = Predicates.never(); } else if (systemIndexAccessLevel == SystemIndexAccessLevel.RESTRICTED) { systemIndexAccessAllowPredicate = systemIndices.getProductSystemIndexNamePredicate(threadContext); } else { diff --git a/server/src/main/java/org/elasticsearch/action/bulk/Retry.java b/server/src/main/java/org/elasticsearch/action/bulk/Retry.java index 33fb81a6520cb..62ef9a08f0070 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/Retry.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/Retry.java @@ -16,6 +16,7 @@ import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.TimeValue; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.threadpool.Scheduler; @@ -104,14 +105,14 @@ static class RetryHandler extends DelegatingActionListener true)); + addResponses(bulkItemResponses, Predicates.always()); finishHim(); } else { if (canRetry(bulkItemResponses)) { addResponses(bulkItemResponses, (r -> r.isFailed() == false)); retry(createBulkRequestForRetry(bulkItemResponses)); } else { - addResponses(bulkItemResponses, (r -> true)); + addResponses(bulkItemResponses, Predicates.always()); finishHim(); } } diff --git a/server/src/main/java/org/elasticsearch/action/bulk/Retry2.java b/server/src/main/java/org/elasticsearch/action/bulk/Retry2.java index 784ba1eb95d5d..999bd6af925a6 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/Retry2.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/Retry2.java @@ -14,6 +14,7 @@ import org.elasticsearch.action.DocWriteRequest; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.TimeValue; import org.elasticsearch.rest.RestStatus; @@ -183,7 +184,7 @@ public void onResponse(BulkResponse bulkItemResponses) { bulkItemResponses.getItems().length ); // we're done here, include all responses - addResponses(bulkItemResponses, (r -> true)); + addResponses(bulkItemResponses, Predicates.always()); listener.onResponse(getAccumulatedResponse()); } else { if (canRetry(bulkItemResponses)) { @@ -201,7 +202,7 @@ public void onResponse(BulkResponse bulkItemResponses) { bulkItemResponses.getTook(), bulkItemResponses.getItems().length ); - addResponses(bulkItemResponses, (r -> true)); + addResponses(bulkItemResponses, Predicates.always()); listener.onResponse(getAccumulatedResponse()); } } diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java index 095a5ec8f5594..856571c305615 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilities.java @@ -13,6 +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.Predicates; import org.elasticsearch.index.mapper.TimeSeriesParams; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.InstantiatingObjectParser; @@ -567,7 +568,7 @@ private String[] filterIndices(int length, Predicate pred) { } FieldCapabilities build(boolean withIndices) { - final String[] indices = withIndices ? filterIndices(totalIndices, ic -> true) : null; + final String[] indices = withIndices ? filterIndices(totalIndices, Predicates.always()) : null; // Iff this field is searchable in some indices AND non-searchable in others // we record the list of non-searchable indices @@ -603,7 +604,7 @@ FieldCapabilities build(boolean withIndices) { // Collect all indices that have this field. If it is marked differently in different indices, we cannot really // make a decisions which index is "right" and which index is "wrong" so collecting all indices where this field // is present is probably the only sensible thing to do here - metricConflictsIndices = Objects.requireNonNullElseGet(indices, () -> filterIndices(totalIndices, ic -> true)); + metricConflictsIndices = Objects.requireNonNullElseGet(indices, () -> filterIndices(totalIndices, Predicates.always())); } else { metricConflictsIndices = null; } diff --git a/server/src/main/java/org/elasticsearch/action/fieldcaps/ResponseRewriter.java b/server/src/main/java/org/elasticsearch/action/fieldcaps/ResponseRewriter.java index 38b0287522207..c4e9b1bce6d81 100644 --- a/server/src/main/java/org/elasticsearch/action/fieldcaps/ResponseRewriter.java +++ b/server/src/main/java/org/elasticsearch/action/fieldcaps/ResponseRewriter.java @@ -10,6 +10,7 @@ import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; +import org.elasticsearch.core.Predicates; import java.util.HashMap; import java.util.Map; @@ -49,7 +50,7 @@ private static Function buildTra String[] filters, String[] allowedTypes ) { - Predicate test = ifc -> true; + Predicate test = Predicates.always(); Set objects = null; Set nestedObjects = null; if (allowedTypes.length > 0) { diff --git a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java index d80322b2954c6..0922e15999e8c 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -51,6 +51,7 @@ import org.elasticsearch.common.util.concurrent.CountDown; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexNotFoundException; @@ -198,8 +199,8 @@ private Map buildPerIndexOriginalIndices( String[] aliases = indexNameExpressionResolver.indexAliases( clusterState, index, - aliasMetadata -> true, - dataStreamAlias -> true, + Predicates.always(), + Predicates.always(), true, indicesAndAliases ); diff --git a/server/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java b/server/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java index 4eea930589dc7..e8244fcd576ff 100644 --- a/server/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java +++ b/server/src/main/java/org/elasticsearch/bootstrap/ESPolicy.java @@ -8,6 +8,7 @@ package org.elasticsearch.bootstrap; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.SuppressForbidden; import java.io.FilePermission; @@ -201,7 +202,10 @@ public String getActions() { // from this policy file or further restrict it to code sources // that you specify, because Thread.stop() is potentially unsafe." // not even sure this method still works... - private static final Permission BAD_DEFAULT_NUMBER_ONE = new BadDefaultPermission(new RuntimePermission("stopThread"), p -> true); + private static final Permission BAD_DEFAULT_NUMBER_ONE = new BadDefaultPermission( + new RuntimePermission("stopThread"), + Predicates.always() + ); // default policy file states: // "allows anyone to listen on dynamic ports" diff --git a/server/src/main/java/org/elasticsearch/cluster/ClusterStateObserver.java b/server/src/main/java/org/elasticsearch/cluster/ClusterStateObserver.java index 74deb90ee411a..40ddafa498ecb 100644 --- a/server/src/main/java/org/elasticsearch/cluster/ClusterStateObserver.java +++ b/server/src/main/java/org/elasticsearch/cluster/ClusterStateObserver.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.TimeValue; import org.elasticsearch.threadpool.ThreadPool; @@ -33,8 +34,6 @@ public class ClusterStateObserver { public static final Predicate NON_NULL_MASTER_PREDICATE = state -> state.nodes().getMasterNode() != null; - private static final Predicate MATCH_ALL_CHANGES_PREDICATE = state -> true; - private final ClusterApplierService clusterApplierService; private final ThreadPool threadPool; private final ThreadContext contextHolder; @@ -109,11 +108,11 @@ public boolean isTimedOut() { } public void waitForNextChange(Listener listener) { - waitForNextChange(listener, MATCH_ALL_CHANGES_PREDICATE); + waitForNextChange(listener, Predicates.always()); } public void waitForNextChange(Listener listener, @Nullable TimeValue timeOutValue) { - waitForNextChange(listener, MATCH_ALL_CHANGES_PREDICATE, timeOutValue); + waitForNextChange(listener, Predicates.always(), timeOutValue); } public void waitForNextChange(Listener listener, Predicate statePredicate) { 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 0446b479b191d..4c3318d8d2f6a 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexNameExpressionResolver.java @@ -26,6 +26,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.IndexVersion; @@ -59,8 +60,6 @@ public class IndexNameExpressionResolver { private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(IndexNameExpressionResolver.class); - private static final Predicate ALWAYS_TRUE = s -> true; - public static final String EXCLUDED_DATA_STREAMS_KEY = "es.excluded_ds"; public static final IndexVersion SYSTEM_INDEX_ENFORCEMENT_INDEX_VERSION = IndexVersions.V_8_0_0; @@ -101,7 +100,7 @@ public String[] concreteIndexNamesWithSystemIndexAccess(ClusterState state, Indi false, request.includeDataStreams(), SystemIndexAccessLevel.BACKWARDS_COMPATIBLE_ONLY, - ALWAYS_TRUE, + Predicates.always(), this.getNetNewSystemIndexPredicate() ); return concreteIndexNames(context, request.indices()); @@ -397,7 +396,7 @@ Index[] concreteIndices(Context context, String... indexExpressions) { private void checkSystemIndexAccess(Context context, Set concreteIndices) { final Predicate systemIndexAccessPredicate = context.getSystemIndexAccessPredicate(); - if (systemIndexAccessPredicate == ALWAYS_TRUE) { + if (systemIndexAccessPredicate == Predicates.always()) { return; } doCheckSystemIndexAccess(context, concreteIndices, systemIndexAccessPredicate); @@ -947,11 +946,11 @@ public Predicate getSystemIndexAccessPredicate() { final SystemIndexAccessLevel systemIndexAccessLevel = getSystemIndexAccessLevel(); final Predicate systemIndexAccessLevelPredicate; if (systemIndexAccessLevel == SystemIndexAccessLevel.NONE) { - systemIndexAccessLevelPredicate = s -> false; + systemIndexAccessLevelPredicate = Predicates.never(); } else if (systemIndexAccessLevel == SystemIndexAccessLevel.BACKWARDS_COMPATIBLE_ONLY) { systemIndexAccessLevelPredicate = getNetNewSystemIndexPredicate(); } else if (systemIndexAccessLevel == SystemIndexAccessLevel.ALL) { - systemIndexAccessLevelPredicate = ALWAYS_TRUE; + systemIndexAccessLevelPredicate = Predicates.always(); } else { // everything other than allowed should be included in the deprecation message systemIndexAccessLevelPredicate = systemIndices.getProductSystemIndexNamePredicate(threadContext); @@ -981,7 +980,7 @@ public static class Context { private final Predicate netNewSystemIndexPredicate; Context(ClusterState state, IndicesOptions options, SystemIndexAccessLevel systemIndexAccessLevel) { - this(state, options, systemIndexAccessLevel, ALWAYS_TRUE, s -> false); + this(state, options, systemIndexAccessLevel, Predicates.always(), Predicates.never()); } Context( @@ -1722,7 +1721,7 @@ public ResolverContext() { } public ResolverContext(long startTime) { - super(null, null, startTime, false, false, false, false, SystemIndexAccessLevel.ALL, name -> false, name -> false); + super(null, null, startTime, false, false, false, false, SystemIndexAccessLevel.ALL, Predicates.never(), Predicates.never()); } @Override diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/IndexRoutingTable.java b/server/src/main/java/org/elasticsearch/cluster/routing/IndexRoutingTable.java index 8fbdd3790e158..6679f17a0427b 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/IndexRoutingTable.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/IndexRoutingTable.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; @@ -54,11 +55,10 @@ public class IndexRoutingTable implements SimpleDiffable { private static final List> PRIORITY_REMOVE_CLAUSES = Stream.>of( shardRouting -> shardRouting.isPromotableToPrimary() == false, - shardRouting -> true + Predicates.always() ) .flatMap( - p1 -> Stream.>of(ShardRouting::unassigned, ShardRouting::initializing, shardRouting -> true) - .map(p1::and) + p1 -> Stream.>of(ShardRouting::unassigned, ShardRouting::initializing, Predicates.always()).map(p1::and) ) .toList(); private final Index index; diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java b/server/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java index 723d65fbc2a3f..855793e9e9782 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/RoutingTable.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.shard.ShardId; @@ -249,7 +250,7 @@ private GroupShardsIterator allSatisfyingPredicateShardsGrouped( } public ShardsIterator allShards(String[] indices) { - return allShardsSatisfyingPredicate(indices, shardRouting -> true, false); + return allShardsSatisfyingPredicate(indices, Predicates.always(), false); } public ShardsIterator allActiveShards(String[] indices) { @@ -257,7 +258,7 @@ public ShardsIterator allActiveShards(String[] indices) { } public ShardsIterator allShardsIncludingRelocationTargets(String[] indices) { - return allShardsSatisfyingPredicate(indices, shardRouting -> true, true); + return allShardsSatisfyingPredicate(indices, Predicates.always(), true); } private ShardsIterator allShardsSatisfyingPredicate( diff --git a/server/src/main/java/org/elasticsearch/common/network/NetworkUtils.java b/server/src/main/java/org/elasticsearch/common/network/NetworkUtils.java index f7cfff8402304..b2602b9c4f9d0 100644 --- a/server/src/main/java/org/elasticsearch/common/network/NetworkUtils.java +++ b/server/src/main/java/org/elasticsearch/common/network/NetworkUtils.java @@ -10,6 +10,7 @@ import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.Constants; +import org.elasticsearch.core.Predicates; import java.io.IOException; import java.net.Inet4Address; @@ -188,7 +189,7 @@ static InetAddress[] getGlobalAddresses() throws IOException { /** Returns all addresses (any scope) for interfaces that are up. * This is only used to pick a publish address, when the user set network.host to a wildcard */ public static InetAddress[] getAllAddresses() throws IOException { - return filterAllAddresses(address -> true, "no up-and-running addresses found"); + return filterAllAddresses(Predicates.always(), "no up-and-running addresses found"); } static Optional maybeGetInterfaceByName(List networkInterfaces, String name) { diff --git a/server/src/main/java/org/elasticsearch/common/regex/Regex.java b/server/src/main/java/org/elasticsearch/common/regex/Regex.java index 532fc2ae9a019..039f484f1ebca 100644 --- a/server/src/main/java/org/elasticsearch/common/regex/Regex.java +++ b/server/src/main/java/org/elasticsearch/common/regex/Regex.java @@ -14,6 +14,7 @@ import org.apache.lucene.util.automaton.CharacterRunAutomaton; import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.common.Strings; +import org.elasticsearch.core.Predicates; import java.util.ArrayList; import java.util.Arrays; @@ -102,12 +103,12 @@ public static Automaton simpleMatchToAutomaton(String... patterns) { */ public static Predicate simpleMatcher(String... patterns) { if (patterns == null || patterns.length == 0) { - return str -> false; + return Predicates.never(); } boolean hasWildcard = false; for (String pattern : patterns) { if (isMatchAllPattern(pattern)) { - return str -> true; + return Predicates.always(); } if (isSimpleMatchPattern(pattern)) { hasWildcard = true; diff --git a/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java b/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java index 1d8a9ef1ce1c4..b246802d06fee 100644 --- a/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java +++ b/server/src/main/java/org/elasticsearch/env/NodeEnvironment.java @@ -38,6 +38,7 @@ import org.elasticsearch.core.CheckedFunction; import org.elasticsearch.core.CheckedRunnable; import org.elasticsearch.core.IOUtils; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.TimeValue; @@ -1119,7 +1120,7 @@ public Path[] availableShardPaths(ShardId shardId) { * Returns all folder names in ${data.paths}/indices folder */ public Set availableIndexFolders() throws IOException { - return availableIndexFolders(p -> false); + return availableIndexFolders(Predicates.never()); } /** @@ -1147,7 +1148,7 @@ public Set availableIndexFolders(Predicate excludeIndexPathIdsPr * @throws IOException if an I/O exception occurs traversing the filesystem */ public Set availableIndexFoldersForPath(final DataPath dataPath) throws IOException { - return availableIndexFoldersForPath(dataPath, p -> false); + return availableIndexFoldersForPath(dataPath, Predicates.never()); } /** diff --git a/server/src/main/java/org/elasticsearch/index/shard/ShardSplittingQuery.java b/server/src/main/java/org/elasticsearch/index/shard/ShardSplittingQuery.java index ca9de756ca211..389485ac4eaf2 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/ShardSplittingQuery.java +++ b/server/src/main/java/org/elasticsearch/index/shard/ShardSplittingQuery.java @@ -33,6 +33,7 @@ import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.routing.IndexRouting; import org.elasticsearch.common.lucene.search.Queries; +import org.elasticsearch.core.Predicates; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.cache.bitset.BitsetFilterCache; import org.elasticsearch.index.mapper.IdFieldMapper; @@ -140,7 +141,12 @@ public Scorer scorer(LeafReaderContext context) throws IOException { * of the document that contains them. */ FixedBitSet hasRoutingValue = new FixedBitSet(leafReader.maxDoc()); - findSplitDocs(RoutingFieldMapper.NAME, ref -> false, leafReader, maybeWrapConsumer.apply(hasRoutingValue::set)); + findSplitDocs( + RoutingFieldMapper.NAME, + Predicates.never(), + leafReader, + maybeWrapConsumer.apply(hasRoutingValue::set) + ); IntConsumer bitSetConsumer = maybeWrapConsumer.apply(bitSet::set); findSplitDocs(IdFieldMapper.NAME, includeInShard, leafReader, docId -> { if (hasRoutingValue.get(docId) == false) { diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesQueryCache.java b/server/src/main/java/org/elasticsearch/indices/IndicesQueryCache.java index 7394e5eb89458..a40a5ab2e2fe8 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesQueryCache.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesQueryCache.java @@ -26,6 +26,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.index.cache.query.QueryCacheStats; import org.elasticsearch.index.shard.ShardId; @@ -78,7 +79,7 @@ public IndicesQueryCache(Settings settings) { logger.debug("using [node] query cache with size [{}] max filter count [{}]", size, count); if (INDICES_QUERIES_CACHE_ALL_SEGMENTS_SETTING.get(settings)) { // Use the default skip_caching_factor (i.e., 10f) in Lucene - cache = new ElasticsearchLRUQueryCache(count, size.getBytes(), context -> true, 10f); + cache = new ElasticsearchLRUQueryCache(count, size.getBytes(), Predicates.always(), 10f); } else { cache = new ElasticsearchLRUQueryCache(count, size.getBytes()); } diff --git a/server/src/main/java/org/elasticsearch/indices/SystemIndices.java b/server/src/main/java/org/elasticsearch/indices/SystemIndices.java index f23f28e4c1047..3261ac83a7e67 100644 --- a/server/src/main/java/org/elasticsearch/indices/SystemIndices.java +++ b/server/src/main/java/org/elasticsearch/indices/SystemIndices.java @@ -33,6 +33,7 @@ import org.elasticsearch.core.Booleans; import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.Index; import org.elasticsearch.plugins.SystemIndexPlugin; @@ -384,11 +385,11 @@ static SystemIndexDescriptor findMatchingDescriptor(SystemIndexDescriptor[] inde public Predicate getProductSystemIndexNamePredicate(ThreadContext threadContext) { final String product = threadContext.getHeader(EXTERNAL_SYSTEM_INDEX_ACCESS_CONTROL_HEADER_KEY); if (product == null) { - return name -> false; + return Predicates.never(); } final CharacterRunAutomaton automaton = productToSystemIndicesMatcher.get(product); if (automaton == null) { - return name -> false; + return Predicates.never(); } return automaton::run; } diff --git a/server/src/main/java/org/elasticsearch/plugins/MapperPlugin.java b/server/src/main/java/org/elasticsearch/plugins/MapperPlugin.java index 5124c94e545c0..401c014488f88 100644 --- a/server/src/main/java/org/elasticsearch/plugins/MapperPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/MapperPlugin.java @@ -8,6 +8,7 @@ package org.elasticsearch.plugins; +import org.elasticsearch.core.Predicates; import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.index.mapper.MetadataFieldMapper; import org.elasticsearch.index.mapper.RuntimeField; @@ -69,7 +70,7 @@ default Function> getFieldFilter() { * The default field predicate applied, which doesn't filter anything. That means that by default get mappings, get index * get field mappings and field capabilities API will return every field that's present in the mappings. */ - Predicate NOOP_FIELD_PREDICATE = field -> true; + Predicate NOOP_FIELD_PREDICATE = Predicates.always(); /** * The default field filter applied, which doesn't filter anything. That means that by default get mappings, get index diff --git a/server/src/main/java/org/elasticsearch/plugins/internal/RestExtension.java b/server/src/main/java/org/elasticsearch/plugins/internal/RestExtension.java index 4864e6bf31222..175d10a096b55 100644 --- a/server/src/main/java/org/elasticsearch/plugins/internal/RestExtension.java +++ b/server/src/main/java/org/elasticsearch/plugins/internal/RestExtension.java @@ -8,6 +8,7 @@ package org.elasticsearch.plugins.internal; +import org.elasticsearch.core.Predicates; import org.elasticsearch.rest.RestHandler; import org.elasticsearch.rest.action.cat.AbstractCatAction; @@ -38,12 +39,12 @@ static RestExtension allowAll() { return new RestExtension() { @Override public Predicate getCatActionsFilter() { - return action -> true; + return Predicates.always(); } @Override public Predicate getActionsFilter() { - return handler -> true; + return Predicates.always(); } }; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java index 45067208cbdd2..b0af2c3d4e618 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.core.Predicates; import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.aggregations.AggregationReduceContext; import org.elasticsearch.search.aggregations.AggregatorReducer; @@ -208,7 +209,7 @@ protected AggregatorReducer getLeaderReducer(AggregationReduceContext reduceCont } else if (format == DocValueFormat.UNSIGNED_LONG_SHIFTED) { needsPromoting = docFormat -> docFormat == DocValueFormat.RAW; } else { - needsPromoting = docFormat -> false; + needsPromoting = Predicates.never(); } return new AggregatorReducer() { diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java index a0782fa8814cd..d505a6ded4809 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotsService.java @@ -73,6 +73,7 @@ import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.ListenableFuture; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.Index; @@ -2266,7 +2267,7 @@ public static IndexVersion minCompatibleVersion( IndexVersion minCompatVersion = minNodeVersion; final Collection snapshotIds = repositoryData.getSnapshotIds(); for (SnapshotId snapshotId : snapshotIds.stream() - .filter(excluded == null ? sn -> true : Predicate.not(excluded::contains)) + .filter(excluded == null ? Predicates.always() : Predicate.not(excluded::contains)) .toList()) { final IndexVersion known = repositoryData.getVersion(snapshotId); // If we don't have the version cached in the repository data yet we load it from the snapshot info blobs diff --git a/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java index cfb6f872ce748..b0c4a6cd95156 100644 --- a/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java +++ b/server/src/main/java/org/elasticsearch/transport/ProxyConnectionStrategy.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.concurrent.CountDown; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.IndexVersions; @@ -182,7 +183,7 @@ public class ProxyConnectionStrategy extends RemoteConnectionStrategy { connectionManager.getCredentialsManager() ), actualProfile.getHandshakeTimeout(), - cn -> true, + Predicates.always(), listener.map(resp -> { ClusterName remote = resp.getClusterName(); if (remoteClusterName.compareAndSet(null, remote)) { diff --git a/server/src/main/java/org/elasticsearch/transport/TransportService.java b/server/src/main/java/org/elasticsearch/transport/TransportService.java index 7f1d63b092cdb..2ade579f216e4 100644 --- a/server/src/main/java/org/elasticsearch/transport/TransportService.java +++ b/server/src/main/java/org/elasticsearch/transport/TransportService.java @@ -37,6 +37,7 @@ import org.elasticsearch.core.Booleans; import org.elasticsearch.core.IOUtils; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.UpdateForV9; @@ -356,7 +357,7 @@ protected void doStop() { // but there may still be pending handlers for node-local requests since this connection is not closed, and we may also // (briefly) track handlers for requests which are sent concurrently with stopping even though the underlying connection is // now closed. We complete all these outstanding handlers here: - for (final Transport.ResponseContext holderToNotify : responseHandlers.prune(h -> true)) { + for (final Transport.ResponseContext holderToNotify : responseHandlers.prune(Predicates.always())) { try { final TransportResponseHandler handler = holderToNotify.handler(); final var targetNode = holderToNotify.connection().getNode(); @@ -499,7 +500,7 @@ public void connectToNode( public ConnectionManager.ConnectionValidator connectionValidator(DiscoveryNode node) { return (newConnection, actualProfile, listener) -> { // We don't validate cluster names to allow for CCS connections. - handshake(newConnection, actualProfile.getHandshakeTimeout(), cn -> true, listener.map(resp -> { + handshake(newConnection, actualProfile.getHandshakeTimeout(), Predicates.always(), listener.map(resp -> { final DiscoveryNode remote = resp.discoveryNode; if (node.equals(remote) == false) { throw new ConnectTransportException(node, "handshake failed. unexpected remote node " + remote); diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/settings/put/UpdateSettingsRequestTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/settings/put/UpdateSettingsRequestTests.java index 48ab2b0802616..36e347204d1cc 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/settings/put/UpdateSettingsRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/settings/put/UpdateSettingsRequestTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.core.Predicates; import org.elasticsearch.test.AbstractXContentTestCase; import org.elasticsearch.test.XContentTestUtils; import org.elasticsearch.xcontent.ToXContent; @@ -110,7 +111,7 @@ protected Predicate getRandomFieldsExcludeFilter() { if (enclosedSettings) { return field -> field.startsWith("settings"); } - return field -> true; + return Predicates.always(); } @Override diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java index 07ccf0e8f34e7..1e35a40dedc17 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/MetadataTests.java @@ -31,6 +31,7 @@ import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexNotFoundException; @@ -783,7 +784,7 @@ public void testFindMappingsWithFilters() throws IOException { && field.equals("address.location") == false; } if (index.equals("index2")) { - return field -> false; + return Predicates.never(); } return MapperPlugin.NOOP_FIELD_PREDICATE; }, Metadata.ON_NEXT_INDEX_FIND_MAPPINGS_NOOP); diff --git a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/AllocationDecidersTests.java b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/AllocationDecidersTests.java index ac3984a2ded21..4fe07756a1d6b 100644 --- a/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/AllocationDecidersTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/routing/allocation/decider/AllocationDecidersTests.java @@ -22,6 +22,7 @@ import org.elasticsearch.cluster.routing.TestShardRouting; import org.elasticsearch.cluster.routing.UnassignedInfo; import org.elasticsearch.cluster.routing.allocation.RoutingAllocation; +import org.elasticsearch.core.Predicates; import org.elasticsearch.index.Index; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.shard.ShardId; @@ -113,7 +114,7 @@ private static List generateDecisions(Decision mandatory, Supplier decisions) { - return collectToMultiDecision(decisions, ignored -> true); + return collectToMultiDecision(decisions, Predicates.always()); } private static Decision.Multi collectToMultiDecision(List decisions, Predicate filter) { diff --git a/server/src/test/java/org/elasticsearch/index/reindex/ReindexRequestTests.java b/server/src/test/java/org/elasticsearch/index/reindex/ReindexRequestTests.java index 65c060aa9005a..c8cce9a9910e7 100644 --- a/server/src/test/java/org/elasticsearch/index/reindex/ReindexRequestTests.java +++ b/server/src/test/java/org/elasticsearch/index/reindex/ReindexRequestTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Predicates; import org.elasticsearch.index.query.TermQueryBuilder; import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.slice.SliceBuilder; @@ -115,7 +116,7 @@ protected ReindexRequest createTestInstance() { @Override protected ReindexRequest doParseInstance(XContentParser parser) throws IOException { - return ReindexRequest.fromXContent(parser, nf -> false); + return ReindexRequest.fromXContent(parser, Predicates.never()); } @Override @@ -403,7 +404,7 @@ private ReindexRequest parseRequestWithSourceIndices(Object sourceIndices) throw request = BytesReference.bytes(b); } try (XContentParser p = createParser(JsonXContent.jsonXContent, request)) { - return ReindexRequest.fromXContent(p, nf -> false); + return ReindexRequest.fromXContent(p, Predicates.never()); } } } diff --git a/test/framework/src/main/java/org/elasticsearch/index/mapper/MockFieldFilterPlugin.java b/test/framework/src/main/java/org/elasticsearch/index/mapper/MockFieldFilterPlugin.java index 21c6b50809ea9..16cb0b4656fcf 100644 --- a/test/framework/src/main/java/org/elasticsearch/index/mapper/MockFieldFilterPlugin.java +++ b/test/framework/src/main/java/org/elasticsearch/index/mapper/MockFieldFilterPlugin.java @@ -8,6 +8,7 @@ package org.elasticsearch.index.mapper; +import org.elasticsearch.core.Predicates; import org.elasticsearch.plugins.MapperPlugin; import org.elasticsearch.plugins.Plugin; @@ -19,6 +20,6 @@ public class MockFieldFilterPlugin extends Plugin implements MapperPlugin { @Override public Function> getFieldFilter() { // this filter doesn't filter any field out, but it's used to exercise the code path executed when the filter is not no-op - return index -> field -> true; + return index -> Predicates.always(); } } diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractSerializationTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractSerializationTestCase.java index 238f523872f83..922f2ba74dcf2 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractSerializationTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractSerializationTestCase.java @@ -10,6 +10,7 @@ import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.rest.action.search.RestSearchAction; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; @@ -126,7 +127,7 @@ protected boolean supportsUnknownFields() { * Returns a predicate that given the field name indicates whether the field has to be excluded from random fields insertion or not */ protected Predicate getRandomFieldsExcludeFilter() { - return field -> false; + return Predicates.never(); } /** diff --git a/test/framework/src/main/java/org/elasticsearch/test/AbstractXContentTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/AbstractXContentTestCase.java index 4df1e745f3bf4..848ec3c2f1738 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/AbstractXContentTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/AbstractXContentTestCase.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.xcontent.ChunkedToXContent; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.CheckedFunction; +import org.elasticsearch.core.Predicates; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContent; import org.elasticsearch.xcontent.XContentBuilder; @@ -326,7 +327,7 @@ protected boolean assertToXContentEquivalence() { * Returns a predicate that given the field name indicates whether the field has to be excluded from random fields insertion or not */ protected Predicate getRandomFieldsExcludeFilter() { - return field -> false; + return Predicates.never(); } /** diff --git a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java index 38c38e719138e..16320b3b26301 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java @@ -64,6 +64,7 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.IOUtils; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.TimeValue; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; @@ -650,7 +651,7 @@ private NodeAndClient getOrBuildRandomNode() { } private NodeAndClient getRandomNodeAndClient() { - return getRandomNodeAndClient(nc -> true); + return getRandomNodeAndClient(Predicates.always()); } private synchronized NodeAndClient getRandomNodeAndClient(Predicate predicate) { @@ -1621,7 +1622,7 @@ private synchronized T getInstance(Class clazz, Predicate * Returns a reference to a random nodes instances of the given class >T< */ public T getInstance(Class clazz) { - return getInstance(clazz, nc -> true); + return getInstance(clazz, Predicates.always()); } private static T getInstanceFromNode(Class clazz, Node node) { @@ -1990,7 +1991,7 @@ public String getMasterName(@Nullable String viaNode) { * @return the name of a random node in a cluster */ public String getRandomNodeName() { - return getNodeNameThat(ignored -> true); + return getNodeNameThat(Predicates.always()); } /** diff --git a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/VersionRange.java b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/VersionRange.java index 20b9708c5ac25..ab5377532bbbc 100644 --- a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/VersionRange.java +++ b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/VersionRange.java @@ -9,6 +9,7 @@ import org.elasticsearch.Build; import org.elasticsearch.Version; +import org.elasticsearch.core.Predicates; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.test.rest.ESRestTestCase; @@ -23,9 +24,9 @@ class VersionRange { private VersionRange() {} - static final Predicate> NEVER = v -> false; + static final Predicate> NEVER = Predicates.never(); - static final Predicate> ALWAYS = v -> true; + static final Predicate> ALWAYS = Predicates.always(); static final Predicate> CURRENT = versions -> versions.size() == 1 && versions.contains(Build.current().version()); diff --git a/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/storage/ReactiveStorageDeciderService.java b/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/storage/ReactiveStorageDeciderService.java index ffa3a7308da90..2379e5f8e9380 100644 --- a/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/storage/ReactiveStorageDeciderService.java +++ b/x-pack/plugin/autoscaling/src/main/java/org/elasticsearch/xpack/autoscaling/storage/ReactiveStorageDeciderService.java @@ -48,6 +48,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeUnit; import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.Tuple; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; @@ -199,11 +200,11 @@ static String message(long unassignedBytes, long assignedBytes) { } static boolean isDiskOnlyNoDecision(Decision decision) { - return singleNoDecision(decision, single -> true).map(DiskThresholdDecider.NAME::equals).orElse(false); + return singleNoDecision(decision, Predicates.always()).map(DiskThresholdDecider.NAME::equals).orElse(false); } static boolean isResizeOnlyNoDecision(Decision decision) { - return singleNoDecision(decision, single -> true).map(ResizeAllocationDecider.NAME::equals).orElse(false); + return singleNoDecision(decision, Predicates.always()).map(ResizeAllocationDecider.NAME::equals).orElse(false); } static boolean isFilterTierOnlyDecision(Decision decision, IndexMetadata indexMetadata) { diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowInfoAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowInfoAction.java index 46c44c9b2392b..a66a79a0f7d76 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowInfoAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowInfoAction.java @@ -18,6 +18,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.core.Predicates; import org.elasticsearch.persistent.PersistentTasksCustomMetadata; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; @@ -89,7 +90,7 @@ static List getFollowInfos(List concreteFollowerIndices, C if (ccrCustomData != null) { Optional result; if (persistentTasks != null) { - result = persistentTasks.findTasks(ShardFollowTask.NAME, task -> true) + result = persistentTasks.findTasks(ShardFollowTask.NAME, Predicates.always()) .stream() .map(task -> (ShardFollowTask) task.getParams()) .filter(shardFollowTask -> index.equals(shardFollowTask.getFollowShardId().getIndexName())) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlTasks.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlTasks.java index 6209ead0cc6a1..6281f656954e5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlTasks.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MlTasks.java @@ -9,6 +9,7 @@ import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.TimeValue; import org.elasticsearch.persistent.PersistentTasksClusterService; import org.elasticsearch.persistent.PersistentTasksCustomMetadata; @@ -308,7 +309,7 @@ public static Collection> openJo return Collections.emptyList(); } - return tasks.findTasks(JOB_TASK_NAME, task -> true); + return tasks.findTasks(JOB_TASK_NAME, Predicates.always()); } public static Collection> datafeedTasksOnNode( @@ -360,7 +361,7 @@ public static Collection> snapsh return Collections.emptyList(); } - return tasks.findTasks(JOB_SNAPSHOT_UPGRADE_TASK_NAME, task -> true); + return tasks.findTasks(JOB_SNAPSHOT_UPGRADE_TASK_NAME, Predicates.always()); } public static Collection> snapshotUpgradeTasksOnNode( @@ -439,7 +440,7 @@ public static Set startedDatafeedIds(@Nullable PersistentTasksCustomMeta return Collections.emptySet(); } - return tasks.findTasks(DATAFEED_TASK_NAME, task -> true) + return tasks.findTasks(DATAFEED_TASK_NAME, Predicates.always()) .stream() .map(t -> t.getId().substring(DATAFEED_TASK_ID_PREFIX.length())) .collect(Collectors.toSet()); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UserRoleMapper.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UserRoleMapper.java index 5addca91902cd..96fb7ff4e6f41 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UserRoleMapper.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/UserRoleMapper.java @@ -15,6 +15,7 @@ import org.apache.lucene.util.automaton.CharacterRunAutomaton; import org.elasticsearch.action.ActionListener; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.xpack.core.security.authc.RealmConfig; import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.ExpressionModel; import org.elasticsearch.xpack.core.security.authc.support.mapper.expressiondsl.FieldExpression; @@ -87,7 +88,7 @@ public ExpressionModel asModel() { groups, groups.stream().>map(g -> new DistinguishedNamePredicate(g, dnNormalizer)) .reduce(Predicate::or) - .orElse(fieldValue -> false) + .orElse(Predicates.never()) ); metadata.keySet().forEach(k -> model.defineField("metadata." + k, metadata.get(k))); model.defineField("realm.name", realm.name()); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/expressiondsl/ExpressionModel.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/expressiondsl/ExpressionModel.java index 3251c54945335..9d25e6830bbbd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/expressiondsl/ExpressionModel.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/mapper/expressiondsl/ExpressionModel.java @@ -9,6 +9,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.common.Numbers; +import org.elasticsearch.core.Predicates; import java.util.Collection; import java.util.Collections; @@ -100,7 +101,7 @@ static Predicate buildPredicate(Object object) { return ((Collection) object).stream() .map(element -> buildPredicate(element)) .reduce((a, b) -> a.or(b)) - .orElse(fieldValue -> false); + .orElse(Predicates.never()); } throw new IllegalArgumentException("Unsupported value type " + object.getClass()); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/restriction/WorkflowsRestriction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/restriction/WorkflowsRestriction.java index f1d9d694304e5..811c6b36d4f7e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/restriction/WorkflowsRestriction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/restriction/WorkflowsRestriction.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.core.security.authz.restriction; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import java.util.Set; import java.util.function.Predicate; @@ -26,10 +27,10 @@ public WorkflowsRestriction(Set names) { this.names = names; if (names == null) { // No restriction, all workflows are allowed - this.predicate = name -> true; + this.predicate = Predicates.always(); } else if (names.isEmpty()) { // Empty restriction, no workflow is allowed - this.predicate = name -> false; + this.predicate = Predicates.never(); } else { this.predicate = name -> { if (name == null) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java index 5d7a4b279298c..f601aa144aa00 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java @@ -17,6 +17,7 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.TimeValue; import java.util.ArrayList; @@ -312,6 +313,11 @@ static int getMaxDeterminizedStates() { } private static Predicate predicate(Automaton automaton, final String toString) { + if (automaton == MATCH_ALL) { + return Predicates.always(); + } else if (automaton == EMPTY) { + return Predicates.never(); + } CharacterRunAutomaton runAutomaton = new CharacterRunAutomaton(automaton, maxDeterminizedStates); return new Predicate() { @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/StringMatcher.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/StringMatcher.java index 235fb3635bac6..ede11fe157487 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/StringMatcher.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/StringMatcher.java @@ -12,6 +12,7 @@ import org.apache.lucene.util.automaton.TooComplexToDeterminizeException; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.Strings; +import org.elasticsearch.core.Predicates; import java.util.ArrayList; import java.util.Collection; @@ -34,9 +35,7 @@ */ public class StringMatcher implements Predicate { - private static final StringMatcher MATCH_NOTHING = new StringMatcher("(empty)", s -> false); - - protected static final Predicate ALWAYS_TRUE_PREDICATE = s -> true; + private static final StringMatcher MATCH_NOTHING = new StringMatcher("(empty)", Predicates.never()); private final String description; private final Predicate predicate; @@ -70,7 +69,7 @@ public boolean test(String s) { } public boolean isTotal() { - return predicate == ALWAYS_TRUE_PREDICATE; + return predicate == Predicates.always(); } // For testing @@ -130,7 +129,7 @@ public StringMatcher build() { final String description = describe(allText); if (nonExactMatch.contains("*")) { - return new StringMatcher(description, ALWAYS_TRUE_PREDICATE); + return new StringMatcher(description, Predicates.always()); } if (exactMatch.isEmpty()) { return new StringMatcher(description, buildAutomataPredicate(nonExactMatch)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/StringMatcherTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/StringMatcherTests.java index 1582cf3404bdc..2e31f760f6db2 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/StringMatcherTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/support/StringMatcherTests.java @@ -9,6 +9,7 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.Strings; +import org.elasticsearch.core.Predicates; import org.elasticsearch.test.ESTestCase; import java.util.List; @@ -49,7 +50,7 @@ public void testMatchAllWildcard() throws Exception { assertMatch(matcher, randomAlphaOfLengthBetween(i, 20)); } - assertThat(matcher.getPredicate(), sameInstance(StringMatcher.ALWAYS_TRUE_PREDICATE)); + assertThat(matcher.getPredicate(), sameInstance(Predicates.always())); } public void testSingleWildcard() throws Exception { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MlDailyMaintenanceService.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MlDailyMaintenanceService.java index 8c46f7229c655..71469fccc0032 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MlDailyMaintenanceService.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MlDailyMaintenanceService.java @@ -22,6 +22,7 @@ import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.Releasable; import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; @@ -288,8 +289,8 @@ public void triggerDeleteJobsInStateDeletingWithoutDeletionTask(ActionListener> chainTaskExecutor = new TypedChainTaskExecutor<>( EsExecutors.DIRECT_EXECUTOR_SERVICE, - unused -> true, - unused -> true + Predicates.always(), + Predicates.always() ); for (String jobId : jobsInStateDeletingWithoutDeletionTask) { DeleteJobAction.Request request = new DeleteJobAction.Request(jobId); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportEvaluateDataFrameAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportEvaluateDataFrameAction.java index 61db7f683f0f3..92c9909441b14 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportEvaluateDataFrameAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportEvaluateDataFrameAction.java @@ -17,6 +17,7 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.core.Predicates; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.tasks.Task; import org.elasticsearch.tasks.TaskId; @@ -128,7 +129,7 @@ private static final class EvaluationExecutor extends TypedChainTaskExecutor true, unused -> true); + super(threadPool.generic(), Predicates.always(), Predicates.always()); this.client = client; this.parameters = parameters; this.request = request; 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 6a8dca8e2776b..d54cac9dca496 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 @@ -16,6 +16,7 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.util.concurrent.AtomicArray; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.Tuple; import org.elasticsearch.inference.InferenceResults; import org.elasticsearch.license.License; @@ -175,9 +176,9 @@ private void getModelAndInfer( TypedChainTaskExecutor typedChainTaskExecutor = new TypedChainTaskExecutor<>( EsExecutors.DIRECT_EXECUTOR_SERVICE, // run through all tasks - r -> true, + Predicates.always(), // Always fail immediately and return an error - ex -> true + Predicates.always() ); request.getObjectsToInfer().forEach(stringObjectMap -> typedChainTaskExecutor.add(chainedTask -> { if (task.isCancelled()) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportSetUpgradeModeAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportSetUpgradeModeAction.java index 07b556cf9a989..4f4eee6e5c597 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportSetUpgradeModeAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportSetUpgradeModeAction.java @@ -28,6 +28,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.persistent.PersistentTasksClusterService; import org.elasticsearch.persistent.PersistentTasksCustomMetadata; @@ -300,7 +301,7 @@ private void unassignPersistentTasks( TypedChainTaskExecutor> chainTaskExecutor = new TypedChainTaskExecutor<>( executor, - r -> true, + Predicates.always(), // Another process could modify tasks and thus we cannot find them via the allocation_id and name // If the task was removed from the node, all is well // We handle the case of allocation_id changing later in this transport class by timing out waiting for task completion @@ -330,8 +331,8 @@ private void isolateDatafeeds( logger.info("Isolating datafeeds: " + datafeedsToIsolate.toString()); TypedChainTaskExecutor isolateDatafeedsExecutor = new TypedChainTaskExecutor<>( executor, - r -> true, - ex -> true + Predicates.always(), + Predicates.always() ); datafeedsToIsolate.forEach(datafeedId -> { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/autoscaling/MlMemoryAutoscalingDecider.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/autoscaling/MlMemoryAutoscalingDecider.java index 4ff7e66d296d0..dfe0e557f749d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/autoscaling/MlMemoryAutoscalingDecider.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/autoscaling/MlMemoryAutoscalingDecider.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.xcontent.XContentElasticsearchExtension; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.TimeValue; import org.elasticsearch.core.Tuple; import org.elasticsearch.logging.LogManager; @@ -913,7 +914,7 @@ private static Collection true) + return tasksCustomMetadata.findTasks(MlTasks.DATAFEED_TASK_NAME, Predicates.always()) .stream() .map(p -> (PersistentTasksCustomMetadata.PersistentTask) p) .toList(); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/VoidChainTaskExecutor.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/VoidChainTaskExecutor.java index d5d7767a7e7a1..f7c46222d4471 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/VoidChainTaskExecutor.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/utils/VoidChainTaskExecutor.java @@ -6,6 +6,8 @@ */ package org.elasticsearch.xpack.ml.utils; +import org.elasticsearch.core.Predicates; + import java.util.concurrent.ExecutorService; import java.util.function.Predicate; @@ -16,7 +18,7 @@ public class VoidChainTaskExecutor extends TypedChainTaskExecutor { public VoidChainTaskExecutor(ExecutorService executorService, boolean shortCircuit) { - this(executorService, (a) -> true, (e) -> shortCircuit); + this(executorService, Predicates.always(), shortCircuit ? Predicates.always() : Predicates.never()); } VoidChainTaskExecutor( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountAction.java index f8a4a8a449f83..372a550eedbc9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountAction.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.util.concurrent.EsExecutors; +import org.elasticsearch.core.Predicates; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountAction; @@ -38,7 +39,7 @@ public TransportGetServiceAccountAction(TransportService transportService, Actio @Override protected void doExecute(Task task, GetServiceAccountRequest request, ActionListener listener) { - Predicate filter = v -> true; + Predicate filter = Predicates.always(); if (request.getNamespace() != null) { filter = filter.and(v -> v.id().namespace().equals(request.getNamespace())); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java index 87c372f561757..01104806c4a1c 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java @@ -30,6 +30,7 @@ import org.elasticsearch.common.util.Maps; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.TimeValue; import org.elasticsearch.http.HttpPreRequest; import org.elasticsearch.node.Node; @@ -1908,7 +1909,7 @@ Predicate ignorePredicate() { } private static Predicate buildIgnorePredicate(Map policyMap) { - return policyMap.values().stream().map(EventFilterPolicy::ignorePredicate).reduce(x -> false, (x, y) -> x.or(y)); + return policyMap.values().stream().map(EventFilterPolicy::ignorePredicate).reduce(Predicates.never(), Predicate::or); } @Override diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java index 51adcab5c3c13..14ca1663e16a5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.cli.EnvironmentAwareCommand; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Predicates; import org.elasticsearch.env.Environment; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.authc.support.Hasher; @@ -132,7 +133,7 @@ public void execute(Terminal terminal, OptionSet options, Environment env, Proce + "]" ); } - Predicate filter = k -> true; + Predicate filter = Predicates.always(); if (args.size() == 1) { final String principal = args.get(0); if (false == ServiceAccountService.isServiceAccountPrincipal(principal)) { diff --git a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformTask.java b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformTask.java index b0435a08a4187..8a78be8417020 100644 --- a/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformTask.java +++ b/x-pack/plugin/transform/src/main/java/org/elasticsearch/xpack/transform/transforms/TransformTask.java @@ -17,6 +17,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.common.Strings; import org.elasticsearch.common.regex.Regex; +import org.elasticsearch.core.Predicates; import org.elasticsearch.core.TimeValue; import org.elasticsearch.persistent.AllocatedPersistentTask; import org.elasticsearch.persistent.PersistentTasksCustomMetadata; @@ -607,7 +608,7 @@ public static PersistentTask getTransformTask(String transformId, ClusterStat } public static Collection> findAllTransformTasks(ClusterState clusterState) { - return findTransformTasks(task -> true, clusterState); + return findTransformTasks(Predicates.always(), clusterState); } public static Collection> findTransformTasks(Set transformIds, ClusterState clusterState) { @@ -616,7 +617,7 @@ public static Collection> findTransformTasks(Set trans public static Collection> findTransformTasks(String transformIdPattern, ClusterState clusterState) { Predicate> taskMatcher = transformIdPattern == null - || Strings.isAllOrWildcard(transformIdPattern) ? t -> true : t -> { + || Strings.isAllOrWildcard(transformIdPattern) ? Predicates.always() : t -> { TransformTaskParams transformParams = (TransformTaskParams) t.getParams(); return Regex.simpleMatch(transformIdPattern, transformParams.getId()); }; From c57d96a853de9674254310a38e64fdc475ddc65f Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Mon, 4 Mar 2024 08:16:03 -0500 Subject: [PATCH 225/250] Test mute for #105839 (#105902) mute for: https://github.com/elastic/elasticsearch/issues/105839 --- .../search/aggregations/bucket/RandomSamplerIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/RandomSamplerIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/RandomSamplerIT.java index 53075e31cd6f9..c9a6cfaf754c6 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/RandomSamplerIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/aggregations/bucket/RandomSamplerIT.java @@ -85,6 +85,7 @@ public void setupSuiteScopeCluster() throws Exception { ensureSearchable(); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105839") public void testRandomSamplerConsistentSeed() { double[] sampleMonotonicValue = new double[1]; double[] sampleNumericValue = new double[1]; From fdfc08a257388a54a435678696237ccbc77be504 Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Mon, 4 Mar 2024 15:31:33 +0100 Subject: [PATCH 226/250] Grow buckets on GlobalAggregator and RandomSamplerAggregator eagerly (#105762) --- .../search/aggregations/bucket/global/GlobalAggregator.java | 3 ++- .../bucket/sampler/random/RandomSamplerAggregator.java | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/global/GlobalAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/global/GlobalAggregator.java index 3beec89853b76..ce3031d4cddf8 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/global/GlobalAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/global/GlobalAggregator.java @@ -43,10 +43,11 @@ public LeafBucketCollector getLeafCollector(AggregationExecutionContext aggCtx, if (scorer == null) { return LeafBucketCollector.NO_OP_COLLECTOR; } + grow(1); scorer.score(new LeafCollector() { @Override public void collect(int doc) throws IOException { - collectBucket(sub, doc, 0); + collectExistingBucket(sub, doc, 0); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregator.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregator.java index a279b8270cd57..276e0bbf300d2 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregator.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/sampler/random/RandomSamplerAggregator.java @@ -101,10 +101,11 @@ protected LeafBucketCollector getLeafCollector(AggregationExecutionContext aggCt } // No sampling is being done, collect all docs if (probability >= 1.0) { + grow(1); return new LeafBucketCollector() { @Override public void collect(int doc, long owningBucketOrd) throws IOException { - collectBucket(sub, doc, 0); + collectExistingBucket(sub, doc, 0); } }; } @@ -117,11 +118,12 @@ public void collect(int doc, long owningBucketOrd) throws IOException { final DocIdSetIterator docIt = scorer.iterator(); final Bits liveDocs = aggCtx.getLeafReaderContext().reader().getLiveDocs(); try { + grow(1); // Iterate every document provided by the scorer iterator for (int docId = docIt.nextDoc(); docId != DocIdSetIterator.NO_MORE_DOCS; docId = docIt.nextDoc()) { // If liveDocs is null, that means that every doc is a live doc, no need to check if it has been deleted or not if (liveDocs == null || liveDocs.get(docIt.docID())) { - collectBucket(sub, docIt.docID(), 0); + collectExistingBucket(sub, docIt.docID(), 0); } } // This collector could throw `CollectionTerminatedException` if the last leaf collector has stopped collecting From 0732628eaa91c183b3b5851eeff68d7394100c8e Mon Sep 17 00:00:00 2001 From: David Turner Date: Mon, 4 Mar 2024 14:37:49 +0000 Subject: [PATCH 227/250] Use constant predicate in `buildAfterPredicate` (#105905) Relates #105881 --- .../cluster/snapshots/get/TransportGetSnapshotsAction.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java index f7b5fec8a2dd5..ce3446317400d 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/get/TransportGetSnapshotsAction.java @@ -26,6 +26,7 @@ import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.common.util.concurrent.ListenableFuture; import org.elasticsearch.core.Nullable; +import org.elasticsearch.core.Predicates; import org.elasticsearch.repositories.IndexId; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.repositories.Repository; @@ -545,8 +546,7 @@ private Comparator buildComparator() { private Predicate buildAfterPredicate() { if (after == null) { - // TODO use constant when https://github.com/elastic/elasticsearch/pull/105881 merged - return snapshotInfo -> true; + return Predicates.always(); } assert offset == 0 : "can't combine after and offset but saw [" + after + "] and offset [" + offset + "]"; From 6ae9dbfda7d71ae3f1bd2bddf9334d37b3294632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20Zolt=C3=A1n=20Szab=C3=B3?= Date: Mon, 4 Mar 2024 16:43:41 +0100 Subject: [PATCH 228/250] [DOCS] Adds cohere service example to the inference API tutorial (#105904) Co-authored-by: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> --- .../semantic-search-inference.asciidoc | 205 ++---------------- .../infer-api-ingest-pipeline-widget.asciidoc | 39 ++++ .../infer-api-ingest-pipeline.asciidoc | 63 ++++++ .../infer-api-mapping-widget.asciidoc | 39 ++++ .../inference-api/infer-api-mapping.asciidoc | 71 ++++++ .../infer-api-reindex-widget.asciidoc | 39 ++++ .../inference-api/infer-api-reindex.asciidoc | 55 +++++ .../infer-api-requirements-widget.asciidoc | 39 ++++ .../infer-api-requirements.asciidoc | 14 ++ .../infer-api-search-widget.asciidoc | 39 ++++ .../inference-api/infer-api-search.asciidoc | 139 ++++++++++++ .../infer-api-task-widget.asciidoc | 39 ++++ .../inference-api/infer-api-task.asciidoc | 56 +++++ 13 files changed, 654 insertions(+), 183 deletions(-) create mode 100644 docs/reference/tab-widgets/inference-api/infer-api-ingest-pipeline-widget.asciidoc create mode 100644 docs/reference/tab-widgets/inference-api/infer-api-ingest-pipeline.asciidoc create mode 100644 docs/reference/tab-widgets/inference-api/infer-api-mapping-widget.asciidoc create mode 100644 docs/reference/tab-widgets/inference-api/infer-api-mapping.asciidoc create mode 100644 docs/reference/tab-widgets/inference-api/infer-api-reindex-widget.asciidoc create mode 100644 docs/reference/tab-widgets/inference-api/infer-api-reindex.asciidoc create mode 100644 docs/reference/tab-widgets/inference-api/infer-api-requirements-widget.asciidoc create mode 100644 docs/reference/tab-widgets/inference-api/infer-api-requirements.asciidoc create mode 100644 docs/reference/tab-widgets/inference-api/infer-api-search-widget.asciidoc create mode 100644 docs/reference/tab-widgets/inference-api/infer-api-search.asciidoc create mode 100644 docs/reference/tab-widgets/inference-api/infer-api-task-widget.asciidoc create mode 100644 docs/reference/tab-widgets/inference-api/infer-api-task.asciidoc 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 249fddce9c416..b9bb36b21ea12 100644 --- a/docs/reference/search/search-your-data/semantic-search-inference.asciidoc +++ b/docs/reference/search/search-your-data/semantic-search-inference.asciidoc @@ -4,18 +4,21 @@ Semantic search with the {infer} API ++++ -The instructions in this tutorial shows you how to use the {infer} API with the -Open AI service to perform semantic search on your data. The following example -uses OpenAI's `text-embedding-ada-002` second generation embedding model. You -can use any OpenAI models, they are all supported by the {infer} API. +The instructions in this tutorial shows you how to use the {infer} API with +various services to perform semantic search on your data. The following examples +use Cohere's `embed-english-light-v3.0` model and OpenAI's +`text-embedding-ada-002` second generation embedding model. You can use any +Cohere and OpenAI models, they are all supported by the {infer} API. + +Click the name of the service you want to use on any of the widgets below to +review the corresponding instructions. [discrete] -[[infer-openai-requirements]] +[[infer-service-requirements]] ==== Requirements -An https://openai.com/[OpenAI account] is required to use the {infer} API with -the OpenAI service. +include::{es-repo-dir}/tab-widgets/inference-api/infer-api-requirements-widget.asciidoc[] [discrete] @@ -24,113 +27,30 @@ the OpenAI service. Create the {infer} task by using the <>: -[source,console] ------------------------------------------------------------- -PUT _inference/text_embedding/openai_embeddings <1> -{ - "service": "openai", - "service_settings": { - "api_key": "" <2> - }, - "task_settings": { - "model": "text-embedding-ada-002" <3> - } -} ------------------------------------------------------------- -// TEST[skip:TBD] -<1> The task type is `text_embedding` in the path. -<2> The API key of your OpenAI account. You can find your OpenAI API keys in -your OpenAI account under the -https://platform.openai.com/api-keys[API keys section]. You need to provide -your API key only once. The <> does not return your API -key. -<3> The name of the embedding model to use. You can find the list of OpenAI -embedding models -https://platform.openai.com/docs/guides/embeddings/embedding-models[here]. +include::{es-repo-dir}/tab-widgets/inference-api/infer-api-task-widget.asciidoc[] [discrete] -[[infer-openai-mappings]] +[[infer-service-mappings]] ==== 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 to index the output of the OpenAI model. +field type to index the output of the used model. -[source,console] --------------------------------------------------- -PUT openai-embeddings -{ - "mappings": { - "properties": { - "content_embedding": { <1> - "type": "dense_vector", <2> - "dims": 1536, <3> - "element_type": "float", - "similarity": "dot_product" <4> - }, - "content": { <5> - "type": "text" <6> - } - } - } -} --------------------------------------------------- -<1> The name of the field to contain the generated tokens. It must be refrenced -in the {infer} pipeline configuration in the next step. -<2> The field to contain the tokens is a `dense_vector` field. -<3> The output dimensions of the model. Find this value in the -https://platform.openai.com/docs/guides/embeddings/embedding-models[OpenAI documentation] -of the model you use. -<4> The faster` dot_product` function can be used to calculate similarity -because OpenAI embeddings are normalised to unit length. You can check the -https://platform.openai.com/docs/guides/embeddings/which-distance-function-should-i-use[OpenAI docs] -about which similarity function to use. -<5> The name of the field from which to create the sparse vector representation. -In this example, the name of the field is `content`. It must be referenced in -the {infer} pipeline configuration in the next step. -<6> The field type which is text in this example. +include::{es-repo-dir}/tab-widgets/inference-api/infer-api-mapping-widget.asciidoc[] [discrete] -[[infer-openai-inference-ingest-pipeline]] +[[infer-service-inference-ingest-pipeline]] ==== Create an ingest pipeline with an inference processor Create an <> with an -<> and use the OpenAI model you created -above to infer against the data that is being ingested in the -pipeline. +<> and use the model you created above to +infer against the data that is being ingested in the pipeline. -[source,console] --------------------------------------------------- -PUT _ingest/pipeline/openai_embeddings -{ - "processors": [ - { - "inference": { - "model_id": "openai_embeddings", <1> - "input_output": { <2> - "input_field": "content", - "output_field": "content_embedding" - } - } - } - ] -} --------------------------------------------------- -<1> The name of the inference model you created by using the -<>. -<2> Configuration object that defines the `input_field` for the {infer} process -and the `output_field` that will contain the {infer} results. - -//// -[source,console] ----- -DELETE _ingest/pipeline/openai_embeddings ----- -// TEST[continued] -//// +include::{es-repo-dir}/tab-widgets/inference-api/infer-api-ingest-pipeline-widget.asciidoc[] [discrete] @@ -157,32 +77,10 @@ you can see an index named `test-data` with 182469 documents. [[reindexing-data-infer]] ==== Ingest the data through the {infer} ingest pipeline -Create the embeddings from the text by reindexing the data throught the {infer} -pipeline that uses the OpenAI model as the inference model. +Create the embeddings from the text by reindexing the data through the {infer} +pipeline that uses the chosen model as the inference model. -[source,console] ----- -POST _reindex?wait_for_completion=false -{ - "source": { - "index": "test-data", - "size": 50 <1> - }, - "dest": { - "index": "openai-embeddings", - "pipeline": "openai_embeddings" - } -} ----- -// TEST[skip:TBD] -<1> The default batch size for reindexing is 1000. Reducing `size` to a smaller -number makes the update of the reindexing process quicker which enables you to -follow the progress closely and detect errors early. - -NOTE: The -https://platform.openai.com/account/limits[rate limit of your OpenAI account] -may affect the throughput of the reindexing process. If this happens, change -`size` to `3` or a similar value in magnitude. +include::{es-repo-dir}/tab-widgets/inference-api/infer-api-reindex-widget.asciidoc[] The call returns a task ID to monitor the progress: @@ -214,63 +112,4 @@ provide the query text and the model you have used to create the embeddings. NOTE: If you cancelled the reindexing process, you run the query only a part of the data which affects the quality of your results. -[source,console] --------------------------------------------------- -GET openai-embeddings/_search -{ - "knn": { - "field": "content_embedding", - "query_vector_builder": { - "text_embedding": { - "model_id": "openai_embeddings", - "model_text": "Calculate fuel cost" - } - }, - "k": 10, - "num_candidates": 100 - }, - "_source": [ - "id", - "content" - ] -} --------------------------------------------------- -// TEST[skip:TBD] - -As a result, you receive the top 10 documents that are closest in meaning to the -query from the `openai-embeddings` index sorted by their proximity to the query: - -[source,consol-result] --------------------------------------------------- -"hits": [ - { - "_index": "openai-embeddings", - "_id": "DDd5OowBHxQKHyc3TDSC", - "_score": 0.83704096, - "_source": { - "id": 862114, - "body": "How to calculate fuel cost for a road trip. By Tara Baukus Mello • Bankrate.com. Dear Driving for Dollars, My family is considering taking a long road trip to finish off the end of the summer, but I'm a little worried about gas prices and our overall fuel cost.It doesn't seem easy to calculate since we'll be traveling through many states and we are considering several routes.y family is considering taking a long road trip to finish off the end of the summer, but I'm a little worried about gas prices and our overall fuel cost. It doesn't seem easy to calculate since we'll be traveling through many states and we are considering several routes." - } - }, - { - "_index": "openai-embeddings", - "_id": "ajd5OowBHxQKHyc3TDSC", - "_score": 0.8345704, - "_source": { - "id": 820622, - "body": "Home Heating Calculator. Typically, approximately 50% of the energy consumed in a home annually is for space heating. When deciding on a heating system, many factors will come into play: cost of fuel, installation cost, convenience and life style are all important.This calculator can help you estimate the cost of fuel for different heating appliances.hen deciding on a heating system, many factors will come into play: cost of fuel, installation cost, convenience and life style are all important. This calculator can help you estimate the cost of fuel for different heating appliances." - } - }, - { - "_index": "openai-embeddings", - "_id": "Djd5OowBHxQKHyc3TDSC", - "_score": 0.8327426, - "_source": { - "id": 8202683, - "body": "Fuel is another important cost. This cost will depend on your boat, how far you travel, and how fast you travel. A 33-foot sailboat traveling at 7 knots should be able to travel 300 miles on 50 gallons of diesel fuel.If you are paying $4 per gallon, the trip would cost you $200.Most boats have much larger gas tanks than cars.uel is another important cost. This cost will depend on your boat, how far you travel, and how fast you travel. A 33-foot sailboat traveling at 7 knots should be able to travel 300 miles on 50 gallons of diesel fuel." - } - }, - (...) - ] --------------------------------------------------- -// NOTCONSOLE +include::{es-repo-dir}/tab-widgets/inference-api/infer-api-search-widget.asciidoc[] \ No newline at end of file diff --git a/docs/reference/tab-widgets/inference-api/infer-api-ingest-pipeline-widget.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-ingest-pipeline-widget.asciidoc new file mode 100644 index 0000000000000..44d2f60966caa --- /dev/null +++ b/docs/reference/tab-widgets/inference-api/infer-api-ingest-pipeline-widget.asciidoc @@ -0,0 +1,39 @@ +++++ +
    +
    + + +
    +
    +++++ + +include::infer-api-ingest-pipeline.asciidoc[tag=cohere] + +++++ +
    + +
    +++++ \ No newline at end of file diff --git a/docs/reference/tab-widgets/inference-api/infer-api-ingest-pipeline.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-ingest-pipeline.asciidoc new file mode 100644 index 0000000000000..a5a1910e8f8ef --- /dev/null +++ b/docs/reference/tab-widgets/inference-api/infer-api-ingest-pipeline.asciidoc @@ -0,0 +1,63 @@ +//// + +[source,console] +---- +DELETE _ingest/pipeline/*_embeddings +---- +// TEST +// TEARDOWN + +//// + +// tag::cohere[] + +[source,console] +-------------------------------------------------- +PUT _ingest/pipeline/cohere_embeddings +{ + "processors": [ + { + "inference": { + "model_id": "cohere_embeddings", <1> + "input_output": { <2> + "input_field": "content", + "output_field": "content_embedding" + } + } + } + ] +} +-------------------------------------------------- +<1> The name of the inference configuration you created by using the +<>. +<2> Configuration object that defines the `input_field` for the {infer} process +and the `output_field` that will contain the {infer} results. + +// end::cohere[] + + +// tag::openai[] + +[source,console] +-------------------------------------------------- +PUT _ingest/pipeline/openai_embeddings +{ + "processors": [ + { + "inference": { + "model_id": "openai_embeddings", <1> + "input_output": { <2> + "input_field": "content", + "output_field": "content_embedding" + } + } + } + ] +} +-------------------------------------------------- +<1> The name of the inference configuration you created by using the +<>. +<2> Configuration object that defines the `input_field` for the {infer} process +and the `output_field` that will contain the {infer} results. + +// end::openai[] \ No newline at end of file diff --git a/docs/reference/tab-widgets/inference-api/infer-api-mapping-widget.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-mapping-widget.asciidoc new file mode 100644 index 0000000000000..336c8052c282f --- /dev/null +++ b/docs/reference/tab-widgets/inference-api/infer-api-mapping-widget.asciidoc @@ -0,0 +1,39 @@ +++++ +
    +
    + + +
    +
    +++++ + +include::infer-api-mapping.asciidoc[tag=cohere] + +++++ +
    + +
    +++++ \ No newline at end of file diff --git a/docs/reference/tab-widgets/inference-api/infer-api-mapping.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-mapping.asciidoc new file mode 100644 index 0000000000000..4b70a1b84f45f --- /dev/null +++ b/docs/reference/tab-widgets/inference-api/infer-api-mapping.asciidoc @@ -0,0 +1,71 @@ +// tag::cohere[] + +[source,console] +-------------------------------------------------- +PUT cohere-embeddings +{ + "mappings": { + "properties": { + "content_embedding": { <1> + "type": "dense_vector", <2> + "dims": 384, <3> + "element_type": "float" + }, + "content": { <4> + "type": "text" <5> + } + } + } +} +-------------------------------------------------- +<1> The name of the field to contain the generated tokens. It must be refrenced +in the {infer} pipeline configuration in the next step. +<2> The field to contain the tokens is a `dense_vector` field. +<3> The output dimensions of the model. Find this value in the +https://docs.cohere.com/reference/embed[Cohere documentation] of the model you +use. +<4> The name of the field from which to create the dense vector representation. +In this example, the name of the field is `content`. It must be referenced in +the {infer} pipeline configuration in the next step. +<5> The field type which is text in this example. + +// end::cohere[] + + +// tag::openai[] + +[source,console] +-------------------------------------------------- +PUT openai-embeddings +{ + "mappings": { + "properties": { + "content_embedding": { <1> + "type": "dense_vector", <2> + "dims": 1536, <3> + "element_type": "float", + "similarity": "dot_product" <4> + }, + "content": { <5> + "type": "text" <6> + } + } + } +} +-------------------------------------------------- +<1> The name of the field to contain the generated tokens. It must be refrenced +in the {infer} pipeline configuration in the next step. +<2> The field to contain the tokens is a `dense_vector` field. +<3> The output dimensions of the model. Find this value in the +https://platform.openai.com/docs/guides/embeddings/embedding-models[OpenAI documentation] +of the model you use. +<4> The faster` dot_product` function can be used to calculate similarity +because OpenAI embeddings are normalised to unit length. You can check the +https://platform.openai.com/docs/guides/embeddings/which-distance-function-should-i-use[OpenAI docs] +about which similarity function to use. +<5> The name of the field from which to create the dense vector representation. +In this example, the name of the field is `content`. It must be referenced in +the {infer} pipeline configuration in the next step. +<6> The field type which is text in this example. + +// end::openai[] \ No newline at end of file diff --git a/docs/reference/tab-widgets/inference-api/infer-api-reindex-widget.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-reindex-widget.asciidoc new file mode 100644 index 0000000000000..a73e4d7d76fc1 --- /dev/null +++ b/docs/reference/tab-widgets/inference-api/infer-api-reindex-widget.asciidoc @@ -0,0 +1,39 @@ +++++ +
    +
    + + +
    +
    +++++ + +include::infer-api-reindex.asciidoc[tag=cohere] + +++++ +
    + +
    +++++ \ No newline at end of file diff --git a/docs/reference/tab-widgets/inference-api/infer-api-reindex.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-reindex.asciidoc new file mode 100644 index 0000000000000..92e781f8b5a8a --- /dev/null +++ b/docs/reference/tab-widgets/inference-api/infer-api-reindex.asciidoc @@ -0,0 +1,55 @@ +// tag::cohere[] + +[source,console] +---- +POST _reindex?wait_for_completion=false +{ + "source": { + "index": "test-data", + "size": 50 <1> + }, + "dest": { + "index": "cohere-embeddings", + "pipeline": "cohere_embeddings" + } +} +---- +// TEST[skip:TBD] +<1> The default batch size for reindexing is 1000. Reducing `size` to a smaller +number makes the update of the reindexing process quicker which enables you to +follow the progress closely and detect errors early. + +NOTE: The +https://dashboard.cohere.com/billing[rate limit of your Cohere account] +may affect the throughput of the reindexing process. + +// end::cohere[] + + +// tag::openai[] + +[source,console] +---- +POST _reindex?wait_for_completion=false +{ + "source": { + "index": "test-data", + "size": 50 <1> + }, + "dest": { + "index": "openai-embeddings", + "pipeline": "openai_embeddings" + } +} +---- +// TEST[skip:TBD] +<1> The default batch size for reindexing is 1000. Reducing `size` to a smaller +number makes the update of the reindexing process quicker which enables you to +follow the progress closely and detect errors early. + +NOTE: The +https://platform.openai.com/account/limits[rate limit of your OpenAI account] +may affect the throughput of the reindexing process. If this happens, change +`size` to `3` or a similar value in magnitude. + +// end::openai[] \ No newline at end of file diff --git a/docs/reference/tab-widgets/inference-api/infer-api-requirements-widget.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-requirements-widget.asciidoc new file mode 100644 index 0000000000000..d1b981158c11b --- /dev/null +++ b/docs/reference/tab-widgets/inference-api/infer-api-requirements-widget.asciidoc @@ -0,0 +1,39 @@ +++++ +
    +
    + + +
    +
    +++++ + +include::infer-api-requirements.asciidoc[tag=cohere] + +++++ +
    + +
    +++++ \ No newline at end of file diff --git a/docs/reference/tab-widgets/inference-api/infer-api-requirements.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-requirements.asciidoc new file mode 100644 index 0000000000000..f0bed750b69c9 --- /dev/null +++ b/docs/reference/tab-widgets/inference-api/infer-api-requirements.asciidoc @@ -0,0 +1,14 @@ +// tag::cohere[] + +A https://cohere.com/[Cohere account] is required to use the {infer} API with +the Cohere service. + +// end::cohere[] + + +// tag::openai[] + +An https://openai.com/[OpenAI account] is required to use the {infer} API with +the OpenAI service. + +// end::openai[] \ No newline at end of file diff --git a/docs/reference/tab-widgets/inference-api/infer-api-search-widget.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-search-widget.asciidoc new file mode 100644 index 0000000000000..4433f2da067f1 --- /dev/null +++ b/docs/reference/tab-widgets/inference-api/infer-api-search-widget.asciidoc @@ -0,0 +1,39 @@ +++++ +
    +
    + + +
    +
    +++++ + +include::infer-api-search.asciidoc[tag=cohere] + +++++ +
    + +
    +++++ \ No newline at end of file diff --git a/docs/reference/tab-widgets/inference-api/infer-api-search.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-search.asciidoc new file mode 100644 index 0000000000000..0c71ab7cecbce --- /dev/null +++ b/docs/reference/tab-widgets/inference-api/infer-api-search.asciidoc @@ -0,0 +1,139 @@ +// tag::cohere[] + +[source,console] +-------------------------------------------------- +GET cohere-embeddings/_search +{ + "knn": { + "field": "content_embedding", + "query_vector_builder": { + "text_embedding": { + "model_id": "cohere_embeddings", + "model_text": "Calculate fuel cost" + } + }, + "k": 10, + "num_candidates": 100 + }, + "_source": [ + "id", + "content" + ] +} +-------------------------------------------------- +// TEST[skip:TBD] + +As a result, you receive the top 10 documents that are closest in meaning to the +query from the `cohere-embeddings` index sorted by their proximity to the query: + +[source,consol-result] +-------------------------------------------------- +"hits": [ + { + "_index": "cohere-embeddings", + "_id": "-eFWCY4BECzWLnMZuI78", + "_score": 0.737484, + "_source": { + "id": 1690948, + "content": "Oxygen is supplied to the muscles via red blood cells. Red blood cells carry hemoglobin which oxygen bonds with as the hemoglobin rich blood cells pass through the blood vessels of the lungs.The now oxygen rich blood cells carry that oxygen to the cells that are demanding it, in this case skeletal muscle cells.ther ways in which muscles are supplied with oxygen include: 1 Blood flow from the heart is increased. 2 Blood flow to your muscles in increased. 3 Blood flow from nonessential organs is transported to working muscles." + } + }, + { + "_index": "cohere-embeddings", + "_id": "HuFWCY4BECzWLnMZuI_8", + "_score": 0.7176013, + "_source": { + "id": 1692482, + "content": "The thoracic cavity is separated from the abdominal cavity by the diaphragm. This is a broad flat muscle. (muscular) diaphragm The diaphragm is a muscle that separat…e the thoracic from the abdominal cavity. The pelvis is the lowest part of the abdominal cavity and it has no physical separation from it Diaphragm." + } + }, + { + "_index": "cohere-embeddings", + "_id": "IOFWCY4BECzWLnMZuI_8", + "_score": 0.7154432, + "_source": { + "id": 1692489, + "content": "Muscular Wall Separating the Abdominal and Thoracic Cavities; Thoracic Cavity of a Fetal Pig; In Mammals the Diaphragm Separates the Abdominal Cavity from the" + } + }, + { + "_index": "cohere-embeddings", + "_id": "C-FWCY4BECzWLnMZuI_8", + "_score": 0.695313, + "_source": { + "id": 1691493, + "content": "Burning, aching, tenderness and stiffness are just some descriptors of the discomfort you may feel in the muscles you exercised one to two days ago.For the most part, these sensations you experience after exercise are collectively known as delayed onset muscle soreness.urning, aching, tenderness and stiffness are just some descriptors of the discomfort you may feel in the muscles you exercised one to two days ago." + } + }, + (...) + ] +-------------------------------------------------- +// NOTCONSOLE + +// end::cohere[] + + +// tag::openai[] + +[source,console] +-------------------------------------------------- +GET openai-embeddings/_search +{ + "knn": { + "field": "content_embedding", + "query_vector_builder": { + "text_embedding": { + "model_id": "openai_embeddings", + "model_text": "Calculate fuel cost" + } + }, + "k": 10, + "num_candidates": 100 + }, + "_source": [ + "id", + "content" + ] +} +-------------------------------------------------- +// TEST[skip:TBD] + +As a result, you receive the top 10 documents that are closest in meaning to the +query from the `openai-embeddings` index sorted by their proximity to the query: + +[source,consol-result] +-------------------------------------------------- +"hits": [ + { + "_index": "openai-embeddings", + "_id": "DDd5OowBHxQKHyc3TDSC", + "_score": 0.83704096, + "_source": { + "id": 862114, + "body": "How to calculate fuel cost for a road trip. By Tara Baukus Mello • Bankrate.com. Dear Driving for Dollars, My family is considering taking a long road trip to finish off the end of the summer, but I'm a little worried about gas prices and our overall fuel cost.It doesn't seem easy to calculate since we'll be traveling through many states and we are considering several routes.y family is considering taking a long road trip to finish off the end of the summer, but I'm a little worried about gas prices and our overall fuel cost. It doesn't seem easy to calculate since we'll be traveling through many states and we are considering several routes." + } + }, + { + "_index": "openai-embeddings", + "_id": "ajd5OowBHxQKHyc3TDSC", + "_score": 0.8345704, + "_source": { + "id": 820622, + "body": "Home Heating Calculator. Typically, approximately 50% of the energy consumed in a home annually is for space heating. When deciding on a heating system, many factors will come into play: cost of fuel, installation cost, convenience and life style are all important.This calculator can help you estimate the cost of fuel for different heating appliances.hen deciding on a heating system, many factors will come into play: cost of fuel, installation cost, convenience and life style are all important. This calculator can help you estimate the cost of fuel for different heating appliances." + } + }, + { + "_index": "openai-embeddings", + "_id": "Djd5OowBHxQKHyc3TDSC", + "_score": 0.8327426, + "_source": { + "id": 8202683, + "body": "Fuel is another important cost. This cost will depend on your boat, how far you travel, and how fast you travel. A 33-foot sailboat traveling at 7 knots should be able to travel 300 miles on 50 gallons of diesel fuel.If you are paying $4 per gallon, the trip would cost you $200.Most boats have much larger gas tanks than cars.uel is another important cost. This cost will depend on your boat, how far you travel, and how fast you travel. A 33-foot sailboat traveling at 7 knots should be able to travel 300 miles on 50 gallons of diesel fuel." + } + }, + (...) + ] +-------------------------------------------------- +// NOTCONSOLE + +// end::openai[] \ No newline at end of file diff --git a/docs/reference/tab-widgets/inference-api/infer-api-task-widget.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-task-widget.asciidoc new file mode 100644 index 0000000000000..bc54bf6b14ddf --- /dev/null +++ b/docs/reference/tab-widgets/inference-api/infer-api-task-widget.asciidoc @@ -0,0 +1,39 @@ +++++ +
    +
    + + +
    +
    +++++ + +include::infer-api-task.asciidoc[tag=cohere] + +++++ +
    + +
    +++++ \ No newline at end of file diff --git a/docs/reference/tab-widgets/inference-api/infer-api-task.asciidoc b/docs/reference/tab-widgets/inference-api/infer-api-task.asciidoc new file mode 100644 index 0000000000000..3395fea9cc053 --- /dev/null +++ b/docs/reference/tab-widgets/inference-api/infer-api-task.asciidoc @@ -0,0 +1,56 @@ +// tag::cohere[] + +[source,console] +------------------------------------------------------------ +PUT _inference/text_embedding/cohere_embeddings <1> +{ + "service": "cohere", + "service_settings": { + "api_key": "", <2> + "model_id": "embed-english-light-v3.0", <3> + "embedding_type": "int8" + }, + "task_settings": { + } +} +------------------------------------------------------------ +// TEST[skip:TBD] +<1> The task type is `text_embedding` in the path. +<2> The API key of your Cohere account. You can find your API keys in your +Cohere dashboard under the +https://dashboard.cohere.com/api-keys[API keys section]. You need to provide +your API key only once. The <> does not return your API +key. +<3> The name of the embedding model to use. You can find the list of Cohere +embedding models https://docs.cohere.com/reference/embed[here]. + +// end::cohere[] + + +// tag::openai[] + +[source,console] +------------------------------------------------------------ +PUT _inference/text_embedding/openai_embeddings <1> +{ + "service": "openai", + "service_settings": { + "api_key": "", <2> + "model_id": "text-embedding-ada-002" <3> + }, + "task_settings": { + } +} +------------------------------------------------------------ +// TEST[skip:TBD] +<1> The task type is `text_embedding` in the path. +<2> The API key of your OpenAI account. You can find your OpenAI API keys in +your OpenAI account under the +https://platform.openai.com/api-keys[API keys section]. You need to provide +your API key only once. The <> does not return your API +key. +<3> The name of the embedding model to use. You can find the list of OpenAI +embedding models +https://platform.openai.com/docs/guides/embeddings/embedding-models[here]. + +// end::openai[] \ No newline at end of file From 89786f59c885d289be144903b35b160b77e8689e Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Mon, 4 Mar 2024 11:42:36 -0500 Subject: [PATCH 229/250] Test mute for #105918 (#105919) mute for: https://github.com/elastic/elasticsearch/issues/105918 --- .../xpack/esql/querydsl/query/SingleValueQueryTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/SingleValueQueryTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/SingleValueQueryTests.java index f773904ed8973..1d62bc0b6eaaa 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/SingleValueQueryTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/SingleValueQueryTests.java @@ -137,6 +137,7 @@ public void testNotMatchNone() throws IOException { ); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105918") public void testNotMatchSome() throws IOException { int max = between(1, 100); testCase( From 6f87bd379f653ab34d912d68d123eca7bfae4060 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:23:48 -0500 Subject: [PATCH 230/250] Skipping tests that are failing because of timezone field (#105924) Muting tests failing related to https://github.com/elastic/elasticsearch/issues/105840 --- .../elasticsearch/xpack/sql/qa/jdbc/FetchSizeTestCase.java | 3 +++ .../xpack/sql/qa/jdbc/JdbcErrorsTestCase.java | 2 ++ .../xpack/sql/qa/jdbc/PreparedStatementTestCase.java | 2 ++ .../elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java | 7 ++++++- .../xpack/sql/qa/security/JdbcSecurityIT.java | 1 + 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/FetchSizeTestCase.java b/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/FetchSizeTestCase.java index b8af2ae44623a..ec20cc3c64104 100644 --- a/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/FetchSizeTestCase.java +++ b/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/FetchSizeTestCase.java @@ -89,6 +89,7 @@ public void testScroll() throws SQLException { * Test for {@code SELECT} that is implemented as a scroll query. * In this test we don't retrieve all records and rely on close() to clean the cursor */ + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testIncompleteScroll() throws SQLException { try (Connection c = esJdbc(); Statement s = c.createStatement()) { s.setFetchSize(4); @@ -152,6 +153,7 @@ public void testScrollWithDatetimeAndTimezoneParam() throws IOException, SQLExce /** * Test for {@code SELECT} that is implemented as an aggregation. */ + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testAggregation() throws SQLException { try (Connection c = esJdbc(); Statement s = c.createStatement()) { s.setFetchSize(4); @@ -170,6 +172,7 @@ public void testAggregation() throws SQLException { /** * Test for nested documents. */ + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testNestedDocuments() throws SQLException { try (Connection c = esJdbc(); Statement s = c.createStatement()) { s.setFetchSize(5); diff --git a/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/JdbcErrorsTestCase.java b/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/JdbcErrorsTestCase.java index e962f35be2a94..bd49ef0f6b39d 100644 --- a/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/JdbcErrorsTestCase.java +++ b/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/JdbcErrorsTestCase.java @@ -78,6 +78,7 @@ public void testSelectProjectScoreInAggContext() throws IOException, SQLExceptio } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testSelectOrderByScoreInAggContext() throws IOException, SQLException { index("test", body -> body.field("foo", 1)); try (Connection c = esJdbc()) { @@ -111,6 +112,7 @@ public void testSelectScoreSubField() throws IOException, SQLException { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testHardLimitForSortOnAggregate() throws IOException, SQLException { index("test", body -> body.field("a", 1).field("b", 2)); try (Connection c = esJdbc()) { diff --git a/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/PreparedStatementTestCase.java b/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/PreparedStatementTestCase.java index b2b983803260c..6575ff780ccb8 100644 --- a/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/PreparedStatementTestCase.java +++ b/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/PreparedStatementTestCase.java @@ -301,6 +301,7 @@ public void testWildcardField() throws IOException, SQLException { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testConstantKeywordField() throws IOException, SQLException { String mapping = """ "properties":{"id":{"type":"integer"},"text":{"type":"constant_keyword"}}"""; @@ -368,6 +369,7 @@ public void testTooMayParameters() throws IOException, SQLException { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testStringEscaping() throws SQLException { try (Connection connection = esJdbc()) { try (PreparedStatement statement = connection.prepareStatement("SELECT ?, ?, ?, ?")) { diff --git a/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java b/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java index d99fb9674818c..d8534b963c2d7 100644 --- a/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java +++ b/x-pack/plugin/sql/qa/jdbc/src/main/java/org/elasticsearch/xpack/sql/qa/jdbc/ResultSetTestCase.java @@ -73,7 +73,6 @@ import static org.elasticsearch.common.time.DateUtils.toMilliSeconds; import static org.elasticsearch.xpack.sql.qa.jdbc.JdbcTestUtils.JDBC_DRIVER_VERSION; import static org.elasticsearch.xpack.sql.qa.jdbc.JdbcTestUtils.JDBC_TIMEZONE; -import static org.elasticsearch.xpack.sql.qa.jdbc.JdbcTestUtils.UNSIGNED_LONG_MAX; import static org.elasticsearch.xpack.sql.qa.jdbc.JdbcTestUtils.UNSIGNED_LONG_TYPE_NAME; import static org.elasticsearch.xpack.sql.qa.jdbc.JdbcTestUtils.asDate; import static org.elasticsearch.xpack.sql.qa.jdbc.JdbcTestUtils.asTime; @@ -846,6 +845,7 @@ public void testGettingValidNumbersWithCastingFromUnsignedLong() throws IOExcept } // Double values testing + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testGettingValidDoubleWithoutCasting() throws IOException, SQLException { List doubleTestValues = createTestDataForNumericValueTests(ESTestCase::randomDouble); double random1 = doubleTestValues.get(0); @@ -1158,6 +1158,7 @@ public void testGettingValidBigDecimalFromFloatWithoutCasting() throws IOExcepti ); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testGettingValidBigDecimalFromDoubleWithoutCasting() throws IOException, SQLException { List doubleTestValues = createTestDataForNumericValueTests(ESTestCase::randomDouble); doWithQuery( @@ -1405,6 +1406,7 @@ public void testGettingDateWithoutCalendarWithNanos() throws Exception { }); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testGettingDateWithCalendar() throws Exception { long randomLongDate = randomMillisUpToYear9999(); setupDataForDateTimeTests(randomLongDate); @@ -1434,6 +1436,7 @@ public void testGettingDateWithCalendar() throws Exception { }); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testGettingDateWithCalendarWithNanos() throws Exception { assumeTrue( "Driver version [" + JDBC_DRIVER_VERSION + "] doesn't support DATETIME with nanosecond resolution]", @@ -1597,6 +1600,7 @@ public void testGettingTimestampWithoutCalendar() throws Exception { }); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testGettingTimestampWithoutCalendarWithNanos() throws Exception { assumeTrue( "Driver version [" + JDBC_DRIVER_VERSION + "] doesn't support DATETIME with nanosecond resolution]", @@ -1929,6 +1933,7 @@ public void testGetTimeType() throws IOException, SQLException { }); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testValidGetObjectCalls() throws IOException, SQLException { createIndexWithMapping("test"); updateMappingForNumericValuesTests("test"); diff --git a/x-pack/plugin/sql/qa/server/security/src/test/java/org/elasticsearch/xpack/sql/qa/security/JdbcSecurityIT.java b/x-pack/plugin/sql/qa/server/security/src/test/java/org/elasticsearch/xpack/sql/qa/security/JdbcSecurityIT.java index 0e0c2bc8d78b4..6a46346f627ac 100644 --- a/x-pack/plugin/sql/qa/server/security/src/test/java/org/elasticsearch/xpack/sql/qa/security/JdbcSecurityIT.java +++ b/x-pack/plugin/sql/qa/server/security/src/test/java/org/elasticsearch/xpack/sql/qa/security/JdbcSecurityIT.java @@ -345,6 +345,7 @@ public void testMetadataGetColumnsSingleFieldExcepted() throws Exception { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105840") public void testMetadataGetColumnsDocumentExcluded() throws Exception { createUser("no_3s", "read_test_without_c_3"); From 6ab69e5bc947dd708d4ff9c2c3a1c141c87ec769 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Mon, 4 Mar 2024 13:22:14 -0500 Subject: [PATCH 231/250] ESQL: Don't test field extraction in 8.11 (#105909) We changed field extraction in ES|QL in 8.12 quite a bit so our tests would have to be super complex to test a cluster of mixed versions between 8.11 and `main`. So let's just skip it. Closes #105837 --- .../xpack/esql/qa/mixed/FieldExtractorIT.java | 2 -- .../xpack/esql/qa/rest/FieldExtractorTestCase.java | 9 +++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.java b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.java index bdb10ea65dc1b..8c1e47c29670a 100644 --- a/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.java +++ b/x-pack/plugin/esql/qa/server/mixed-cluster/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/mixed/FieldExtractorIT.java @@ -9,13 +9,11 @@ import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; -import org.apache.lucene.tests.util.LuceneTestCase; import org.elasticsearch.test.TestClustersThreadFilter; import org.elasticsearch.test.cluster.ElasticsearchCluster; import org.elasticsearch.xpack.esql.qa.rest.FieldExtractorTestCase; import org.junit.ClassRule; -@LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105837") @ThreadLeakFilters(filters = TestClustersThreadFilter.class) public class FieldExtractorIT extends FieldExtractorTestCase { @ClassRule diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java index 3f8caa3bdf5d4..39c21651a7e02 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/FieldExtractorTestCase.java @@ -27,6 +27,7 @@ import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xcontent.json.JsonXContent; import org.hamcrest.Matcher; +import org.junit.Before; import java.io.IOException; import java.math.BigDecimal; @@ -57,6 +58,14 @@ public abstract class FieldExtractorTestCase extends ESRestTestCase { private static final Logger logger = LogManager.getLogger(FieldExtractorTestCase.class); + @Before + public void notOld() { + assumeTrue( + "support changed pretty radically in 8.12 so we don't test against 8.11", + getCachedNodesVersions().stream().allMatch(v -> Version.fromString(v).onOrAfter(Version.V_8_12_0)) + ); + } + public void testTextField() throws IOException { textTest().test(randomAlphaOfLength(20)); } From 93fd12d6dbddd11f2c8ff0ed76eeb8c9ce5e32d0 Mon Sep 17 00:00:00 2001 From: Niels Bauman <33722607+nielsbauman@users.noreply.github.com> Date: Mon, 4 Mar 2024 19:34:40 +0100 Subject: [PATCH 232/250] Update health YAML REST test skip version (#105927) The health report API changed names in https://github.com/elastic/elasticsearch/pull/92879, which causes this YAML REST test to fail in versions < 8.7.0. Closes #105923 --- .../resources/rest-api-spec/test/health/10_basic.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/10_basic.yml b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/10_basic.yml index 1dc35c165b4e0..a000a9eac16ad 100644 --- a/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/10_basic.yml +++ b/rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/health/10_basic.yml @@ -1,8 +1,8 @@ --- "cluster health basic test": - skip: - version: "- 8.3.99" - reason: "health was only added in 8.2.0, and master_is_stable in 8.4.0" + version: "- 8.6.99" + reason: "health was added in 8.2.0, master_is_stable in 8.4.0, and REST API updated in 8.7" - do: health_report: { } From ca10472541159f19bfd5e0af3c253d87eb4228cc Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Tue, 5 Mar 2024 07:14:35 +0100 Subject: [PATCH 233/250] Make use of ActionListener#delegateFailureAndWrap in more spots in ML codebase (#105882) Found a bunch more spots where this shortcut helps save both memory and brainpower for thinking through potential leaks. => made use of it and sometimes also inlined a couple local variables for readability. --- .../xpack/ml/datafeed/DatafeedManager.java | 57 ++++++++------- .../extractor/DataExtractorFactory.java | 7 +- .../persistence/DatafeedConfigProvider.java | 28 ++++---- .../dataframe/DataFrameAnalyticsManager.java | 69 +++++++++---------- .../xpack/ml/dataframe/DestinationIndex.java | 8 ++- .../DataFrameDataExtractorFactory.java | 30 ++++---- .../ExtractedFieldsDetectorFactory.java | 7 +- .../DataFrameAnalyticsConfigProvider.java | 60 ++++++++-------- .../DataFrameAnalyticsDeleter.java | 28 ++++---- .../steps/AbstractDataFrameAnalyticsStep.java | 6 +- .../ml/dataframe/steps/AnalysisStep.java | 11 ++- .../xpack/ml/dataframe/steps/FinalStep.java | 17 ++--- .../ml/dataframe/steps/InferenceStep.java | 62 ++++++++--------- .../xpack/ml/job/JobManager.java | 62 ++++++++--------- .../AbstractExpiredJobDataRemover.java | 8 +-- .../job/retention/EmptyStateIndexRemover.java | 42 ++++++----- .../ExpiredModelSnapshotsRemover.java | 6 +- .../job/retention/ExpiredResultsRemover.java | 10 +-- .../task/OpenJobPersistentTasksExecutor.java | 8 +-- .../EmptyStateIndexRemoverTests.java | 3 + .../ExpiredAnnotationsRemoverTests.java | 1 + .../retention/ExpiredResultsRemoverTests.java | 1 + 22 files changed, 261 insertions(+), 270 deletions(-) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedManager.java index ede57764a0813..d44d2181f0ce8 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/DatafeedManager.java @@ -121,9 +121,8 @@ public void putDatafeed( final RoleDescriptor.IndicesPrivileges.Builder indicesPrivilegesBuilder = RoleDescriptor.IndicesPrivileges.builder() .indices(indices); - ActionListener privResponseListener = ActionListener.wrap( - r -> handlePrivsResponse(username, request, r, state, threadPool, listener), - listener::onFailure + ActionListener privResponseListener = listener.delegateFailureAndWrap( + (l, r) -> handlePrivsResponse(username, request, r, state, threadPool, l) ); ActionListener getRollupIndexCapsActionHandler = ActionListener.wrap(response -> { @@ -173,15 +172,14 @@ public void getDatafeeds( request.getDatafeedId(), request.allowNoMatch(), parentTaskId, - ActionListener.wrap( - datafeedBuilders -> listener.onResponse( + listener.delegateFailureAndWrap( + (l, datafeedBuilders) -> l.onResponse( new QueryPage<>( datafeedBuilders.stream().map(DatafeedConfig.Builder::build).collect(Collectors.toList()), datafeedBuilders.size(), DatafeedConfig.RESULTS_FIELD ) - ), - listener::onFailure + ) ) ); } @@ -222,10 +220,7 @@ public void updateDatafeed( request.getUpdate(), headers, jobConfigProvider::validateDatafeedJob, - ActionListener.wrap( - updatedConfig -> listener.onResponse(new PutDatafeedAction.Response(updatedConfig)), - listener::onFailure - ) + listener.delegateFailureAndWrap((l, updatedConfig) -> l.onResponse(new PutDatafeedAction.Response(updatedConfig))) ); }); @@ -254,19 +249,18 @@ public void deleteDatafeed(DeleteDatafeedAction.Request request, ClusterState st String datafeedId = request.getDatafeedId(); - datafeedConfigProvider.getDatafeedConfig(datafeedId, null, ActionListener.wrap(datafeedConfigBuilder -> { + datafeedConfigProvider.getDatafeedConfig(datafeedId, null, listener.delegateFailureAndWrap((delegate, datafeedConfigBuilder) -> { String jobId = datafeedConfigBuilder.build().getJobId(); JobDataDeleter jobDataDeleter = new JobDataDeleter(client, jobId); jobDataDeleter.deleteDatafeedTimingStats( - ActionListener.wrap( - unused1 -> datafeedConfigProvider.deleteDatafeedConfig( + delegate.delegateFailureAndWrap( + (l, unused1) -> datafeedConfigProvider.deleteDatafeedConfig( datafeedId, - ActionListener.wrap(unused2 -> listener.onResponse(AcknowledgedResponse.TRUE), listener::onFailure) - ), - listener::onFailure + l.delegateFailureAndWrap((ll, unused2) -> ll.onResponse(AcknowledgedResponse.TRUE)) + ) ) ); - }, listener::onFailure)); + })); } @@ -316,7 +310,7 @@ private void putDatafeed( CheckedConsumer mappingsUpdated = ok -> datafeedConfigProvider.putDatafeedConfig( request.getDatafeed(), headers, - ActionListener.wrap(response -> listener.onResponse(new PutDatafeedAction.Response(response.v1())), listener::onFailure) + listener.delegateFailureAndWrap((l, response) -> l.onResponse(new PutDatafeedAction.Response(response.v1()))) ); CheckedConsumer validationOk = ok -> { @@ -345,16 +339,19 @@ private void putDatafeed( } private void checkJobDoesNotHaveADatafeed(String jobId, ActionListener listener) { - datafeedConfigProvider.findDatafeedIdsForJobIds(Collections.singletonList(jobId), ActionListener.wrap(datafeedIds -> { - if (datafeedIds.isEmpty()) { - listener.onResponse(Boolean.TRUE); - } else { - listener.onFailure( - ExceptionsHelper.conflictStatusException( - "A datafeed [" + datafeedIds.iterator().next() + "] already exists for job [" + jobId + "]" - ) - ); - } - }, listener::onFailure)); + datafeedConfigProvider.findDatafeedIdsForJobIds( + Collections.singletonList(jobId), + listener.delegateFailureAndWrap((delegate, datafeedIds) -> { + if (datafeedIds.isEmpty()) { + delegate.onResponse(Boolean.TRUE); + } else { + delegate.onFailure( + ExceptionsHelper.conflictStatusException( + "A datafeed [" + datafeedIds.iterator().next() + "] already exists for job [" + jobId + "]" + ) + ); + } + }) + ); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactory.java index be2c8dd871a9b..bcdf5e83cc5ca 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/extractor/DataExtractorFactory.java @@ -59,13 +59,12 @@ static void create( ) { final boolean hasAggs = datafeed.hasAggregations(); final boolean isComposite = hasAggs && datafeed.hasCompositeAgg(xContentRegistry); - ActionListener factoryHandler = ActionListener.wrap( - factory -> listener.onResponse( + ActionListener factoryHandler = listener.delegateFailureAndWrap( + (l, factory) -> l.onResponse( datafeed.getChunkingConfig().isEnabled() ? new ChunkedDataExtractorFactory(datafeed, job, xContentRegistry, factory) : factory - ), - listener::onFailure + ) ); ActionListener getRollupIndexCapsActionHandler = ActionListener.wrap(response -> { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/persistence/DatafeedConfigProvider.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/persistence/DatafeedConfigProvider.java index e226056217351..fbabc9903c4cc 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/persistence/DatafeedConfigProvider.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/persistence/DatafeedConfigProvider.java @@ -223,7 +223,7 @@ public void findDatafeedIdsForJobIds(Collection jobIds, ActionListenerwrap(response -> { + listener.delegateFailureAndWrap((delegate, response) -> { Set datafeedIds = new HashSet<>(); // There cannot be more than one datafeed per job assert response.getHits().getTotalHits().value <= jobIds.size(); @@ -233,8 +233,8 @@ public void findDatafeedIdsForJobIds(Collection jobIds, ActionListenerwrap(response -> { + listener.delegateFailureAndWrap((delegate, response) -> { Map datafeedsByJobId = new HashMap<>(); // There cannot be more than one datafeed per job assert response.getHits().getTotalHits().value <= jobIds.size(); @@ -265,8 +265,8 @@ public void findDatafeedsByJobIds( DatafeedConfig.Builder builder = parseLenientlyFromSource(hit.getSourceRef()); datafeedsByJobId.put(builder.getJobId(), builder); } - listener.onResponse(datafeedsByJobId); - }, listener::onFailure), + delegate.onResponse(datafeedsByJobId); + }), client::search ); } @@ -440,7 +440,7 @@ public void expandDatafeedIds( client.threadPool().getThreadContext(), ML_ORIGIN, searchRequest, - ActionListener.wrap(response -> { + listener.delegateFailureAndWrap((delegate, response) -> { SortedSet datafeedIds = new TreeSet<>(); SearchHit[] hits = response.getHits().getHits(); for (SearchHit hit : hits) { @@ -453,12 +453,12 @@ public void expandDatafeedIds( requiredMatches.filterMatchedIds(datafeedIds); if (requiredMatches.hasUnmatchedIds()) { // some required datafeeds were not found - listener.onFailure(ExceptionsHelper.missingDatafeedException(requiredMatches.unmatchedIdsString())); + delegate.onFailure(ExceptionsHelper.missingDatafeedException(requiredMatches.unmatchedIdsString())); return; } - listener.onResponse(datafeedIds); - }, listener::onFailure), + delegate.onResponse(datafeedIds); + }), client::search ); @@ -502,7 +502,7 @@ public void expandDatafeedConfigs( client.threadPool().getThreadContext(), ML_ORIGIN, searchRequest, - ActionListener.wrap(response -> { + listener.delegateFailureAndWrap((delegate, response) -> { List datafeeds = new ArrayList<>(); Set datafeedIds = new HashSet<>(); SearchHit[] hits = response.getHits().getHits(); @@ -521,12 +521,12 @@ public void expandDatafeedConfigs( requiredMatches.filterMatchedIds(datafeedIds); if (requiredMatches.hasUnmatchedIds()) { // some required datafeeds were not found - listener.onFailure(ExceptionsHelper.missingDatafeedException(requiredMatches.unmatchedIdsString())); + delegate.onFailure(ExceptionsHelper.missingDatafeedException(requiredMatches.unmatchedIdsString())); return; } - listener.onResponse(datafeeds); - }, listener::onFailure), + delegate.onResponse(datafeeds); + }), client::search ); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java index 223154737df3f..d370e8af52549 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DataFrameAnalyticsManager.java @@ -33,7 +33,6 @@ import org.elasticsearch.xpack.core.ml.job.persistence.AnomalyDetectorsIndex; import org.elasticsearch.xpack.core.ml.job.persistence.ElasticsearchMappings; import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper; -import org.elasticsearch.xpack.ml.dataframe.extractor.ExtractedFieldsDetector; import org.elasticsearch.xpack.ml.dataframe.extractor.ExtractedFieldsDetectorFactory; import org.elasticsearch.xpack.ml.dataframe.inference.InferenceRunner; import org.elasticsearch.xpack.ml.dataframe.persistence.DataFrameAnalyticsConfigProvider; @@ -171,9 +170,8 @@ public void execute(DataFrameAnalyticsTask task, ClusterState clusterState, Time }, task::setFailed); // Retrieve configuration - ActionListener statsIndexListener = ActionListener.wrap( - aBoolean -> configProvider.get(task.getParams().getId(), configListener), - configListener::onFailure + ActionListener statsIndexListener = configListener.delegateFailureAndWrap( + (l, aBoolean) -> configProvider.get(task.getParams().getId(), l) ); // Make sure the stats index and alias exist @@ -203,25 +201,22 @@ private void createStatsIndexAndUpdateMappingsIfNecessary( TimeValue masterNodeTimeout, ActionListener listener ) { - ActionListener createIndexListener = ActionListener.wrap( - aBoolean -> ElasticsearchMappings.addDocMappingIfMissing( - MlStatsIndex.writeAlias(), - MlStatsIndex::wrappedMapping, - clientToUse, - clusterState, - masterNodeTimeout, - listener, - MlStatsIndex.STATS_INDEX_MAPPINGS_VERSION - ), - listener::onFailure - ); - MlStatsIndex.createStatsIndexAndAliasIfNecessary( clientToUse, clusterState, expressionResolver, masterNodeTimeout, - createIndexListener + listener.delegateFailureAndWrap( + (l, aBoolean) -> ElasticsearchMappings.addDocMappingIfMissing( + MlStatsIndex.writeAlias(), + MlStatsIndex::wrappedMapping, + clientToUse, + clusterState, + masterNodeTimeout, + l, + MlStatsIndex.STATS_INDEX_MAPPINGS_VERSION + ) + ) ); } @@ -306,25 +301,25 @@ private void executeJobInMiddleOfReindexing(DataFrameAnalyticsTask task, DataFra private void buildInferenceStep(DataFrameAnalyticsTask task, DataFrameAnalyticsConfig config, ActionListener listener) { ParentTaskAssigningClient parentTaskClient = new ParentTaskAssigningClient(client, task.getParentTaskId()); - - ActionListener extractedFieldsDetectorListener = ActionListener.wrap(extractedFieldsDetector -> { - ExtractedFields extractedFields = extractedFieldsDetector.detect().v1(); - InferenceRunner inferenceRunner = new InferenceRunner( - settings, - parentTaskClient, - modelLoadingService, - resultsPersisterService, - task.getParentTaskId(), - config, - extractedFields, - task.getStatsHolder().getProgressTracker(), - task.getStatsHolder().getDataCountsTracker() - ); - InferenceStep inferenceStep = new InferenceStep(client, task, auditor, config, threadPool, inferenceRunner); - listener.onResponse(inferenceStep); - }, listener::onFailure); - - new ExtractedFieldsDetectorFactory(parentTaskClient).createFromDest(config, extractedFieldsDetectorListener); + new ExtractedFieldsDetectorFactory(parentTaskClient).createFromDest( + config, + listener.delegateFailureAndWrap((delegate, extractedFieldsDetector) -> { + ExtractedFields extractedFields = extractedFieldsDetector.detect().v1(); + InferenceRunner inferenceRunner = new InferenceRunner( + settings, + parentTaskClient, + modelLoadingService, + resultsPersisterService, + task.getParentTaskId(), + config, + extractedFields, + task.getStatsHolder().getProgressTracker(), + task.getStatsHolder().getDataCountsTracker() + ); + InferenceStep inferenceStep = new InferenceStep(client, task, auditor, config, threadPool, inferenceRunner); + delegate.onResponse(inferenceStep); + }) + ); } public boolean isNodeShuttingDown() { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DestinationIndex.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DestinationIndex.java index 81de8add4ae2e..8623f456b2035 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DestinationIndex.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/DestinationIndex.java @@ -134,9 +134,11 @@ private static void prepareCreateIndexRequest( AtomicReference settingsHolder = new AtomicReference<>(); AtomicReference mappingsHolder = new AtomicReference<>(); - ActionListener fieldCapabilitiesListener = ActionListener.wrap(fieldCapabilitiesResponse -> { - listener.onResponse(createIndexRequest(clock, config, settingsHolder.get(), mappingsHolder.get(), fieldCapabilitiesResponse)); - }, listener::onFailure); + ActionListener fieldCapabilitiesListener = listener.delegateFailureAndWrap( + (l, fieldCapabilitiesResponse) -> l.onResponse( + createIndexRequest(clock, config, settingsHolder.get(), mappingsHolder.get(), fieldCapabilitiesResponse) + ) + ); ActionListener mappingsListener = ActionListener.wrap(mappings -> { mappingsHolder.set(mappings); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractorFactory.java index b9d7e31a2cf73..09c3ae15c90a3 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/DataFrameDataExtractorFactory.java @@ -147,22 +147,22 @@ public static void createForDestinationIndex( ActionListener listener ) { ExtractedFieldsDetectorFactory extractedFieldsDetectorFactory = new ExtractedFieldsDetectorFactory(client); - extractedFieldsDetectorFactory.createFromDest(config, ActionListener.wrap(extractedFieldsDetector -> { + extractedFieldsDetectorFactory.createFromDest(config, listener.delegateFailureAndWrap((delegate, extractedFieldsDetector) -> { ExtractedFields extractedFields = extractedFieldsDetector.detect().v1(); - - DataFrameDataExtractorFactory extractorFactory = new DataFrameDataExtractorFactory( - client, - config.getId(), - Collections.singletonList(config.getDest().getIndex()), - config.getSource().getParsedQuery(), - extractedFields, - config.getAnalysis().getRequiredFields(), - config.getHeaders(), - config.getAnalysis().supportsMissingValues(), - createTrainTestSplitterFactory(client, config, extractedFields), - Collections.emptyMap() + delegate.onResponse( + new DataFrameDataExtractorFactory( + client, + config.getId(), + Collections.singletonList(config.getDest().getIndex()), + config.getSource().getParsedQuery(), + extractedFields, + config.getAnalysis().getRequiredFields(), + config.getHeaders(), + config.getAnalysis().supportsMissingValues(), + createTrainTestSplitterFactory(client, config, extractedFields), + Collections.emptyMap() + ) ); - listener.onResponse(extractorFactory); - }, listener::onFailure)); + })); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java index 49e25c95713ef..73f8e7bd520d4 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/extractor/ExtractedFieldsDetectorFactory.java @@ -112,11 +112,6 @@ private void getCardinalitiesForFieldsWithConstraints( return; } - ActionListener searchListener = ActionListener.wrap( - searchResponse -> buildFieldCardinalitiesMap(config, searchResponse, listener), - listener::onFailure - ); - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().size(0) .query(config.getSource().getParsedQuery()) .runtimeMappings(config.getSource().getRuntimeMappings()); @@ -147,7 +142,7 @@ private void getCardinalitiesForFieldsWithConstraints( client, TransportSearchAction.TYPE, searchRequest, - searchListener + listener.delegateFailureAndWrap((l, searchResponse) -> buildFieldCardinalitiesMap(config, searchResponse, l)) ); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/persistence/DataFrameAnalyticsConfigProvider.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/persistence/DataFrameAnalyticsConfigProvider.java index 5469c6a7a7d87..8c7d490f37787 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/persistence/DataFrameAnalyticsConfigProvider.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/persistence/DataFrameAnalyticsConfigProvider.java @@ -103,19 +103,17 @@ public void put( TimeValue timeout, ActionListener listener ) { - - ActionListener deleteLeftOverDocsListener = ActionListener.wrap( - r -> index(prepareConfigForIndex(config, headers), null, listener), - listener::onFailure - ); - - ActionListener existsListener = ActionListener.wrap(exists -> { + ActionListener existsListener = listener.delegateFailureAndWrap((l, exists) -> { if (exists) { - listener.onFailure(ExceptionsHelper.dataFrameAnalyticsAlreadyExists(config.getId())); + l.onFailure(ExceptionsHelper.dataFrameAnalyticsAlreadyExists(config.getId())); } else { - deleteLeftOverDocs(config, timeout, deleteLeftOverDocsListener); + deleteLeftOverDocs( + config, + timeout, + l.delegateFailureAndWrap((ll, r) -> index(prepareConfigForIndex(config, headers), null, ll)) + ); } - }, listener::onFailure); + }); exists(config.getId(), existsListener); } @@ -194,10 +192,10 @@ public void update( DataFrameAnalyticsConfig updatedConfig = updatedConfigBuilder.build(); // Index the update config - index(updatedConfig, getResponse, ActionListener.wrap(indexedConfig -> { + index(updatedConfig, getResponse, listener.delegateFailureAndWrap((l, indexedConfig) -> { auditor.info(id, Messages.getMessage(Messages.DATA_FRAME_ANALYTICS_AUDIT_UPDATED, update.getUpdatedFields())); - listener.onResponse(indexedConfig); - }, listener::onFailure)); + l.onResponse(indexedConfig); + })); }, listener::onFailure)); } @@ -269,20 +267,26 @@ private void index( public void get(String id, ActionListener listener) { GetDataFrameAnalyticsAction.Request request = new GetDataFrameAnalyticsAction.Request(); request.setResourceId(id); - executeAsyncWithOrigin(client, ML_ORIGIN, GetDataFrameAnalyticsAction.INSTANCE, request, ActionListener.wrap(response -> { - List analytics = response.getResources().results(); - if (analytics.size() != 1) { - listener.onFailure( - ExceptionsHelper.badRequestException( - "Expected a single match for data frame analytics [{}] " + "but got [{}]", - id, - analytics.size() - ) - ); - } else { - listener.onResponse(analytics.get(0)); - } - }, listener::onFailure)); + executeAsyncWithOrigin( + client, + ML_ORIGIN, + GetDataFrameAnalyticsAction.INSTANCE, + request, + listener.delegateFailureAndWrap((delegate, response) -> { + List analytics = response.getResources().results(); + if (analytics.size() != 1) { + delegate.onFailure( + ExceptionsHelper.badRequestException( + "Expected a single match for data frame analytics [{}] " + "but got [{}]", + id, + analytics.size() + ) + ); + } else { + delegate.onResponse(analytics.get(0)); + } + }) + ); } /** @@ -298,7 +302,7 @@ public void getMultiple(String ids, boolean allowNoMatch, ActionListener listener.onResponse(response.getResources().results()), listener::onFailure) + listener.delegateFailureAndWrap((l, response) -> l.onResponse(response.getResources().results())) ); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/persistence/DataFrameAnalyticsDeleter.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/persistence/DataFrameAnalyticsDeleter.java index 843d9d74a1c7d..2a8b23728fbdb 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/persistence/DataFrameAnalyticsDeleter.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/persistence/DataFrameAnalyticsDeleter.java @@ -126,14 +126,13 @@ private void deleteConfig(String id, ActionListener listen } private void deleteState(DataFrameAnalyticsConfig config, TimeValue timeout, ActionListener listener) { - ActionListener deleteModelStateListener = ActionListener.wrap( - r -> executeDeleteByQuery( + ActionListener deleteModelStateListener = listener.delegateFailureAndWrap( + (l, r) -> executeDeleteByQuery( AnomalyDetectorsIndex.jobStateIndexPattern(), QueryBuilders.idsQuery().addIds(StoredProgress.documentId(config.getId())), timeout, - listener - ), - listener::onFailure + l + ) ); deleteModelState(config, timeout, 1, deleteModelStateListener); @@ -146,13 +145,18 @@ private void deleteModelState(DataFrameAnalyticsConfig config, TimeValue timeout } IdsQueryBuilder query = QueryBuilders.idsQuery().addIds(config.getAnalysis().getStateDocIdPrefix(config.getId()) + docNum); - executeDeleteByQuery(AnomalyDetectorsIndex.jobStateIndexPattern(), query, timeout, ActionListener.wrap(response -> { - if (response.getDeleted() > 0) { - deleteModelState(config, timeout, docNum + 1, listener); - return; - } - listener.onResponse(true); - }, listener::onFailure)); + executeDeleteByQuery( + AnomalyDetectorsIndex.jobStateIndexPattern(), + query, + timeout, + listener.delegateFailureAndWrap((l, response) -> { + if (response.getDeleted() > 0) { + deleteModelState(config, timeout, docNum + 1, l); + return; + } + l.onResponse(true); + }) + ); } private void deleteStats(String jobId, TimeValue timeout, ActionListener listener) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/AbstractDataFrameAnalyticsStep.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/AbstractDataFrameAnalyticsStep.java index 0c693ff2d34f4..112d164601546 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/AbstractDataFrameAnalyticsStep.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/AbstractDataFrameAnalyticsStep.java @@ -67,11 +67,11 @@ public final void execute(ActionListener listener) { listener.onResponse(new StepResponse(true)); return; } - doExecute(ActionListener.wrap(stepResponse -> { + doExecute(listener.delegateFailureAndWrap((l, stepResponse) -> { // We persist progress at the end of each step to ensure we do not have // to repeat the step in case the node goes down without getting a chance to persist progress. - task.persistProgress(() -> listener.onResponse(stepResponse)); - }, listener::onFailure)); + task.persistProgress(() -> l.onResponse(stepResponse)); + })); } protected abstract void doExecute(ActionListener listener); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/AnalysisStep.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/AnalysisStep.java index 9e56387ed773e..ec914546c7de5 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/AnalysisStep.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/AnalysisStep.java @@ -58,17 +58,16 @@ protected void doExecute(ActionListener listener) { final ParentTaskAssigningClient parentTaskClient = parentTaskClient(); // Update state to ANALYZING and start process - ActionListener dataExtractorFactoryListener = ActionListener.wrap( - dataExtractorFactory -> processManager.runJob(task, config, dataExtractorFactory, listener), - listener::onFailure + ActionListener dataExtractorFactoryListener = listener.delegateFailureAndWrap( + (l, dataExtractorFactory) -> processManager.runJob(task, config, dataExtractorFactory, l) ); - ActionListener refreshListener = ActionListener.wrap(refreshResponse -> { + ActionListener refreshListener = dataExtractorFactoryListener.delegateFailureAndWrap((l, refreshResponse) -> { // TODO This could fail with errors. In that case we get stuck with the copied index. // We could delete the index in case of failure or we could try building the factory before reindexing // to catch the error early on. - DataFrameDataExtractorFactory.createForDestinationIndex(parentTaskClient, config, dataExtractorFactoryListener); - }, dataExtractorFactoryListener::onFailure); + DataFrameDataExtractorFactory.createForDestinationIndex(parentTaskClient, config, l); + }); // First we need to refresh the dest index to ensure data is searchable in case the job // was stopped after reindexing was complete but before the index was refreshed. diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/FinalStep.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/FinalStep.java index dbf1f3e7be3d9..258c66ad5cb0f 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/FinalStep.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/FinalStep.java @@ -59,18 +59,13 @@ public Name name() { @Override protected void doExecute(ActionListener listener) { - - ActionListener refreshListener = ActionListener.wrap( - refreshResponse -> listener.onResponse(new StepResponse(false)), - listener::onFailure - ); - - ActionListener dataCountsIndexedListener = ActionListener.wrap( - indexResponse -> refreshIndices(refreshListener), - listener::onFailure + indexDataCounts( + listener.delegateFailureAndWrap( + (l, indexResponse) -> refreshIndices( + l.delegateFailureAndWrap((ll, refreshResponse) -> ll.onResponse(new StepResponse(false))) + ) + ) ); - - indexDataCounts(dataCountsIndexedListener); } private void indexDataCounts(ActionListener listener) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/InferenceStep.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/InferenceStep.java index ad005e6d9ae6c..37ad1a5cb8f56 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/InferenceStep.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/dataframe/steps/InferenceStep.java @@ -13,7 +13,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.TransportSearchAction; -import org.elasticsearch.action.support.broadcast.BroadcastResponse; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.query.QueryBuilders; @@ -70,27 +69,21 @@ protected void doExecute(ActionListener listener) { return; } - ActionListener modelIdListener = ActionListener.wrap(modelId -> runInference(modelId, listener), listener::onFailure); - - ActionListener testDocsExistListener = ActionListener.wrap(testDocsExist -> { - if (testDocsExist) { - getModelId(modelIdListener); - } else { - // no need to run inference at all so let us skip - // loading the model in memory. - LOGGER.debug(() -> "[" + config.getId() + "] Inference step completed immediately as there are no test docs"); - task.getStatsHolder().getProgressTracker().updateInferenceProgress(100); - listener.onResponse(new StepResponse(isTaskStopping())); - return; - } - }, listener::onFailure); - - ActionListener refreshDestListener = ActionListener.wrap( - refreshResponse -> searchIfTestDocsExist(testDocsExistListener), - listener::onFailure + refreshDestAsync( + listener.delegateFailureAndWrap( + (delegate, refreshResponse) -> searchIfTestDocsExist(delegate.delegateFailureAndWrap((delegate2, testDocsExist) -> { + if (testDocsExist) { + getModelId(delegate2.delegateFailureAndWrap((l, modelId) -> runInference(modelId, l))); + } else { + // no need to run inference at all so let us skip + // loading the model in memory. + LOGGER.debug(() -> "[" + config.getId() + "] Inference step completed immediately as there are no test docs"); + task.getStatsHolder().getProgressTracker().updateInferenceProgress(100); + delegate2.onResponse(new StepResponse(isTaskStopping())); + } + })) + ) ); - - refreshDestAsync(refreshDestListener); } private void runInference(String modelId, ActionListener listener) { @@ -124,10 +117,7 @@ private void searchIfTestDocsExist(ActionListener listener) { ML_ORIGIN, TransportSearchAction.TYPE, searchRequest, - ActionListener.wrap( - searchResponse -> listener.onResponse(searchResponse.getHits().getTotalHits().value > 0), - listener::onFailure - ) + listener.delegateFailureAndWrap((l, searchResponse) -> l.onResponse(searchResponse.getHits().getTotalHits().value > 0)) ); } @@ -142,14 +132,20 @@ private void getModelId(ActionListener listener) { SearchRequest searchRequest = new SearchRequest(InferenceIndexConstants.INDEX_PATTERN); searchRequest.source(searchSourceBuilder); - executeAsyncWithOrigin(client, ML_ORIGIN, TransportSearchAction.TYPE, searchRequest, ActionListener.wrap(searchResponse -> { - SearchHit[] hits = searchResponse.getHits().getHits(); - if (hits.length == 0) { - listener.onFailure(new ResourceNotFoundException("No model could be found to perform inference")); - } else { - listener.onResponse(hits[0].getId()); - } - }, listener::onFailure)); + executeAsyncWithOrigin( + client, + ML_ORIGIN, + TransportSearchAction.TYPE, + searchRequest, + listener.delegateFailureAndWrap((l, searchResponse) -> { + SearchHit[] hits = searchResponse.getHits().getHits(); + if (hits.length == 0) { + l.onFailure(new ResourceNotFoundException("No model could be found to perform inference")); + } else { + l.onResponse(hits[0].getId()); + } + }) + ); } @Override diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/JobManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/JobManager.java index 7532ae4317830..9887152c6f311 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/JobManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/JobManager.java @@ -142,14 +142,8 @@ public void jobExists(String jobId, @Nullable TaskId parentTaskId, ActionListene * a ResourceNotFoundException is returned */ public void getJob(String jobId, ActionListener jobListener) { - jobConfigProvider.getJob( - jobId, - null, - ActionListener.wrap( - r -> jobListener.onResponse(r.build()), // TODO JIndex we shouldn't be building the job here - jobListener::onFailure - ) - ); + // TODO JIndex we shouldn't be building the job here + jobConfigProvider.getJob(jobId, null, jobListener.delegateFailureAndWrap((l, r) -> l.onResponse(r.build()))); } /** @@ -183,15 +177,14 @@ public void expandJobs(String expression, boolean allowNoMatch, ActionListener jobsListener.onResponse( + jobsListener.delegateFailureAndWrap( + (l, jobBuilders) -> l.onResponse( new QueryPage<>( jobBuilders.stream().map(Job.Builder::build).collect(Collectors.toList()), jobBuilders.size(), Job.RESULTS_FIELD ) - ), - jobsListener::onFailure + ) ) ); } @@ -253,10 +246,10 @@ public void putJob( @Override public void onResponse(Boolean mappingsUpdated) { - jobConfigProvider.putJob(job, ActionListener.wrap(response -> { + jobConfigProvider.putJob(job, actionListener.delegateFailureAndWrap((l, response) -> { auditor.info(job.getId(), Messages.getMessage(Messages.JOB_AUDIT_CREATED)); - actionListener.onResponse(new PutJobAction.Response(job)); - }, actionListener::onFailure)); + l.onResponse(new PutJobAction.Response(job)); + })); } @Override @@ -275,17 +268,16 @@ public void onFailure(Exception e) { } }; - ActionListener addDocMappingsListener = ActionListener.wrap( - indicesCreated -> ElasticsearchMappings.addDocMappingIfMissing( + ActionListener addDocMappingsListener = putJobListener.delegateFailureAndWrap( + (l, indicesCreated) -> ElasticsearchMappings.addDocMappingIfMissing( MlConfigIndex.indexName(), MlConfigIndex::mapping, client, state, request.masterNodeTimeout(), - putJobListener, + l, MlConfigIndex.CONFIG_INDEX_MAPPINGS_VERSION - ), - putJobListener::onFailure + ) ); ActionListener> checkForLeftOverDocs = ActionListener.wrap(matchedIds -> { @@ -634,14 +626,15 @@ public void updateProcessOnCalendarChanged(List calendarJobIds, ActionLi // calendarJobIds may be a group or job jobConfigProvider.expandGroupIds( calendarJobIds, - ActionListener.wrap(expandedIds -> threadPool.executor(MachineLearning.UTILITY_THREAD_POOL_NAME).execute(() -> { - // Merge the expanded group members with the request Ids. - // Ids that aren't jobs will be filtered by isJobOpen() - expandedIds.addAll(calendarJobIds); - - openJobIds.retainAll(expandedIds); - submitJobEventUpdate(openJobIds, updateListener); - }), updateListener::onFailure) + updateListener.delegateFailureAndWrap( + (delegate, expandedIds) -> threadPool.executor(MachineLearning.UTILITY_THREAD_POOL_NAME).execute(() -> { + // Merge the expanded group members with the request Ids. + // Ids that aren't jobs will be filtered by isJobOpen() + expandedIds.addAll(calendarJobIds); + openJobIds.retainAll(expandedIds); + submitJobEventUpdate(openJobIds, delegate); + }) + ) ); } @@ -678,12 +671,13 @@ public void revertSnapshot( jobResultsPersister.persistQuantiles( modelSnapshot.getQuantiles(), WriteRequest.RefreshPolicy.IMMEDIATE, - ActionListener.wrap(quantilesResponse -> { - // The quantiles can be large, and totally dominate the output - - // it's clearer to remove them as they are not necessary for the revert op - ModelSnapshot snapshotWithoutQuantiles = new ModelSnapshot.Builder(modelSnapshot).setQuantiles(null).build(); - actionListener.onResponse(new RevertModelSnapshotAction.Response(snapshotWithoutQuantiles)); - }, actionListener::onFailure) + // The quantiles can be large, and totally dominate the output - + // it's clearer to remove them as they are not necessary for the revert op + actionListener.delegateFailureAndWrap( + (l, quantilesResponse) -> l.onResponse( + new RevertModelSnapshotAction.Response(new ModelSnapshot.Builder(modelSnapshot).setQuantiles(null).build()) + ) + ) ); }; diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/AbstractExpiredJobDataRemover.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/AbstractExpiredJobDataRemover.java index aa82c7a261b96..bd1e47e3cb160 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/AbstractExpiredJobDataRemover.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/AbstractExpiredJobDataRemover.java @@ -70,19 +70,19 @@ private void removeData( return; } - calcCutoffEpochMs(job.getId(), retentionDays, ActionListener.wrap(response -> { + calcCutoffEpochMs(job.getId(), retentionDays, listener.delegateFailureAndWrap((delegate, response) -> { if (response == null) { - removeData(jobIterator, requestsPerSecond, listener, isTimedOutSupplier); + removeData(jobIterator, requestsPerSecond, delegate, isTimedOutSupplier); } else { removeDataBefore( job, requestsPerSecond, response.latestTimeMs, response.cutoffEpochMs, - ActionListener.wrap(r -> removeData(jobIterator, requestsPerSecond, listener, isTimedOutSupplier), listener::onFailure) + delegate.delegateFailureAndWrap((l, r) -> removeData(jobIterator, requestsPerSecond, l, isTimedOutSupplier)) ); } - }, listener::onFailure)); + })); } abstract void calcCutoffEpochMs(String jobId, long retentionDays, ActionListener listener); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/EmptyStateIndexRemover.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/EmptyStateIndexRemover.java index 0a5612f8e0ccc..1c8c100939dc7 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/EmptyStateIndexRemover.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/EmptyStateIndexRemover.java @@ -42,20 +42,20 @@ public void remove(float requestsPerSec, ActionListener listener, Boole listener.onResponse(false); return; } - getEmptyStateIndices(ActionListener.wrap(emptyStateIndices -> { + getEmptyStateIndices(listener.delegateFailureAndWrap((delegate, emptyStateIndices) -> { if (emptyStateIndices.isEmpty()) { - listener.onResponse(true); + delegate.onResponse(true); return; } - getCurrentStateIndices(ActionListener.wrap(currentStateIndices -> { + getCurrentStateIndices(delegate.delegateFailureAndWrap((l, currentStateIndices) -> { Set stateIndicesToRemove = Sets.difference(emptyStateIndices, currentStateIndices); if (stateIndicesToRemove.isEmpty()) { - listener.onResponse(true); + l.onResponse(true); return; } - executeDeleteEmptyStateIndices(stateIndicesToRemove, listener); - }, listener::onFailure)); - }, listener::onFailure)); + executeDeleteEmptyStateIndices(stateIndicesToRemove, l); + })); + })); } catch (Exception e) { listener.onFailure(e); } @@ -64,15 +64,21 @@ public void remove(float requestsPerSec, ActionListener listener, Boole private void getEmptyStateIndices(ActionListener> listener) { IndicesStatsRequest indicesStatsRequest = new IndicesStatsRequest().indices(AnomalyDetectorsIndex.jobStateIndexPattern()); indicesStatsRequest.setParentTask(parentTaskId); - client.admin().indices().stats(indicesStatsRequest, ActionListener.wrap(indicesStatsResponse -> { - Set emptyStateIndices = indicesStatsResponse.getIndices() - .values() - .stream() - .filter(stats -> stats.getTotal().getDocs().getCount() == 0) - .map(IndexStats::getIndex) - .collect(toSet()); - listener.onResponse(emptyStateIndices); - }, listener::onFailure)); + client.admin() + .indices() + .stats( + indicesStatsRequest, + listener.delegateFailureAndWrap( + (l, indicesStatsResponse) -> l.onResponse( + indicesStatsResponse.getIndices() + .values() + .stream() + .filter(stats -> stats.getTotal().getDocs().getCount() == 0) + .map(IndexStats::getIndex) + .collect(toSet()) + ) + ) + ); } private void getCurrentStateIndices(ActionListener> listener) { @@ -82,7 +88,7 @@ private void getCurrentStateIndices(ActionListener> listener) { .indices() .getIndex( getIndexRequest, - ActionListener.wrap(getIndexResponse -> listener.onResponse(Set.of(getIndexResponse.getIndices())), listener::onFailure) + listener.delegateFailureAndWrap((l, getIndexResponse) -> l.onResponse(Set.of(getIndexResponse.getIndices()))) ); } @@ -93,7 +99,7 @@ private void executeDeleteEmptyStateIndices(Set emptyStateIndices, Actio .indices() .delete( deleteIndexRequest, - ActionListener.wrap(deleteResponse -> listener.onResponse(deleteResponse.isAcknowledged()), listener::onFailure) + listener.delegateFailureAndWrap((l, deleteResponse) -> l.onResponse(deleteResponse.isAcknowledged())) ); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/ExpiredModelSnapshotsRemover.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/ExpiredModelSnapshotsRemover.java index 507e9dac6282d..27bd3c926d944 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/ExpiredModelSnapshotsRemover.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/ExpiredModelSnapshotsRemover.java @@ -249,7 +249,7 @@ private void deleteModelSnapshots(List modelSnapshots, String job return; } JobDataDeleter deleter = new JobDataDeleter(client, jobId); - deleter.deleteModelSnapshots(modelSnapshots, ActionListener.wrap(bulkResponse -> { + deleter.deleteModelSnapshots(modelSnapshots, listener.delegateFailureAndWrap((l, bulkResponse) -> { auditor.info(jobId, Messages.getMessage(Messages.JOB_AUDIT_SNAPSHOTS_DELETED, modelSnapshots.size())); LOGGER.debug( () -> format( @@ -259,8 +259,8 @@ private void deleteModelSnapshots(List modelSnapshots, String job modelSnapshots.stream().map(ModelSnapshot::getDescription).collect(toList()) ) ); - listener.onResponse(true); - }, listener::onFailure)); + l.onResponse(true); + })); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/ExpiredResultsRemover.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/ExpiredResultsRemover.java index 654ce87fc5e30..35e16b9fa8b88 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/ExpiredResultsRemover.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/retention/ExpiredResultsRemover.java @@ -195,11 +195,11 @@ static void latestBucketTime(OriginSettingClient client, TaskId parentTaskId, St searchRequest.indicesOptions(MlIndicesUtils.addIgnoreUnavailable(SearchRequest.DEFAULT_INDICES_OPTIONS)); searchRequest.setParentTask(parentTaskId); - client.search(searchRequest, ActionListener.wrap(response -> { + client.search(searchRequest, listener.delegateFailureAndWrap((delegate, response) -> { SearchHit[] hits = response.getHits().getHits(); if (hits.length == 0) { // no buckets found - listener.onResponse(null); + delegate.onResponse(null); } else { try ( @@ -210,12 +210,12 @@ static void latestBucketTime(OriginSettingClient client, TaskId parentTaskId, St ) ) { Bucket bucket = Bucket.LENIENT_PARSER.apply(parser, null); - listener.onResponse(bucket.getTimestamp().getTime()); + delegate.onResponse(bucket.getTimestamp().getTime()); } catch (IOException e) { - listener.onFailure(new ElasticsearchParseException("failed to parse bucket", e)); + delegate.onFailure(new ElasticsearchParseException("failed to parse bucket", e)); } } - }, listener::onFailure)); + })); } private void auditResultsWereDeleted(String jobId, long cutoffEpochMs) { 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 09cd6225cf0ca..c50e744bde96b 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 @@ -398,18 +398,18 @@ private void stopAssociatedDatafeedForFailedJob(String jobId) { } private void getRunningDatafeed(String jobId, ActionListener listener) { - ActionListener> datafeedListener = ActionListener.wrap(datafeeds -> { + ActionListener> datafeedListener = listener.delegateFailureAndWrap((delegate, datafeeds) -> { assert datafeeds.size() <= 1; if (datafeeds.isEmpty()) { - listener.onResponse(null); + delegate.onResponse(null); return; } String datafeedId = datafeeds.iterator().next(); PersistentTasksCustomMetadata tasks = clusterState.getMetadata().custom(PersistentTasksCustomMetadata.TYPE); PersistentTasksCustomMetadata.PersistentTask datafeedTask = MlTasks.getDatafeedTask(datafeedId, tasks); - listener.onResponse(datafeedTask != null ? datafeedId : null); - }, listener::onFailure); + delegate.onResponse(datafeedTask != null ? datafeedId : null); + }); datafeedConfigProvider.findDatafeedIdsForJobIds(Collections.singleton(jobId), datafeedListener); } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/EmptyStateIndexRemoverTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/EmptyStateIndexRemoverTests.java index b560a758b8e83..a452c156e77f1 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/EmptyStateIndexRemoverTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/EmptyStateIndexRemoverTests.java @@ -27,6 +27,7 @@ import org.junit.Before; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; +import org.mockito.Mockito; import org.mockito.stubbing.Answer; import java.util.Map; @@ -57,6 +58,7 @@ public void setUpTests() { client = mock(Client.class); OriginSettingClient originSettingClient = MockOriginSettingClient.mockOriginSettingClient(client, ClientHelper.ML_ORIGIN); listener = mock(ActionListener.class); + when(listener.delegateFailureAndWrap(any())).thenCallRealMethod(); deleteIndexRequestCaptor = ArgumentCaptor.forClass(DeleteIndexRequest.class); remover = new EmptyStateIndexRemover(originSettingClient, new TaskId("test", 0L)); @@ -66,6 +68,7 @@ public void setUpTests() { public void verifyNoOtherInteractionsWithMocks() { verify(client).settings(); verify(client, atLeastOnce()).threadPool(); + verify(listener, Mockito.atLeast(0)).delegateFailureAndWrap(any()); verifyNoMoreInteractions(client, listener); } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/ExpiredAnnotationsRemoverTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/ExpiredAnnotationsRemoverTests.java index ad0719011c92e..39f1ead7e24e0 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/ExpiredAnnotationsRemoverTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/ExpiredAnnotationsRemoverTests.java @@ -60,6 +60,7 @@ public void setUpTests() { client = mock(Client.class); originSettingClient = MockOriginSettingClient.mockOriginSettingClient(client, ClientHelper.ML_ORIGIN); listener = mock(ActionListener.class); + when(listener.delegateFailureAndWrap(any())).thenCallRealMethod(); } public void testRemove_GivenNoJobs() { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/ExpiredResultsRemoverTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/ExpiredResultsRemoverTests.java index 5aa5b847b26be..4dbb4eda07b0a 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/ExpiredResultsRemoverTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/retention/ExpiredResultsRemoverTests.java @@ -60,6 +60,7 @@ public void setUpTests() { client = mock(Client.class); originSettingClient = MockOriginSettingClient.mockOriginSettingClient(client, ClientHelper.ML_ORIGIN); listener = mock(ActionListener.class); + when(listener.delegateFailureAndWrap(any())).thenCallRealMethod(); } public void testRemove_GivenNoJobs() { From 0dd92b027a601d12d9dc46d501bf1c5a2fd88fb8 Mon Sep 17 00:00:00 2001 From: Ievgen Degtiarenko Date: Tue, 5 Mar 2024 09:48:11 +0100 Subject: [PATCH 234/250] Improve data_tier allocation explanation message (#105858) This change updates allocation explanation message for the DataTierDecider to include the actual role (eg generic data or specific data_*) that permitted the shard to be allocated to the node. --- .../allocation/DataTierAllocationDecider.java | 26 +++++++------------ .../DataTierAllocationDeciderTests.java | 9 +++++++ 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierAllocationDecider.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierAllocationDecider.java index 9231dfb744a36..eb4db6c24507d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierAllocationDecider.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierAllocationDecider.java @@ -95,21 +95,18 @@ public static Decision shouldFilter( ); if (tier.isPresent()) { String tierName = tier.get(); - if (allocationAllowed(tierName, node)) { - if (allocation.debugDecision()) { - return debugYesAllowed(allocation, tierPreference, tierName); - } - return Decision.YES; + assert Strings.hasText(tierName) : "tierName must be not null and non-empty, but was [" + tierName + "]"; + if (node.hasRole(DiscoveryNodeRole.DATA_ROLE.roleName())) { + return allocation.debugDecision() + ? debugYesAllowed(allocation, tierPreference, DiscoveryNodeRole.DATA_ROLE.roleName()) + : Decision.YES; } - if (allocation.debugDecision()) { - return debugNoRequirementsNotMet(allocation, tierPreference, tierName); + if (node.hasRole(tierName)) { + return allocation.debugDecision() ? debugYesAllowed(allocation, tierPreference, tierName) : Decision.YES; } - return Decision.NO; + return allocation.debugDecision() ? debugNoRequirementsNotMet(allocation, tierPreference, tierName) : Decision.NO; } - if (allocation.debugDecision()) { - return debugNoNoNodesAvailable(allocation, tierPreference); - } - return Decision.NO; + return allocation.debugDecision() ? debugNoNoNodesAvailable(allocation, tierPreference) : Decision.NO; } private static Decision debugNoNoNodesAvailable(RoutingAllocation allocation, List tierPreference) { @@ -278,11 +275,6 @@ static boolean tierNodesPresentConsideringRemovals(String singleTier, DiscoveryN return false; } - public static boolean allocationAllowed(String tierName, DiscoveryNode node) { - assert Strings.hasText(tierName) : "tierName must be not null and non-empty, but was [" + tierName + "]"; - return node.hasRole(DiscoveryNodeRole.DATA_ROLE.roleName()) || node.hasRole(tierName); - } - public static boolean allocationAllowed(String tierName, Set roles) { assert Strings.hasText(tierName) : "tierName must be not null and non-empty, but was [" + tierName + "]"; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierAllocationDeciderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierAllocationDeciderTests.java index 7134ceba475fe..7ed57ca93adf0 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierAllocationDeciderTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/cluster/routing/allocation/DataTierAllocationDeciderTests.java @@ -202,7 +202,16 @@ public void testIndexPrefer() { ) ); } + } + { + final var state = clusterStateWithIndexAndNodes("data_warm", DiscoveryNodes.builder().add(DATA_NODE).build(), null); + assertAllocationDecision( + state, + DATA_NODE, + Decision.Type.YES, + "index has a preference for tiers [data_warm] and node has tier [data]" + ); } } From 1c03489f469713621522d50fc9d1b9cc2c96a747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Tue, 5 Mar 2024 10:20:22 +0100 Subject: [PATCH 235/250] YAML test framework: simplify version node_selector (#103954) * Simplify version node_selector: remove ranges and allow just current/non_current * Version match still present, but only for legacy rest compat tests --- .../test/rest/ClientYamlTestSuiteIT.java | 9 ++- .../test/indices.put_mapping/10_basic.yml | 4 +- .../rest/yaml/ESClientYamlSuiteTestCase.java | 26 ++++++++ .../test/rest/yaml/section/DoSection.java | 43 +++++++++++-- .../rest/yaml/section/DoSectionTests.java | 64 ++++++++++++------- 5 files changed, 112 insertions(+), 34 deletions(-) diff --git a/rest-api-spec/src/yamlRestTest/java/org/elasticsearch/test/rest/ClientYamlTestSuiteIT.java b/rest-api-spec/src/yamlRestTest/java/org/elasticsearch/test/rest/ClientYamlTestSuiteIT.java index 465f17eca5532..2b3bab21e8ae6 100644 --- a/rest-api-spec/src/yamlRestTest/java/org/elasticsearch/test/rest/ClientYamlTestSuiteIT.java +++ b/rest-api-spec/src/yamlRestTest/java/org/elasticsearch/test/rest/ClientYamlTestSuiteIT.java @@ -13,6 +13,7 @@ import com.carrotsearch.randomizedtesting.annotations.TimeoutSuite; import org.apache.lucene.tests.util.TimeUnits; +import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.test.cluster.ElasticsearchCluster; import org.elasticsearch.test.cluster.FeatureFlag; import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; @@ -39,9 +40,15 @@ public ClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate super(testCandidate); } + @UpdateForV9 // remove restCompat check @ParametersFactory public static Iterable parameters() throws Exception { - return createParameters(); + String restCompatProperty = System.getProperty("tests.restCompat"); + if ("true".equals(restCompatProperty)) { + return createParametersWithLegacyNodeSelectorSupport(); + } else { + return createParameters(); + } } @Override 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 fc747f401b11d..ef121411d8351 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 @@ -87,7 +87,7 @@ --- "Put mappings with explicit _doc type bwc": - skip: - version: "8.0.0 - " #TODO: add "mixed" to skip test for mixed cluster/upgrade tests + version: "8.0.0 - " reason: "old deprecation message for pre 8.0" features: "node_selector" - do: @@ -96,7 +96,7 @@ - do: node_selector: - version: " - 7.99.99" #TODO: OR replace with "non_current" here + version: "original" catch: bad_request indices.put_mapping: index: test_index diff --git a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/ESClientYamlSuiteTestCase.java b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/ESClientYamlSuiteTestCase.java index 4be9481df58b1..7d8d1175385a1 100644 --- a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/ESClientYamlSuiteTestCase.java +++ b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/ESClientYamlSuiteTestCase.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.core.IOUtils; +import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.test.ClasspathUtils; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.test.rest.TestFeatureService; @@ -35,8 +36,10 @@ import org.elasticsearch.test.rest.yaml.restspec.ClientYamlSuiteRestSpec; import org.elasticsearch.test.rest.yaml.section.ClientYamlTestSection; import org.elasticsearch.test.rest.yaml.section.ClientYamlTestSuite; +import org.elasticsearch.test.rest.yaml.section.DoSection; import org.elasticsearch.test.rest.yaml.section.ExecutableSection; import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; import org.junit.AfterClass; @@ -61,6 +64,7 @@ import java.util.SortedSet; import java.util.TreeSet; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder; @@ -230,6 +234,28 @@ public static void closeClient() throws IOException { } } + /** + * Create parameters for this parameterized test. + * Enables support for parsing the legacy version-based node_selector format. + */ + @Deprecated + @UpdateForV9 + public static Iterable createParametersWithLegacyNodeSelectorSupport() throws Exception { + var executableSectionRegistry = new NamedXContentRegistry( + Stream.concat( + ExecutableSection.DEFAULT_EXECUTABLE_CONTEXTS.stream().filter(entry -> entry.name.getPreferredName().equals("do") == false), + Stream.of( + new NamedXContentRegistry.Entry( + ExecutableSection.class, + new ParseField("do"), + DoSection::parseWithLegacyNodeSelectorSupport + ) + ) + ).toList() + ); + return createParameters(executableSectionRegistry, null); + } + /** * Create parameters for this parameterized test. Uses the * {@link ExecutableSection#XCONTENT_REGISTRY list} of executable sections diff --git a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java index 4155472b42640..e850ade2bdf1d 100644 --- a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java +++ b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/DoSection.java @@ -10,6 +10,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.Build; import org.elasticsearch.Version; import org.elasticsearch.client.HasAttributeNodeSelector; import org.elasticsearch.client.Node; @@ -86,6 +87,16 @@ */ public class DoSection implements ExecutableSection { public static DoSection parse(XContentParser parser) throws IOException { + return parse(parser, false); + } + + @UpdateForV9 + @Deprecated + public static DoSection parseWithLegacyNodeSelectorSupport(XContentParser parser) throws IOException { + return parse(parser, true); + } + + private static DoSection parse(XContentParser parser, boolean enableLegacyNodeSelectorSupport) throws IOException { String currentFieldName = null; XContentParser.Token token; @@ -175,7 +186,7 @@ public static DoSection parse(XContentParser parser) throws IOException { if (token == XContentParser.Token.FIELD_NAME) { selectorName = parser.currentName(); } else { - NodeSelector newSelector = buildNodeSelector(selectorName, parser); + NodeSelector newSelector = buildNodeSelector(selectorName, parser, enableLegacyNodeSelectorSupport); nodeSelector = nodeSelector == NodeSelector.ANY ? newSelector : new ComposeNodeSelector(nodeSelector, newSelector); @@ -610,10 +621,11 @@ private String formatStatusCodeMessage(ClientYamlTestResponse restTestResponse, ) ); - private static NodeSelector buildNodeSelector(String name, XContentParser parser) throws IOException { + private static NodeSelector buildNodeSelector(String name, XContentParser parser, boolean enableLegacyVersionSupport) + throws IOException { return switch (name) { case "attribute" -> parseAttributeValuesSelector(parser); - case "version" -> parseVersionSelector(parser); + case "version" -> parseVersionSelector(parser, enableLegacyVersionSupport); default -> throw new XContentParseException(parser.getTokenLocation(), "unknown node_selector [" + name + "]"); }; } @@ -678,14 +690,31 @@ private static boolean matchWithRange( } } - private static NodeSelector parseVersionSelector(XContentParser parser) throws IOException { + private static NodeSelector parseVersionSelector(XContentParser parser, boolean enableLegacyVersionSupport) throws IOException { if (false == parser.currentToken().isValue()) { throw new XContentParseException(parser.getTokenLocation(), "expected [version] to be a value"); } - var acceptedVersionRange = VersionRange.parseVersionRanges(parser.text()); - final Predicate nodeMatcher = nodeVersion -> matchWithRange(nodeVersion, acceptedVersionRange, parser.getTokenLocation()); - final String versionSelectorString = "version ranges " + acceptedVersionRange; + final Predicate nodeMatcher; + final String versionSelectorString; + if (parser.text().equals("current")) { + nodeMatcher = nodeVersion -> Build.current().version().equals(nodeVersion); + versionSelectorString = "version is " + Build.current().version() + " (current)"; + } else if (parser.text().equals("original")) { + nodeMatcher = nodeVersion -> Build.current().version().equals(nodeVersion) == false; + versionSelectorString = "version is not current (original)"; + } else { + if (enableLegacyVersionSupport) { + var acceptedVersionRange = VersionRange.parseVersionRanges(parser.text()); + nodeMatcher = nodeVersion -> matchWithRange(nodeVersion, acceptedVersionRange, parser.getTokenLocation()); + versionSelectorString = "version ranges " + acceptedVersionRange; + } else { + throw new XContentParseException( + parser.getTokenLocation(), + "unknown version selector [" + parser.text() + "]. Only [current] and [original] are allowed." + ); + } + } return new NodeSelector() { @Override diff --git a/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/DoSectionTests.java b/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/DoSectionTests.java index 0cb9a3e29e63f..7d9557d29e568 100644 --- a/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/DoSectionTests.java +++ b/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/DoSectionTests.java @@ -10,12 +10,12 @@ import org.apache.http.HttpHost; import org.elasticsearch.Build; -import org.elasticsearch.Version; import org.elasticsearch.client.Node; import org.elasticsearch.client.NodeSelector; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.core.Strings; +import org.elasticsearch.core.UpdateForV9; import org.elasticsearch.test.rest.yaml.ClientYamlTestExecutionContext; import org.elasticsearch.test.rest.yaml.ClientYamlTestResponse; import org.elasticsearch.xcontent.XContentLocation; @@ -579,14 +579,15 @@ public void testParseDoSectionAllowedWarnings() throws Exception { assertThat(e.getMessage(), equalTo("the warning [foo] was both allowed and expected")); } - public void testNodeSelectorByVersionRange() throws IOException { + @UpdateForV9 // remove + public void testLegacyNodeSelectorByVersionRange() throws IOException { parser = createParser(YamlXContent.yamlXContent, """ node_selector: version: 5.2.0-6.0.0 indices.get_field_mapping: index: test_index"""); - DoSection doSection = DoSection.parse(parser); + DoSection doSection = DoSection.parseWithLegacyNodeSelectorSupport(parser); assertNotSame(NodeSelector.ANY, doSection.getApiCallSection().getNodeSelector()); Node v170 = nodeWithVersion("1.7.0"); Node v521 = nodeWithVersion("5.2.1"); @@ -629,26 +630,21 @@ public void testNodeSelectorByVersionRange() throws IOException { } } - public void testNodeSelectorByVersionRangeFailsWithNonSemanticVersion() throws IOException { + public void testNodeSelectorByVersionRangeFails() throws IOException { parser = createParser(YamlXContent.yamlXContent, """ node_selector: version: 5.2.0-6.0.0 indices.get_field_mapping: index: test_index"""); - DoSection doSection = DoSection.parse(parser); - assertNotSame(NodeSelector.ANY, doSection.getApiCallSection().getNodeSelector()); - Node nonSemantic = nodeWithVersion("abddef"); - List nodes = new ArrayList<>(); + var exception = expectThrows(XContentParseException.class, () -> DoSection.parse(parser)); + assertThat(exception.getMessage(), endsWith("unknown version selector [5.2.0-6.0.0]. Only [current] and [original] are allowed.")); - var exception = expectThrows( - XContentParseException.class, - () -> doSection.getApiCallSection().getNodeSelector().select(List.of(nonSemantic)) - ); - assertThat( - exception.getMessage(), - endsWith("[version] range node selector expects a semantic version format (x.y.z), but found abddef") - ); + // We are throwing an early exception - this means the parser content is not fully consumed. This is OK as it would make + // the tests fail pointing to the correct syntax error location, preventing any further use of parser. + // Explicitly close the parser to avoid AbstractClientYamlTestFragmentParserTestCase checks. + parser.close(); + parser = null; } public void testNodeSelectorCurrentVersion() throws IOException { @@ -663,16 +659,36 @@ public void testNodeSelectorCurrentVersion() throws IOException { Node v170 = nodeWithVersion("1.7.0"); Node v521 = nodeWithVersion("5.2.1"); Node v550 = nodeWithVersion("5.5.0"); - Node oldCurrent = nodeWithVersion(Version.CURRENT.toString()); - Node newCurrent = nodeWithVersion(Build.current().version()); + Node current = nodeWithVersion(Build.current().version()); + List nodes = new ArrayList<>(); + nodes.add(v170); + nodes.add(v521); + nodes.add(v550); + nodes.add(current); + doSection.getApiCallSection().getNodeSelector().select(nodes); + assertEquals(List.of(current), nodes); + } + + public void testNodeSelectorNonCurrentVersion() throws IOException { + parser = createParser(YamlXContent.yamlXContent, """ + node_selector: + version: original + indices.get_field_mapping: + index: test_index"""); + + DoSection doSection = DoSection.parse(parser); + assertNotSame(NodeSelector.ANY, doSection.getApiCallSection().getNodeSelector()); + Node v170 = nodeWithVersion("1.7.0"); + Node v521 = nodeWithVersion("5.2.1"); + Node v550 = nodeWithVersion("5.5.0"); + Node current = nodeWithVersion(Build.current().version()); List nodes = new ArrayList<>(); nodes.add(v170); nodes.add(v521); nodes.add(v550); - nodes.add(oldCurrent); - nodes.add(newCurrent); + nodes.add(current); doSection.getApiCallSection().getNodeSelector().select(nodes); - assertEquals(List.of(oldCurrent, newCurrent), nodes); + assertEquals(List.of(v170, v521, v550), nodes); } private static Node nodeWithVersion(String version) { @@ -741,7 +757,7 @@ private static Node nodeWithAttributes(Map> attributes) { public void testNodeSelectorByTwoThings() throws IOException { parser = createParser(YamlXContent.yamlXContent, """ node_selector: - version: 5.2.0-6.0.0 + version: current attribute: attr: val indices.get_field_mapping: @@ -749,9 +765,9 @@ public void testNodeSelectorByTwoThings() throws IOException { DoSection doSection = DoSection.parse(parser); assertNotSame(NodeSelector.ANY, doSection.getApiCallSection().getNodeSelector()); - Node both = nodeWithVersionAndAttributes("5.2.1", singletonMap("attr", singletonList("val"))); + Node both = nodeWithVersionAndAttributes(Build.current().version(), singletonMap("attr", singletonList("val"))); Node badVersion = nodeWithVersionAndAttributes("5.1.1", singletonMap("attr", singletonList("val"))); - Node badAttr = nodeWithVersionAndAttributes("5.2.1", singletonMap("notattr", singletonList("val"))); + Node badAttr = nodeWithVersionAndAttributes(Build.current().version(), singletonMap("notattr", singletonList("val"))); List nodes = new ArrayList<>(); nodes.add(both); nodes.add(badVersion); From 30828a56803738f1491794e6eba8487bf4924335 Mon Sep 17 00:00:00 2001 From: Jennie Soria Date: Tue, 5 Mar 2024 05:26:49 -0500 Subject: [PATCH 236/250] Update geoip.asciidoc (#105908) The GeoIP endpoint does not use the xpack http client. The GeoIP downloader uses the JDKs builtin cacerts. If customer is using custom https endpoint they need to provide the cacert in the jdk, whether our jdk bundled in or their jdk. Otherwise they will see something like ``` ...PKiX path building failed: sun.security.provier.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target... ``` --- docs/reference/ingest/processors/geoip.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reference/ingest/processors/geoip.asciidoc b/docs/reference/ingest/processors/geoip.asciidoc index 77572f707f4cb..7e0e53747834a 100644 --- a/docs/reference/ingest/processors/geoip.asciidoc +++ b/docs/reference/ingest/processors/geoip.asciidoc @@ -435,6 +435,8 @@ each node's <> at `$ES_TMPDIR/geoip-databases/>, <>) From d65461d7fbbdfd7d6eecb4cf586d2374be9d0355 Mon Sep 17 00:00:00 2001 From: Mary Gouseti Date: Tue, 5 Mar 2024 12:43:36 +0200 Subject: [PATCH 237/250] [DSL] Refactor effective data retention (#105750) --- .../lifecycle/CrudDataStreamLifecycleIT.java | 4 +- .../ExplainDataStreamLifecycleIT.java | 6 +- .../lifecycle/DataStreamLifecycleService.java | 72 +++++++++---------- .../MetadataIndexTemplateServiceTests.java | 6 +- .../cluster/metadata/DataStream.java | 2 +- .../cluster/metadata/DataStreamLifecycle.java | 10 +++ .../cluster/metadata/DataStreamTests.java | 2 +- ...ataStreamAndIndexLifecycleMixingTests.java | 4 +- 8 files changed, 55 insertions(+), 51 deletions(-) diff --git a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/CrudDataStreamLifecycleIT.java b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/CrudDataStreamLifecycleIT.java index e33b1fdcfa57a..b772e0bb347e2 100644 --- a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/CrudDataStreamLifecycleIT.java +++ b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/CrudDataStreamLifecycleIT.java @@ -164,7 +164,7 @@ public void testPutLifecycle() throws Exception { ).get(); assertThat(response.getDataStreamLifecycles().size(), equalTo(1)); assertThat(response.getDataStreamLifecycles().get(0).dataStreamName(), equalTo("my-data-stream")); - assertThat(response.getDataStreamLifecycles().get(0).lifecycle().getEffectiveDataRetention(), equalTo(dataRetention)); + assertThat(response.getDataStreamLifecycles().get(0).lifecycle().getDataStreamRetention(), equalTo(dataRetention)); assertThat(response.getDataStreamLifecycles().get(0).lifecycle().isEnabled(), equalTo(true)); } @@ -189,7 +189,7 @@ public void testPutLifecycle() throws Exception { ).get(); assertThat(response.getDataStreamLifecycles().size(), equalTo(1)); assertThat(response.getDataStreamLifecycles().get(0).dataStreamName(), equalTo("my-data-stream")); - assertThat(response.getDataStreamLifecycles().get(0).lifecycle().getEffectiveDataRetention(), equalTo(dataRetention)); + assertThat(response.getDataStreamLifecycles().get(0).lifecycle().getDataStreamRetention(), equalTo(dataRetention)); assertThat(response.getDataStreamLifecycles().get(0).lifecycle().isEnabled(), equalTo(false)); } } diff --git a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/ExplainDataStreamLifecycleIT.java b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/ExplainDataStreamLifecycleIT.java index 471622489d9b2..a497eed121b0c 100644 --- a/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/ExplainDataStreamLifecycleIT.java +++ b/modules/data-streams/src/internalClusterTest/java/org/elasticsearch/datastreams/lifecycle/ExplainDataStreamLifecycleIT.java @@ -118,7 +118,7 @@ public void testExplainLifecycle() throws Exception { assertThat(explainIndex.isManagedByLifecycle(), is(true)); assertThat(explainIndex.getIndexCreationDate(), notNullValue()); assertThat(explainIndex.getLifecycle(), notNullValue()); - assertThat(explainIndex.getLifecycle().getEffectiveDataRetention(), nullValue()); + assertThat(explainIndex.getLifecycle().getDataStreamRetention(), nullValue()); if (internalCluster().numDataNodes() > 1) { // If the number of nodes is 1 then the cluster will be yellow so forcemerge will report an error if it has run assertThat(explainIndex.getError(), nullValue()); @@ -175,7 +175,7 @@ public void testExplainLifecycle() throws Exception { assertThat(explainIndex.isManagedByLifecycle(), is(true)); assertThat(explainIndex.getIndexCreationDate(), notNullValue()); assertThat(explainIndex.getLifecycle(), notNullValue()); - assertThat(explainIndex.getLifecycle().getEffectiveDataRetention(), nullValue()); + assertThat(explainIndex.getLifecycle().getDataStreamRetention(), nullValue()); if (explainIndex.getIndex().equals(DataStream.getDefaultBackingIndexName(dataStreamName, 1))) { // first generation index was rolled over @@ -243,7 +243,7 @@ public void testExplainLifecycleForIndicesWithErrors() throws Exception { assertThat(explainIndex.isManagedByLifecycle(), is(true)); assertThat(explainIndex.getIndexCreationDate(), notNullValue()); assertThat(explainIndex.getLifecycle(), notNullValue()); - assertThat(explainIndex.getLifecycle().getEffectiveDataRetention(), nullValue()); + assertThat(explainIndex.getLifecycle().getDataStreamRetention(), nullValue()); assertThat(explainIndex.getRolloverDate(), nullValue()); assertThat(explainIndex.getTimeSinceRollover(System::currentTimeMillis), nullValue()); // index has not been rolled over yet diff --git a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleService.java b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleService.java index 8b15d6a4b7bdf..1b875c28f7f43 100644 --- a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleService.java +++ b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/lifecycle/DataStreamLifecycleService.java @@ -822,38 +822,40 @@ private void maybeExecuteRollover(ClusterState state, DataStream dataStream) { * @return The set of indices that delete requests have been sent for */ private Set maybeExecuteRetention(ClusterState state, DataStream dataStream, Set indicesToExcludeForRemainingRun) { - TimeValue retention = getRetentionConfiguration(dataStream); + Metadata metadata = state.metadata(); + List backingIndicesOlderThanRetention = dataStream.getIndicesPastRetention(metadata::index, nowSupplier); + if (backingIndicesOlderThanRetention.isEmpty()) { + return Set.of(); + } Set indicesToBeRemoved = new HashSet<>(); - if (retention != null) { - Metadata metadata = state.metadata(); - List backingIndicesOlderThanRetention = dataStream.getIndicesPastRetention(metadata::index, nowSupplier); - - for (Index index : backingIndicesOlderThanRetention) { - if (indicesToExcludeForRemainingRun.contains(index) == false) { - IndexMetadata backingIndex = metadata.index(index); - assert backingIndex != null : "the data stream backing indices must exist"; - - IndexMetadata.DownsampleTaskStatus downsampleStatus = INDEX_DOWNSAMPLE_STATUS.get(backingIndex.getSettings()); - // we don't want to delete the source index if they have an in-progress downsampling operation because the - // target downsample index will remain in the system as a standalone index - if (downsampleStatus.equals(UNKNOWN)) { - indicesToBeRemoved.add(index); - - // there's an opportunity here to batch the delete requests (i.e. delete 100 indices / request) - // let's start simple and reevaluate - String indexName = backingIndex.getIndex().getName(); - deleteIndexOnce(indexName, "the lapsed [" + retention + "] retention period"); - } else { - // there's an opportunity here to cancel downsampling and delete the source index now - logger.trace( - "Data stream lifecycle skips deleting index [{}] even though its retention period [{}] has lapsed " - + "because there's a downsampling operation currently in progress for this index. Current downsampling " - + "status is [{}]. When downsampling completes, DSL will delete this index.", - index.getName(), - retention, - downsampleStatus - ); - } + // We know that there is lifecycle and retention because there are indices to be deleted + assert dataStream.getLifecycle() != null; + TimeValue effectiveDataRetention = dataStream.getLifecycle().getEffectiveDataRetention(); + for (Index index : backingIndicesOlderThanRetention) { + if (indicesToExcludeForRemainingRun.contains(index) == false) { + IndexMetadata backingIndex = metadata.index(index); + assert backingIndex != null : "the data stream backing indices must exist"; + + IndexMetadata.DownsampleTaskStatus downsampleStatus = INDEX_DOWNSAMPLE_STATUS.get(backingIndex.getSettings()); + // we don't want to delete the source index if they have an in-progress downsampling operation because the + // target downsample index will remain in the system as a standalone index + if (downsampleStatus.equals(UNKNOWN)) { + indicesToBeRemoved.add(index); + + // there's an opportunity here to batch the delete requests (i.e. delete 100 indices / request) + // let's start simple and reevaluate + String indexName = backingIndex.getIndex().getName(); + deleteIndexOnce(indexName, "the lapsed [" + effectiveDataRetention + "] retention period"); + } else { + // there's an opportunity here to cancel downsampling and delete the source index now + logger.trace( + "Data stream lifecycle skips deleting index [{}] even though its retention period [{}] has lapsed " + + "because there's a downsampling operation currently in progress for this index. Current downsampling " + + "status is [{}]. When downsampling completes, DSL will delete this index.", + index.getName(), + effectiveDataRetention, + downsampleStatus + ); } } } @@ -1222,14 +1224,6 @@ private static boolean isForceMergeComplete(IndexMetadata backingIndex) { return customMetadata != null && customMetadata.containsKey(FORCE_MERGE_COMPLETED_TIMESTAMP_METADATA_KEY); } - @Nullable - static TimeValue getRetentionConfiguration(DataStream dataStream) { - if (dataStream.getLifecycle() == null) { - return null; - } - return dataStream.getLifecycle().getEffectiveDataRetention(); - } - /** * @return the duration of the last run in millis or null if the service hasn't completed a run yet. */ diff --git a/modules/data-streams/src/test/java/org/elasticsearch/datastreams/MetadataIndexTemplateServiceTests.java b/modules/data-streams/src/test/java/org/elasticsearch/datastreams/MetadataIndexTemplateServiceTests.java index e7339cc3f334a..d1e07aacaddce 100644 --- a/modules/data-streams/src/test/java/org/elasticsearch/datastreams/MetadataIndexTemplateServiceTests.java +++ b/modules/data-streams/src/test/java/org/elasticsearch/datastreams/MetadataIndexTemplateServiceTests.java @@ -151,7 +151,7 @@ public void testLifecycleComposition() { DataStreamLifecycle result = composeDataLifecycles(lifecycles); // Defaults to true assertThat(result.isEnabled(), equalTo(true)); - assertThat(result.getEffectiveDataRetention(), equalTo(lifecycle.getEffectiveDataRetention())); + assertThat(result.getDataStreamRetention(), equalTo(lifecycle.getDataStreamRetention())); assertThat(result.getDownsamplingRounds(), equalTo(lifecycle.getDownsamplingRounds())); } // If the last lifecycle is missing a property (apart from enabled) we keep the latest from the previous ones @@ -165,7 +165,7 @@ public void testLifecycleComposition() { List lifecycles = List.of(lifecycle, new DataStreamLifecycle()); DataStreamLifecycle result = composeDataLifecycles(lifecycles); assertThat(result.isEnabled(), equalTo(true)); - assertThat(result.getEffectiveDataRetention(), equalTo(lifecycle.getEffectiveDataRetention())); + assertThat(result.getDataStreamRetention(), equalTo(lifecycle.getDataStreamRetention())); assertThat(result.getDownsamplingRounds(), equalTo(lifecycle.getDownsamplingRounds())); } // If both lifecycle have all properties, then the latest one overwrites all the others @@ -183,7 +183,7 @@ public void testLifecycleComposition() { List lifecycles = List.of(lifecycle1, lifecycle2); DataStreamLifecycle result = composeDataLifecycles(lifecycles); assertThat(result.isEnabled(), equalTo(lifecycle2.isEnabled())); - assertThat(result.getEffectiveDataRetention(), equalTo(lifecycle2.getEffectiveDataRetention())); + assertThat(result.getDataStreamRetention(), equalTo(lifecycle2.getDataStreamRetention())); assertThat(result.getDownsamplingRounds(), equalTo(lifecycle2.getDownsamplingRounds())); } } 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 14de79636be0d..1bcfdba1d16f4 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStream.java @@ -705,7 +705,7 @@ public DataStream snapshot(Collection indicesInSnapshot) { * is treated differently for the write index (i.e. they first need to be rolled over) */ public List getIndicesPastRetention(Function indexMetadataSupplier, LongSupplier nowSupplier) { - if (lifecycle == null || lifecycle.getEffectiveDataRetention() == null) { + if (lifecycle == null || lifecycle.isEnabled() == false || lifecycle.getEffectiveDataRetention() == null) { return List.of(); } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamLifecycle.java b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamLifecycle.java index 215ed515748ab..b4a3a1eb3502a 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamLifecycle.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/DataStreamLifecycle.java @@ -134,6 +134,16 @@ public boolean isEnabled() { */ @Nullable public TimeValue getEffectiveDataRetention() { + return getDataStreamRetention(); + } + + /** + * The least amount of time data the data stream is requesting es to keep the data. + * NOTE: this can be overriden by the {@link DataStreamLifecycle#getEffectiveDataRetention()}. + * @return the time period or null, null represents that data should never be deleted. + */ + @Nullable + public TimeValue getDataStreamRetention() { return dataRetention == null ? null : dataRetention.value; } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java index 1c4cb8c0681ff..9f7d6b49b0844 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/DataStreamTests.java @@ -1249,7 +1249,7 @@ public void testGetIndicesPastRetentionWithOriginationDate() { creationAndRolloverTimes, settings(IndexVersion.current()), new DataStreamLifecycle() { - public TimeValue getEffectiveDataRetention() { + public TimeValue getDataStreamRetention() { return testRetentionReference.get(); } } diff --git a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/DataStreamAndIndexLifecycleMixingTests.java b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/DataStreamAndIndexLifecycleMixingTests.java index 637fbc8f8bf82..b9c58f728d1e3 100644 --- a/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/DataStreamAndIndexLifecycleMixingTests.java +++ b/x-pack/plugin/ilm/src/internalClusterTest/java/org/elasticsearch/xpack/ilm/DataStreamAndIndexLifecycleMixingTests.java @@ -238,7 +238,7 @@ public void testIndexTemplateSwapsILMForDataStreamLifecycle() throws Exception { // let's migrate this data stream to use the custom data stream lifecycle client().execute( PutDataStreamLifecycleAction.INSTANCE, - new PutDataStreamLifecycleAction.Request(new String[] { dataStreamName }, customLifecycle.getEffectiveDataRetention()) + new PutDataStreamLifecycleAction.Request(new String[] { dataStreamName }, customLifecycle.getDataStreamRetention()) ).actionGet(); assertBusy(() -> { @@ -580,7 +580,7 @@ public void testUpdateIndexTemplateToDataStreamLifecyclePreference() throws Exce // let's migrate this data stream to use the custom data stream lifecycle client().execute( PutDataStreamLifecycleAction.INSTANCE, - new PutDataStreamLifecycleAction.Request(new String[] { dataStreamName }, customLifecycle.getEffectiveDataRetention()) + new PutDataStreamLifecycleAction.Request(new String[] { dataStreamName }, customLifecycle.getDataStreamRetention()) ).actionGet(); // data stream was rolled over and has 4 indices, 2 managed by ILM, and 2 managed by the custom data stream lifecycle From 139c94b5852c8435c0877f4ba8e8bdfcd813db7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Tue, 5 Mar 2024 12:33:12 +0100 Subject: [PATCH 238/250] Fix node_selector in new esql test (#105943) --- .../resources/rest-api-spec/test/esql/120_profile.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/120_profile.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/120_profile.yml index 81d87435ad39e..c2e728535a408 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/120_profile.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/120_profile.yml @@ -121,11 +121,10 @@ setup: --- avg 8.14 or after: - skip: - features: ["node_selector"] + version: " - 8.13.99" + reason: "avg changed starting 8.14" - do: - node_selector: - version: "8.13.99 - " esql.query: body: query: 'FROM test | STATS AVG(data) | LIMIT 1' From e9ff896738cba9e0ded5ab1bd6b098075a5b6e67 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 5 Mar 2024 22:53:25 +1100 Subject: [PATCH 239/250] Fix gradle run on Serverless (#105938) On Serverless it is not possible to configure deprecation indexing (it is always off). This commit updates the behaviour of `ElasticsearchCluster` to no longer attempt to configure deprecation indexing on stateless nodes. --- .../gradle/testclusters/ElasticsearchCluster.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchCluster.java b/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchCluster.java index bf539efaf3c30..54962ac241f75 100644 --- a/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchCluster.java +++ b/build-tools/src/main/java/org/elasticsearch/gradle/testclusters/ElasticsearchCluster.java @@ -433,7 +433,7 @@ private void commonNodeConfig() { if (node.getTestDistribution().equals(TestDistribution.INTEG_TEST)) { node.defaultConfig.put("xpack.security.enabled", "false"); } else { - if (node.getVersion().onOrAfter("7.16.0")) { + if (hasDeprecationIndexing(node)) { node.defaultConfig.put("cluster.deprecation_indexing.enabled", "false"); } } @@ -474,13 +474,17 @@ public void nextNodeToNextVersion() { commonNodeConfig(); nodeIndex += 1; if (node.getTestDistribution().equals(TestDistribution.DEFAULT)) { - if (node.getVersion().onOrAfter("7.16.0")) { + if (hasDeprecationIndexing(node)) { node.setting("cluster.deprecation_indexing.enabled", "false"); } } node.start(); } + private static boolean hasDeprecationIndexing(ElasticsearchNode node) { + return node.getVersion().onOrAfter("7.16.0") && node.getSettingKeys().contains("stateless.enabled") == false; + } + @Override public void extraConfigFile(String destination, File from) { nodes.all(node -> node.extraConfigFile(destination, from)); From 1e76b18f7691795fc71ba2285cc413cb9e2d5735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Tue, 5 Mar 2024 13:37:50 +0100 Subject: [PATCH 240/250] YAML test framework: re-introduce `requires` section and `cluster_features` conditions (#105763) --- .../yaml/section/ClientYamlTestSuite.java | 23 ++--- .../yaml/section/PrerequisiteSection.java | 97 ++++++++++++++++++- .../section/ClientYamlTestSuiteTests.java | 67 ++++++++++--- .../section/PrerequisiteSectionTests.java | 93 +++++++++++++++++- 4 files changed, 252 insertions(+), 28 deletions(-) diff --git a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSuite.java b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSuite.java index 65a23bd376212..e5f46ff135171 100644 --- a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSuite.java +++ b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSuite.java @@ -177,7 +177,7 @@ private static Stream validateExecutableSections( .filter(section -> false == section.getExpectedWarningHeaders().isEmpty()) .filter(section -> false == hasYamlRunnerFeature("warnings", testSection, setupSection, teardownSection)) .map(section -> String.format(Locale.ROOT, """ - attempted to add a [do] with a [warnings] section without a corresponding ["skip": "features": "warnings"] \ + attempted to add a [do] with a [warnings] section without a corresponding ["requires": "test_runner_features": "warnings"] \ so runners that do not support the [warnings] section can skip the test at line [%d]\ """, section.getLocation().lineNumber())); @@ -190,7 +190,7 @@ private static Stream validateExecutableSections( .filter(section -> false == hasYamlRunnerFeature("warnings_regex", testSection, setupSection, teardownSection)) .map(section -> String.format(Locale.ROOT, """ attempted to add a [do] with a [warnings_regex] section without a corresponding \ - ["skip": "features": "warnings_regex"] so runners that do not support the [warnings_regex] \ + ["requires": "test_runner_features": "warnings_regex"] so runners that do not support the [warnings_regex] \ section can skip the test at line [%d]\ """, section.getLocation().lineNumber())) ); @@ -204,7 +204,7 @@ private static Stream validateExecutableSections( .filter(section -> false == hasYamlRunnerFeature("allowed_warnings", testSection, setupSection, teardownSection)) .map(section -> String.format(Locale.ROOT, """ attempted to add a [do] with a [allowed_warnings] section without a corresponding \ - ["skip": "features": "allowed_warnings"] so runners that do not support the [allowed_warnings] \ + ["requires": "test_runner_features": "allowed_warnings"] so runners that do not support the [allowed_warnings] \ section can skip the test at line [%d]\ """, section.getLocation().lineNumber())) ); @@ -218,8 +218,8 @@ private static Stream validateExecutableSections( .filter(section -> false == hasYamlRunnerFeature("allowed_warnings_regex", testSection, setupSection, teardownSection)) .map(section -> String.format(Locale.ROOT, """ attempted to add a [do] with a [allowed_warnings_regex] section without a corresponding \ - ["skip": "features": "allowed_warnings_regex"] so runners that do not support the [allowed_warnings_regex] \ - section can skip the test at line [%d]\ + ["requires": "test_runner_features": "allowed_warnings_regex"] so runners that do not support the \ + [allowed_warnings_regex] section can skip the test at line [%d]\ """, section.getLocation().lineNumber())) ); @@ -232,7 +232,7 @@ private static Stream validateExecutableSections( .filter(section -> false == hasYamlRunnerFeature("node_selector", testSection, setupSection, teardownSection)) .map(section -> String.format(Locale.ROOT, """ attempted to add a [do] with a [node_selector] section without a corresponding \ - ["skip": "features": "node_selector"] so runners that do not support the [node_selector] section \ + ["requires": "test_runner_features": "node_selector"] so runners that do not support the [node_selector] section \ can skip the test at line [%d]\ """, section.getLocation().lineNumber())) ); @@ -243,7 +243,7 @@ private static Stream validateExecutableSections( .filter(section -> section instanceof ContainsAssertion) .filter(section -> false == hasYamlRunnerFeature("contains", testSection, setupSection, teardownSection)) .map(section -> String.format(Locale.ROOT, """ - attempted to add a [contains] assertion without a corresponding ["skip": "features": "contains"] \ + attempted to add a [contains] assertion without a corresponding ["requires": "test_runner_features": "contains"] \ so runners that do not support the [contains] assertion can skip the test at line [%d]\ """, section.getLocation().lineNumber())) ); @@ -256,8 +256,9 @@ private static Stream validateExecutableSections( .filter(section -> false == section.getApiCallSection().getHeaders().isEmpty()) .filter(section -> false == hasYamlRunnerFeature("headers", testSection, setupSection, teardownSection)) .map(section -> String.format(Locale.ROOT, """ - attempted to add a [do] with a [headers] section without a corresponding ["skip": "features": "headers"] \ - so runners that do not support the [headers] section can skip the test at line [%d]\ + attempted to add a [do] with a [headers] section without a corresponding \ + ["requires": "test_runner_features": "headers"] so runners that do not support the [headers] section \ + can skip the test at line [%d]\ """, section.getLocation().lineNumber())) ); @@ -267,7 +268,7 @@ private static Stream validateExecutableSections( .filter(section -> section instanceof CloseToAssertion) .filter(section -> false == hasYamlRunnerFeature("close_to", testSection, setupSection, teardownSection)) .map(section -> String.format(Locale.ROOT, """ - attempted to add a [close_to] assertion without a corresponding ["skip": "features": "close_to"] \ + attempted to add a [close_to] assertion without a corresponding ["requires": "test_runner_features": "close_to"] \ so runners that do not support the [close_to] assertion can skip the test at line [%d]\ """, section.getLocation().lineNumber())) ); @@ -278,7 +279,7 @@ private static Stream validateExecutableSections( .filter(section -> section instanceof IsAfterAssertion) .filter(section -> false == hasYamlRunnerFeature("is_after", testSection, setupSection, teardownSection)) .map(section -> String.format(Locale.ROOT, """ - attempted to add an [is_after] assertion without a corresponding ["skip": "features": "is_after"] \ + attempted to add an [is_after] assertion without a corresponding ["requires": "test_runner_features": "is_after"] \ so runners that do not support the [is_after] assertion can skip the test at line [%d]\ """, section.getLocation().lineNumber())) ); diff --git a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/PrerequisiteSection.java b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/PrerequisiteSection.java index 7f65a29e510b6..f4c9aaa619911 100644 --- a/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/PrerequisiteSection.java +++ b/test/yaml-rest-runner/src/main/java/org/elasticsearch/test/rest/yaml/section/PrerequisiteSection.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.test.rest.yaml.ClientYamlTestExecutionContext; import org.elasticsearch.test.rest.yaml.Features; import org.elasticsearch.xcontent.XContentLocation; @@ -17,7 +18,9 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Predicate; /** @@ -34,9 +37,13 @@ public class PrerequisiteSection { static class PrerequisiteSectionBuilder { String skipVersionRange = null; String skipReason = null; + String requiresReason = null; List requiredYamlRunnerFeatures = new ArrayList<>(); List skipOperatingSystems = new ArrayList<>(); + Set skipClusterFeatures = new HashSet<>(); + Set requiredClusterFeatures = new HashSet<>(); + enum XPackRequired { NOT_SPECIFIED, YES, @@ -56,6 +63,11 @@ public PrerequisiteSectionBuilder setSkipReason(String skipReason) { return this; } + public PrerequisiteSectionBuilder setRequiresReason(String requiresReason) { + this.requiresReason = requiresReason; + return this; + } + public PrerequisiteSectionBuilder requireYamlRunnerFeature(String featureName) { requiredYamlRunnerFeatures.add(featureName); return this; @@ -79,6 +91,16 @@ public PrerequisiteSectionBuilder skipIfXPack() { return this; } + public PrerequisiteSectionBuilder skipIfClusterFeature(String featureName) { + skipClusterFeatures.add(featureName); + return this; + } + + public PrerequisiteSectionBuilder requireClusterFeature(String featureName) { + requiredClusterFeatures.add(featureName); + return this; + } + public PrerequisiteSectionBuilder skipIfOs(String osName) { this.skipOperatingSystems.add(osName); return this; @@ -88,7 +110,9 @@ void validate(XContentLocation contentLocation) { if ((Strings.hasLength(skipVersionRange) == false) && requiredYamlRunnerFeatures.isEmpty() && skipOperatingSystems.isEmpty() - && xpackRequired == XPackRequired.NOT_SPECIFIED) { + && xpackRequired == XPackRequired.NOT_SPECIFIED + && requiredClusterFeatures.isEmpty() + && skipClusterFeatures.isEmpty()) { throw new ParsingException( contentLocation, "at least one criteria (version, cluster features, runner features, os) is mandatory within a skip section" @@ -100,6 +124,12 @@ void validate(XContentLocation contentLocation) { if (skipOperatingSystems.isEmpty() == false && Strings.hasLength(skipReason) == false) { throw new ParsingException(contentLocation, "reason is mandatory within skip os section"); } + if (skipClusterFeatures.isEmpty() == false && Strings.hasLength(skipReason) == false) { + throw new ParsingException(contentLocation, "reason is mandatory within skip cluster_features section"); + } + if (requiredClusterFeatures.isEmpty() == false && Strings.hasLength(requiresReason) == false) { + throw new ParsingException(contentLocation, "reason is mandatory within requires cluster_features section"); + } // make feature "skip_os" mandatory if os is given, this is a temporary solution until language client tests know about os if (skipOperatingSystems.isEmpty() == false && requiredYamlRunnerFeatures.contains("skip_os") == false) { throw new ParsingException(contentLocation, "if os is specified, test runner feature [skip_os] must be set"); @@ -107,6 +137,9 @@ void validate(XContentLocation contentLocation) { if (xpackRequired == XPackRequired.MISMATCHED) { throw new ParsingException(contentLocation, "either [xpack] or [no_xpack] can be present, not both"); } + if (Sets.haveNonEmptyIntersection(skipClusterFeatures, requiredClusterFeatures)) { + throw new ParsingException(contentLocation, "a cluster feature can be specified either in [requires] or [skip], not both"); + } } public PrerequisiteSection build() { @@ -131,8 +164,14 @@ public PrerequisiteSection build() { if (skipOperatingSystems.isEmpty() == false) { skipCriteriaList.add(Prerequisites.skipOnOsList(skipOperatingSystems)); } + if (requiredClusterFeatures.isEmpty() == false) { + requiresCriteriaList.add(Prerequisites.requireClusterFeatures(requiredClusterFeatures)); + } + if (skipClusterFeatures.isEmpty() == false) { + skipCriteriaList.add(Prerequisites.skipOnClusterFeatures(skipClusterFeatures)); + } } - return new PrerequisiteSection(skipCriteriaList, skipReason, requiresCriteriaList, null, requiredYamlRunnerFeatures); + return new PrerequisiteSection(skipCriteriaList, skipReason, requiresCriteriaList, requiresReason, requiredYamlRunnerFeatures); } } @@ -160,6 +199,10 @@ static PrerequisiteSectionBuilder parseInternal(XContentParser parser) throws IO parseSkipSection(parser, builder); hasPrerequisiteSection = true; maybeAdvanceToNextField(parser); + } else if ("requires".equals(parser.currentName())) { + parseRequiresSection(parser, builder); + hasPrerequisiteSection = true; + maybeAdvanceToNextField(parser); } else { unknownFieldName = true; } @@ -209,6 +252,8 @@ static void parseSkipSection(XContentParser parser, PrerequisiteSectionBuilder b parseFeatureField(parser.text(), builder); } else if ("os".equals(currentFieldName)) { builder.skipIfOs(parser.text()); + } else if ("cluster_features".equals(currentFieldName)) { + builder.skipIfClusterFeature(parser.text()); } else { throw new ParsingException( parser.getTokenLocation(), @@ -224,6 +269,54 @@ static void parseSkipSection(XContentParser parser, PrerequisiteSectionBuilder b while (parser.nextToken() != XContentParser.Token.END_ARRAY) { builder.skipIfOs(parser.text()); } + } else if ("cluster_features".equals(currentFieldName)) { + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + builder.skipIfClusterFeature(parser.text()); + } + } + } + } + parser.nextToken(); + } + + static void parseRequiresSection(XContentParser parser, PrerequisiteSectionBuilder builder) throws IOException { + if (parser.nextToken() != XContentParser.Token.START_OBJECT) { + throw new IllegalArgumentException( + "Expected [" + + XContentParser.Token.START_OBJECT + + ", found [" + + parser.currentToken() + + "], the requires section is not properly indented" + ); + } + String currentFieldName = null; + XContentParser.Token token; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token.isValue()) { + if ("reason".equals(currentFieldName)) { + builder.setRequiresReason(parser.text()); + } else if ("test_runner_features".equals(currentFieldName)) { + parseFeatureField(parser.text(), builder); + } else if ("cluster_features".equals(currentFieldName)) { + builder.requireClusterFeature(parser.text()); + } else { + throw new ParsingException( + parser.getTokenLocation(), + "field " + currentFieldName + " not supported within requires section" + ); + } + } else if (token == XContentParser.Token.START_ARRAY) { + if ("test_runner_features".equals(currentFieldName)) { + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + parseFeatureField(parser.text(), builder); + } + } else if ("cluster_features".equals(currentFieldName)) { + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + builder.requireClusterFeature(parser.text()); + } } } } diff --git a/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSuiteTests.java b/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSuiteTests.java index edc043e15527d..1f5bdc71dde37 100644 --- a/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSuiteTests.java +++ b/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/ClientYamlTestSuiteTests.java @@ -468,6 +468,41 @@ public void testParseSkipOs() throws Exception { assertThat(restTestSuite.getTestSections().get(0).getPrerequisiteSection().hasYamlRunnerFeature("skip_os"), equalTo(true)); } + public void testParseSkipAndRequireClusterFeatures() throws Exception { + parser = createParser(YamlXContent.yamlXContent, """ + "Broken on some os": + + - skip: + cluster_features: [unsupported-feature1, unsupported-feature2] + reason: "unsupported-features are not supported" + - requires: + cluster_features: required-feature1 + reason: "required-feature1 is required" + - do: + indices.get_mapping: + index: test_index + type: test_type + + - match: {test_type.properties.text.type: string} + - match: {test_type.properties.text.analyzer: whitespace} + """); + + ClientYamlTestSuite restTestSuite = ClientYamlTestSuite.parse(getTestClass().getName(), getTestName(), Optional.empty(), parser); + + assertThat(restTestSuite, notNullValue()); + assertThat(restTestSuite.getName(), equalTo(getTestName())); + assertThat(restTestSuite.getFile().isPresent(), equalTo(false)); + assertThat(restTestSuite.getTestSections().size(), equalTo(1)); + + assertThat(restTestSuite.getTestSections().get(0).getName(), equalTo("Broken on some os")); + assertThat(restTestSuite.getTestSections().get(0).getPrerequisiteSection().isEmpty(), equalTo(false)); + assertThat( + restTestSuite.getTestSections().get(0).getPrerequisiteSection().skipReason, + equalTo("unsupported-features are not supported") + ); + assertThat(restTestSuite.getTestSections().get(0).getPrerequisiteSection().requireReason, equalTo("required-feature1 is required")); + } + public void testParseFileWithSingleTestSection() throws Exception { final Path filePath = createTempFile("tyf", ".yml"); Files.writeString(filePath, """ @@ -541,7 +576,7 @@ public void testAddingDoWithWarningWithoutSkipWarnings() { Exception e = expectThrows(IllegalArgumentException.class, testSuite::validate); assertThat(e.getMessage(), containsString(Strings.format(""" api/name: - attempted to add a [do] with a [warnings] section without a corresponding ["skip": "features": "warnings"] \ + attempted to add a [do] with a [warnings] section without a corresponding ["requires": "test_runner_features": "warnings"] \ so runners that do not support the [warnings] section can skip the test at line [%d]\ """, lineNumber))); } @@ -555,7 +590,8 @@ public void testAddingDoWithWarningRegexWithoutSkipWarnings() { Exception e = expectThrows(IllegalArgumentException.class, testSuite::validate); assertThat(e.getMessage(), containsString(Strings.format(""" api/name: - attempted to add a [do] with a [warnings_regex] section without a corresponding ["skip": "features": "warnings_regex"] \ + attempted to add a [do] with a [warnings_regex] section without a corresponding \ + ["requires": "test_runner_features": "warnings_regex"] \ so runners that do not support the [warnings_regex] section can skip the test at line [%d]\ """, lineNumber))); } @@ -569,7 +605,7 @@ public void testAddingDoWithAllowedWarningWithoutSkipAllowedWarnings() { Exception e = expectThrows(IllegalArgumentException.class, testSuite::validate); assertThat(e.getMessage(), containsString(Strings.format(""" api/name: - attempted to add a [do] with a [allowed_warnings] section without a corresponding ["skip": "features": \ + attempted to add a [do] with a [allowed_warnings] section without a corresponding ["requires": "test_runner_features": \ "allowed_warnings"] so runners that do not support the [allowed_warnings] section can skip the test at \ line [%d]\ """, lineNumber))); @@ -584,7 +620,7 @@ public void testAddingDoWithAllowedWarningRegexWithoutSkipAllowedWarnings() { Exception e = expectThrows(IllegalArgumentException.class, testSuite::validate); assertThat(e.getMessage(), containsString(Strings.format(""" api/name: - attempted to add a [do] with a [allowed_warnings_regex] section without a corresponding ["skip": "features": \ + attempted to add a [do] with a [allowed_warnings_regex] section without a corresponding ["requires": "test_runner_features": \ "allowed_warnings_regex"] so runners that do not support the [allowed_warnings_regex] section can skip the test \ at line [%d]\ """, lineNumber))); @@ -600,7 +636,7 @@ public void testAddingDoWithHeaderWithoutSkipHeaders() { Exception e = expectThrows(IllegalArgumentException.class, testSuite::validate); assertThat(e.getMessage(), containsString(Strings.format(""" api/name: - attempted to add a [do] with a [headers] section without a corresponding ["skip": "features": "headers"] \ + attempted to add a [do] with a [headers] section without a corresponding ["requires": "test_runner_features": "headers"] \ so runners that do not support the [headers] section can skip the test at line [%d]\ """, lineNumber))); } @@ -615,7 +651,8 @@ public void testAddingDoWithNodeSelectorWithoutSkipNodeSelector() { Exception e = expectThrows(IllegalArgumentException.class, testSuite::validate); assertThat(e.getMessage(), containsString(Strings.format(""" api/name: - attempted to add a [do] with a [node_selector] section without a corresponding ["skip": "features": "node_selector"] \ + attempted to add a [do] with a [node_selector] section without a corresponding \ + ["requires": "test_runner_features": "node_selector"] \ so runners that do not support the [node_selector] section can skip the test at line [%d]\ """, lineNumber))); } @@ -631,7 +668,7 @@ public void testAddingContainsWithoutSkipContains() { Exception e = expectThrows(IllegalArgumentException.class, testSuite::validate); assertThat(e.getMessage(), containsString(Strings.format(""" api/name: - attempted to add a [contains] assertion without a corresponding ["skip": "features": "contains"] \ + attempted to add a [contains] assertion without a corresponding ["requires": "test_runner_features": "contains"] \ so runners that do not support the [contains] assertion can skip the test at line [%d]\ """, lineNumber))); } @@ -683,13 +720,15 @@ public void testMultipleValidationErrors() { Exception e = expectThrows(IllegalArgumentException.class, testSuite::validate); assertEquals(Strings.format(""" api/name: - attempted to add a [contains] assertion without a corresponding ["skip": "features": "contains"] so runners that \ - do not support the [contains] assertion can skip the test at line [%d], - attempted to add a [do] with a [warnings] section without a corresponding ["skip": "features": "warnings"] so runners \ - that do not support the [warnings] section can skip the test at line [%d], - attempted to add a [do] with a [node_selector] section without a corresponding ["skip": "features": "node_selector"] so \ - runners that do not support the [node_selector] section can skip the test \ - at line [%d]\ + attempted to add a [contains] assertion without a corresponding \ + ["requires": "test_runner_features": "contains"] \ + so runners that do not support the [contains] assertion can skip the test at line [%d], + attempted to add a [do] with a [warnings] section without a corresponding \ + ["requires": "test_runner_features": "warnings"] \ + so runners that do not support the [warnings] section can skip the test at line [%d], + attempted to add a [do] with a [node_selector] section without a corresponding \ + ["requires": "test_runner_features": "node_selector"] \ + so runners that do not support the [node_selector] section can skip the test at line [%d]\ """, firstLineNumber, secondLineNumber, thirdLineNumber), e.getMessage()); } diff --git a/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/PrerequisiteSectionTests.java b/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/PrerequisiteSectionTests.java index b02658694d82f..181ec34fefb7e 100644 --- a/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/PrerequisiteSectionTests.java +++ b/test/yaml-rest-runner/src/test/java/org/elasticsearch/test/rest/yaml/section/PrerequisiteSectionTests.java @@ -363,8 +363,10 @@ public void testParseSkipSectionOsListNoVersion() throws Exception { public void testParseSkipSectionOsListTestFeaturesInRequires() throws Exception { parser = createParser(YamlXContent.yamlXContent, """ + - requires: + test_runner_features: skip_os + reason: skip_os is needed for skip based on os - skip: - features: [skip_os] os: [debian-9,windows-95,ms-dos] reason: see gh#xyz """); @@ -391,6 +393,95 @@ public void testParseSkipSectionOsNoFeatureNoVersion() throws Exception { assertThat(e.getMessage(), is("if os is specified, test runner feature [skip_os] must be set")); } + public void testParseRequireSectionClusterFeatures() throws Exception { + parser = createParser(YamlXContent.yamlXContent, """ + cluster_features: needed-feature + reason: test skipped when cluster lacks needed-feature + """); + + var skipSectionBuilder = new PrerequisiteSection.PrerequisiteSectionBuilder(); + PrerequisiteSection.parseRequiresSection(parser, skipSectionBuilder); + assertThat(skipSectionBuilder, notNullValue()); + assertThat(skipSectionBuilder.skipVersionRange, emptyOrNullString()); + assertThat(skipSectionBuilder.requiredClusterFeatures, contains("needed-feature")); + assertThat(skipSectionBuilder.requiresReason, is("test skipped when cluster lacks needed-feature")); + } + + public void testParseSkipSectionClusterFeatures() throws Exception { + parser = createParser(YamlXContent.yamlXContent, """ + cluster_features: undesired-feature + reason: test skipped when undesired-feature is present + """); + + var skipSectionBuilder = new PrerequisiteSection.PrerequisiteSectionBuilder(); + PrerequisiteSection.parseSkipSection(parser, skipSectionBuilder); + assertThat(skipSectionBuilder, notNullValue()); + assertThat(skipSectionBuilder.skipVersionRange, emptyOrNullString()); + assertThat(skipSectionBuilder.skipClusterFeatures, contains("undesired-feature")); + assertThat(skipSectionBuilder.skipReason, is("test skipped when undesired-feature is present")); + } + + public void testParseRequireAndSkipSectionsClusterFeatures() throws Exception { + parser = createParser(YamlXContent.yamlXContent, """ + - requires: + cluster_features: needed-feature + reason: test needs needed-feature to run + - skip: + cluster_features: undesired-feature + reason: test cannot run when undesired-feature are present + """); + + var skipSectionBuilder = PrerequisiteSection.parseInternal(parser); + assertThat(skipSectionBuilder, notNullValue()); + assertThat(skipSectionBuilder.skipVersionRange, emptyOrNullString()); + assertThat(skipSectionBuilder.skipClusterFeatures, contains("undesired-feature")); + assertThat(skipSectionBuilder.requiredClusterFeatures, contains("needed-feature")); + assertThat(skipSectionBuilder.skipReason, is("test cannot run when undesired-feature are present")); + assertThat(skipSectionBuilder.requiresReason, is("test needs needed-feature to run")); + + assertThat(parser.currentToken(), equalTo(XContentParser.Token.END_ARRAY)); + assertThat(parser.nextToken(), nullValue()); + } + + public void testParseRequireAndSkipSectionMultipleClusterFeatures() throws Exception { + parser = createParser(YamlXContent.yamlXContent, """ + - requires: + cluster_features: [needed-feature-1, needed-feature-2] + reason: test needs some to run + - skip: + cluster_features: [undesired-feature-1, undesired-feature-2] + reason: test cannot run when some are present + """); + + var skipSectionBuilder = PrerequisiteSection.parseInternal(parser); + assertThat(skipSectionBuilder, notNullValue()); + assertThat(skipSectionBuilder.skipVersionRange, emptyOrNullString()); + assertThat(skipSectionBuilder.skipClusterFeatures, containsInAnyOrder("undesired-feature-1", "undesired-feature-2")); + assertThat(skipSectionBuilder.requiredClusterFeatures, containsInAnyOrder("needed-feature-1", "needed-feature-2")); + assertThat(skipSectionBuilder.skipReason, is("test cannot run when some are present")); + assertThat(skipSectionBuilder.requiresReason, is("test needs some to run")); + + assertThat(parser.currentToken(), equalTo(XContentParser.Token.END_ARRAY)); + assertThat(parser.nextToken(), nullValue()); + } + + public void testParseSameRequireAndSkipClusterFeatures() throws Exception { + parser = createParser(YamlXContent.yamlXContent, """ + - requires: + cluster_features: some-feature + reason: test needs some-feature to run + - skip: + cluster_features: some-feature + reason: test cannot run with some-feature + """); + + var e = expectThrows(ParsingException.class, () -> PrerequisiteSection.parseInternal(parser)); + assertThat(e.getMessage(), is("a cluster feature can be specified either in [requires] or [skip], not both")); + + assertThat(parser.currentToken(), equalTo(XContentParser.Token.END_ARRAY)); + assertThat(parser.nextToken(), nullValue()); + } + public void testSkipClusterFeaturesAllRequiredMatch() { PrerequisiteSection section = new PrerequisiteSection( emptyList(), From 7191758ac1a801d0ac0157e98437560b43ad181c Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Tue, 5 Mar 2024 13:55:24 +0100 Subject: [PATCH 241/250] Unmute testRollupNonTSIndex() and (#105949) increase ILM logging to TRACE. Relates to #103981 --- .../elasticsearch/xpack/ilm/actions/DownsampleActionIT.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/DownsampleActionIT.java b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/DownsampleActionIT.java index ec9fad3e5077d..6d34fb0eced79 100644 --- a/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/DownsampleActionIT.java +++ b/x-pack/plugin/ilm/qa/multi-node/src/javaRestTest/java/org/elasticsearch/xpack/ilm/actions/DownsampleActionIT.java @@ -23,6 +23,7 @@ import org.elasticsearch.index.IndexSettings; import org.elasticsearch.rest.action.admin.indices.RestPutIndexTemplateAction; import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; +import org.elasticsearch.test.junit.annotations.TestLogging; import org.elasticsearch.test.rest.ESRestTestCase; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; @@ -395,7 +396,7 @@ public void testILMWaitsForTimeSeriesEndTimeToLapse() throws Exception { }, 30, TimeUnit.SECONDS); } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/103981") + @TestLogging(value = "org.elasticsearch.xpack.ilm:TRACE", reason = "https://github.com/elastic/elasticsearch/issues/103981") public void testRollupNonTSIndex() throws Exception { createIndex(index, alias, false); index(client(), index, true, null, "@timestamp", "2020-01-01T05:10:00Z", "volume", 11.0, "metricset", randomAlphaOfLength(5)); From 4f2c8ca0789cf34cffb9357d4b29810d7635be91 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Tue, 5 Mar 2024 07:56:42 -0500 Subject: [PATCH 242/250] Test mute for #105952 (#105953) mute for https://github.com/elastic/elasticsearch/issues/105952 --- .../xpack/esql/querydsl/query/SingleValueQueryTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/SingleValueQueryTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/SingleValueQueryTests.java index 1d62bc0b6eaaa..55e8ba164ba70 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/SingleValueQueryTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/querydsl/query/SingleValueQueryTests.java @@ -77,6 +77,7 @@ public void testMatchAll() throws IOException { testCase(new SingleValueQuery(new MatchAll(Source.EMPTY), "foo").asBuilder(), false, false, this::runCase); } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/105952") public void testMatchSome() throws IOException { int max = between(1, 100); testCase( From 61b3d98227c98513fa4788e8425095a27e2dba9b Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Tue, 5 Mar 2024 08:44:03 -0500 Subject: [PATCH 243/250] Add note about optional times and epochs (#105786) --- docs/reference/mapping/params/format.asciidoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/reference/mapping/params/format.asciidoc b/docs/reference/mapping/params/format.asciidoc index dff7bb4a11ee4..5babb4def2320 100644 --- a/docs/reference/mapping/params/format.asciidoc +++ b/docs/reference/mapping/params/format.asciidoc @@ -70,6 +70,11 @@ The following tables lists all the defaults ISO formats supported: (separated by `T`), is optional. Examples: `yyyy-MM-dd'T'HH:mm:ss.SSSZ` or `yyyy-MM-dd`. + NOTE: When using `date_optional_time`, the parsing is lenient and will attempt to parse + numbers as a year (e.g. `292278994` will be parsed as a year). This can lead to unexpected results + when paired with a numeric focused format like `epoch_second` and `epoch_millis`. + It is recommended you use `strict_date_optional_time` when pairing with a numeric focused format. + [[strict-date-time-nanos]]`strict_date_optional_time_nanos`:: A generic ISO datetime parser, where the date must include the year at a minimum, and the time From fbbfbd5503f6cf1b6da472271d22588d48165b99 Mon Sep 17 00:00:00 2001 From: Artem Prigoda Date: Tue, 5 Mar 2024 14:59:45 +0100 Subject: [PATCH 244/250] [test] Disable index.shard.check_on_startup for searchable snapshot tests (#105731) This setting requires expensive processing due to verification the integrity of many important files during a shard recovery or relocation. Therefore, it takes lots of time for the files to clean up and the assertShardFolder check may not complete in 30s. Fixes #105202 --- .../FrozenSearchableSnapshotsIntegTests.java | 5 ++++- .../searchablesnapshots/SearchableSnapshotsIntegTests.java | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/FrozenSearchableSnapshotsIntegTests.java b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/FrozenSearchableSnapshotsIntegTests.java index 18b4e6ed7cb31..4b9e1b0d9211e 100644 --- a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/FrozenSearchableSnapshotsIntegTests.java +++ b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/FrozenSearchableSnapshotsIntegTests.java @@ -102,7 +102,10 @@ public void testCreateAndRestorePartialSearchableSnapshot() throws Exception { // we can bypass this by forcing soft deletes to be used. TODO this restriction can be lifted when #55142 is resolved. final Settings.Builder originalIndexSettings = Settings.builder().put(INDEX_SOFT_DELETES_SETTING.getKey(), true); if (randomBoolean()) { - originalIndexSettings.put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), randomFrom("false", "true", "checksum")); + // INDEX_CHECK_ON_STARTUP requires expensive processing due to verification the integrity of many important files during + // a shard recovery or relocation. Therefore, it takes lots of time for the files to clean up and the assertShardFolder + // check may not complete in 30s. + originalIndexSettings.put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), "false"); } assertAcked(prepareCreate(indexName, originalIndexSettings)); assertAcked(indicesAdmin().prepareAliases().addAlias(indexName, aliasName)); diff --git a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsIntegTests.java b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsIntegTests.java index 38222f64b282b..ddd9f40b5404c 100644 --- a/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsIntegTests.java +++ b/x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsIntegTests.java @@ -111,7 +111,7 @@ public void testCreateAndRestoreSearchableSnapshot() throws Exception { // we can bypass this by forcing soft deletes to be used. TODO this restriction can be lifted when #55142 is resolved. final Settings.Builder originalIndexSettings = Settings.builder().put(INDEX_SOFT_DELETES_SETTING.getKey(), true); if (randomBoolean()) { - originalIndexSettings.put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), randomFrom("false", "true", "checksum")); + originalIndexSettings.put(IndexSettings.INDEX_CHECK_ON_STARTUP.getKey(), "false"); } assertAcked(prepareCreate(indexName, originalIndexSettings)); assertAcked(indicesAdmin().prepareAliases().addAlias(indexName, aliasName)); From 7c6120b50f03088d4fdac0e81994d991c54d20a7 Mon Sep 17 00:00:00 2001 From: David Turner Date: Tue, 5 Mar 2024 14:06:53 +0000 Subject: [PATCH 245/250] Fix TransportSLMGetExpiredSnapshotsActionTests (#105950) Addresses test bug introduced in #105721: we must consume all the `SnapshotInfo` instances before completing the final listener. Closes #105922 --- ...portSLMGetExpiredSnapshotsActionTests.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/TransportSLMGetExpiredSnapshotsActionTests.java b/x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/TransportSLMGetExpiredSnapshotsActionTests.java index 573edc6e517bf..e6d7a66a2bdb3 100644 --- a/x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/TransportSLMGetExpiredSnapshotsActionTests.java +++ b/x-pack/plugin/slm/src/test/java/org/elasticsearch/xpack/slm/TransportSLMGetExpiredSnapshotsActionTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.action.support.RefCountingRunnable; import org.elasticsearch.action.support.SubscribableListener; import org.elasticsearch.cluster.metadata.RepositoryMetadata; import org.elasticsearch.common.settings.Settings; @@ -286,7 +287,7 @@ private static Repository createMockRepository(ThreadPool threadPool, List consumer = invocation.getArgument(3); final ActionListener listener = invocation.getArgument(4); - final Set snapshotIds = new HashSet<>(snapshotIdCollection); - for (SnapshotInfo snapshotInfo : snapshotInfos) { - if (snapshotIds.remove(snapshotInfo.snapshotId())) { - threadPool.generic().execute(() -> { - try { - consumer.accept(snapshotInfo); - } catch (Exception e) { - fail(e); - } - }); + try (var refs = new RefCountingRunnable(() -> listener.onResponse(null))) { + final Set snapshotIds = new HashSet<>(snapshotIdCollection); + for (SnapshotInfo snapshotInfo : snapshotInfos) { + if (snapshotIds.remove(snapshotInfo.snapshotId())) { + threadPool.generic().execute(ActionRunnable.run(refs.acquireListener(), () -> { + try { + consumer.accept(snapshotInfo); + } catch (Exception e) { + fail(e); + } + })); + } } } - listener.onResponse(null); return null; }).when(repository).getSnapshotInfo(any(), anyBoolean(), any(), any(), any()); From 335afe5b882fd06d31e991d8fe4357acf5e0bd3d Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Tue, 5 Mar 2024 16:06:20 +0000 Subject: [PATCH 246/250] Fix performance bug in `SourceConfirmedTextQuery#matches` (#105930) This change ensures that the matches implementation of the `SourceConfirmedTextQuery` only checks the current document instead of calling advance on the two phase iterator. The latter tries to find the first doc that matches the query instead of restricting the search to the current doc. This can lead to abnormally slow highlighting if the query is very restrictive and the highlight is done on a non-matching document. Closes #103298 --- .../extras/SourceConfirmedTextQuery.java | 62 ++++++++-------- .../extras/SourceConfirmedTextQueryTests.java | 71 +++++++++++++------ 2 files changed, 80 insertions(+), 53 deletions(-) diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/SourceConfirmedTextQuery.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/SourceConfirmedTextQuery.java index dc51afe5d420d..3d0f26e8cc130 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/SourceConfirmedTextQuery.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/extras/SourceConfirmedTextQuery.java @@ -9,9 +9,7 @@ package org.elasticsearch.index.mapper.extras; import org.apache.lucene.analysis.Analyzer; -import org.apache.lucene.index.FieldInfo; import org.apache.lucene.index.FieldInvertState; -import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.Term; import org.apache.lucene.index.TermStates; @@ -300,19 +298,23 @@ public RuntimePhraseScorer scorer(LeafReaderContext context) throws IOException @Override public Matches matches(LeafReaderContext context, int doc) throws IOException { - FieldInfo fi = context.reader().getFieldInfos().fieldInfo(field); - if (fi == null) { + var terms = context.reader().terms(field); + if (terms == null) { return null; } - // Some highlighters will already have reindexed the source with positions and offsets, + // Some highlighters will already have re-indexed the source with positions and offsets, // so rather than doing it again we check to see if this data is available on the // current context and if so delegate directly to the inner query - if (fi.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS) > 0) { + if (terms.hasOffsets()) { Weight innerWeight = in.createWeight(searcher, ScoreMode.COMPLETE_NO_SCORES, 1); return innerWeight.matches(context, doc); } RuntimePhraseScorer scorer = scorer(context); - if (scorer == null || scorer.iterator().advance(doc) != doc) { + if (scorer == null) { + return null; + } + final TwoPhaseIterator twoPhase = scorer.twoPhaseIterator(); + if (twoPhase.approximation().advance(doc) != doc || scorer.twoPhaseIterator().matches() == false) { return null; } return scorer.matches(); @@ -321,13 +323,14 @@ public Matches matches(LeafReaderContext context, int doc) throws IOException { } private class RuntimePhraseScorer extends Scorer { - private final LeafSimScorer scorer; private final CheckedIntFunction, IOException> valueFetcher; private final String field; private final Query query; private final TwoPhaseIterator twoPhase; + private final MemoryIndexEntry cacheEntry = new MemoryIndexEntry(); + private int doc = -1; private float freq; @@ -357,7 +360,6 @@ public float matchCost() { // Defaults to a high-ish value so that it likely runs last. return 10_000f; } - }; } @@ -394,35 +396,35 @@ private float freq() throws IOException { return freq; } - private float computeFreq() throws IOException { - MemoryIndex index = new MemoryIndex(); - index.setSimilarity(FREQ_SIMILARITY); - List values = valueFetcher.apply(docID()); - float frequency = 0; - for (Object value : values) { - if (value == null) { - continue; + private MemoryIndex getOrCreateMemoryIndex() throws IOException { + if (cacheEntry.docID != docID()) { + cacheEntry.docID = docID(); + cacheEntry.memoryIndex = new MemoryIndex(true, false); + cacheEntry.memoryIndex.setSimilarity(FREQ_SIMILARITY); + List values = valueFetcher.apply(docID()); + for (Object value : values) { + if (value == null) { + continue; + } + cacheEntry.memoryIndex.addField(field, value.toString(), indexAnalyzer); } - index.addField(field, value.toString(), indexAnalyzer); - frequency += index.search(query); - index.reset(); } - return frequency; + return cacheEntry.memoryIndex; + } + + private float computeFreq() throws IOException { + return getOrCreateMemoryIndex().search(query); } private Matches matches() throws IOException { - MemoryIndex index = new MemoryIndex(true, false); - List values = valueFetcher.apply(docID()); - for (Object value : values) { - if (value == null) { - continue; - } - index.addField(field, value.toString(), indexAnalyzer); - } - IndexSearcher searcher = index.createSearcher(); + IndexSearcher searcher = getOrCreateMemoryIndex().createSearcher(); Weight w = searcher.createWeight(searcher.rewrite(query), ScoreMode.COMPLETE_NO_SCORES, 1); return w.matches(searcher.getLeafContexts().get(0), 0); } } + private static class MemoryIndexEntry { + private int docID = -1; + private MemoryIndex memoryIndex; + } } diff --git a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/SourceConfirmedTextQueryTests.java b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/SourceConfirmedTextQueryTests.java index 2b8d5870cb8aa..81e1dd7099860 100644 --- a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/SourceConfirmedTextQueryTests.java +++ b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/extras/SourceConfirmedTextQueryTests.java @@ -49,13 +49,19 @@ import java.io.IOException; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; public class SourceConfirmedTextQueryTests extends ESTestCase { + private static final AtomicInteger sourceFetchCount = new AtomicInteger(); private static final IOFunction, IOException>> SOURCE_FETCHER_PROVIDER = - context -> docID -> Collections.singletonList(context.reader().document(docID).get("body")); + context -> docID -> { + sourceFetchCount.incrementAndGet(); + return Collections.singletonList(context.reader().document(docID).get("body")); + }; public void testTerm() throws Exception { try (Directory dir = newDirectory(); IndexWriter w = new IndexWriter(dir, newIndexWriterConfig(Lucene.STANDARD_ANALYZER))) { @@ -440,11 +446,11 @@ public void testEmptyIndex() throws Exception { } public void testMatches() throws Exception { - checkMatches(new TermQuery(new Term("body", "d")), "a b c d e", new int[] { 3, 3 }); - checkMatches(new PhraseQuery("body", "b", "c"), "a b c d c b c a", new int[] { 1, 2, 5, 6 }); + checkMatches(new TermQuery(new Term("body", "d")), "a b c d e", new int[] { 3, 3 }, false); + checkMatches(new PhraseQuery("body", "b", "c"), "a b c d c b c a", new int[] { 1, 2, 5, 6 }, true); } - private static void checkMatches(Query query, String inputDoc, int[] expectedMatches) throws IOException { + private static void checkMatches(Query query, String inputDoc, int[] expectedMatches, boolean expectedFetch) throws IOException { try (Directory dir = newDirectory(); IndexWriter w = new IndexWriter(dir, newIndexWriterConfig(Lucene.STANDARD_ANALYZER))) { Document doc = new Document(); doc.add(new TextField("body", "xxxxxnomatchxxxx", Store.YES)); @@ -464,30 +470,48 @@ private static void checkMatches(Query query, String inputDoc, int[] expectedMat Query sourceConfirmedQuery = new SourceConfirmedTextQuery(query, SOURCE_FETCHER_PROVIDER, Lucene.STANDARD_ANALYZER); try (IndexReader ir = DirectoryReader.open(w)) { - - IndexSearcher searcher = new IndexSearcher(ir); - TopDocs td = searcher.search( - sourceConfirmedQuery, - 3, - new Sort(KeywordField.newSortField("sort", false, SortedSetSelector.Type.MAX)) - ); - - Weight weight = searcher.createWeight(searcher.rewrite(sourceConfirmedQuery), ScoreMode.COMPLETE_NO_SCORES, 1); - - int firstDoc = td.scoreDocs[0].doc; - LeafReaderContext firstCtx = searcher.getLeafContexts().get(ReaderUtil.subIndex(firstDoc, searcher.getLeafContexts())); - checkMatches(weight, firstCtx, firstDoc - firstCtx.docBase, expectedMatches, 0); - - int secondDoc = td.scoreDocs[1].doc; - LeafReaderContext secondCtx = searcher.getLeafContexts().get(ReaderUtil.subIndex(secondDoc, searcher.getLeafContexts())); - checkMatches(weight, secondCtx, secondDoc - secondCtx.docBase, expectedMatches, 1); - + { + IndexSearcher searcher = new IndexSearcher(ir); + TopDocs td = searcher.search( + sourceConfirmedQuery, + 3, + new Sort(KeywordField.newSortField("sort", false, SortedSetSelector.Type.MAX)) + ); + + Weight weight = searcher.createWeight(searcher.rewrite(sourceConfirmedQuery), ScoreMode.COMPLETE_NO_SCORES, 1); + + int firstDoc = td.scoreDocs[0].doc; + LeafReaderContext firstCtx = searcher.getLeafContexts().get(ReaderUtil.subIndex(firstDoc, searcher.getLeafContexts())); + checkMatches(weight, firstCtx, firstDoc - firstCtx.docBase, expectedMatches, 0, expectedFetch); + + int secondDoc = td.scoreDocs[1].doc; + LeafReaderContext secondCtx = searcher.getLeafContexts() + .get(ReaderUtil.subIndex(secondDoc, searcher.getLeafContexts())); + checkMatches(weight, secondCtx, secondDoc - secondCtx.docBase, expectedMatches, 1, expectedFetch); + } + + { + IndexSearcher searcher = new IndexSearcher(ir); + TopDocs td = searcher.search(KeywordField.newExactQuery("sort", "0"), 1); + + Weight weight = searcher.createWeight(searcher.rewrite(sourceConfirmedQuery), ScoreMode.COMPLETE_NO_SCORES, 1); + int firstDoc = td.scoreDocs[0].doc; + LeafReaderContext firstCtx = searcher.getLeafContexts().get(ReaderUtil.subIndex(firstDoc, searcher.getLeafContexts())); + checkMatches(weight, firstCtx, firstDoc - firstCtx.docBase, new int[0], 0, false); + } } } } - private static void checkMatches(Weight w, LeafReaderContext ctx, int doc, int[] expectedMatches, int offset) throws IOException { + private static void checkMatches(Weight w, LeafReaderContext ctx, int doc, int[] expectedMatches, int offset, boolean expectedFetch) + throws IOException { + int count = sourceFetchCount.get(); Matches matches = w.matches(ctx, doc); + if (expectedMatches.length == 0) { + assertNull(matches); + assertThat(sourceFetchCount.get() - count, equalTo(expectedFetch ? 1 : 0)); + return; + } assertNotNull(matches); MatchesIterator mi = matches.getMatches("body"); int i = 0; @@ -498,6 +522,7 @@ private static void checkMatches(Weight w, LeafReaderContext ctx, int doc, int[] i += 2; } assertEquals(expectedMatches.length, i); + assertThat(sourceFetchCount.get() - count, equalTo(expectedFetch ? 1 : 0)); } } From fe13a04a5437017381b71d41afb63cb999bf62c4 Mon Sep 17 00:00:00 2001 From: Kathleen DeRusso Date: Tue, 5 Mar 2024 17:21:23 +0100 Subject: [PATCH 247/250] Bugfix for mixed version cluster queries using text expansion (#105912) * Bugfix for CCR queries using text expansion * Fix test * PR feedback * Fix test * Minor cleanup * Edit comment * One more comment clarification --------- Co-authored-by: Elastic Machine --- .../ml/queries/TextExpansionQueryBuilder.java | 48 +++++++++++-------- .../TextExpansionQueryBuilderTests.java | 7 ++- .../test/ml/text_expansion_search.yml | 1 + 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilder.java index 675d062fdb3af..f6fa7ca9005c5 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilder.java @@ -18,6 +18,7 @@ import org.elasticsearch.core.Nullable; import org.elasticsearch.index.query.AbstractQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.QueryRewriteContext; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.xcontent.ParseField; @@ -67,12 +68,7 @@ public String getTypeName() { } public static boolean isFieldTypeAllowed(String typeName) { - for (AllowedFieldType fieldType : values()) { - if (fieldType.getTypeName().equals(typeName)) { - return true; - } - } - return false; + return Arrays.stream(values()).anyMatch(value -> value.typeName.equals(typeName)); } public static String getAllowedFieldTypesAsString() { @@ -168,8 +164,7 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep } @Override - protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException { - + protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) { if (weightedTokensSupplier != null) { if (weightedTokensSupplier.get() == null) { return this; @@ -188,8 +183,8 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws inferRequest.setPrefixType(TrainedModelPrefixStrings.PrefixType.SEARCH); SetOnce textExpansionResultsSupplier = new SetOnce<>(); - queryRewriteContext.registerAsyncAction((client, listener) -> { - executeAsyncWithOrigin( + queryRewriteContext.registerAsyncAction( + (client, listener) -> executeAsyncWithOrigin( client, ML_ORIGIN, CoordinatedInferenceAction.INSTANCE, @@ -220,21 +215,34 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws ); } }, listener::onFailure) - ); - }); + ) + ); return new TextExpansionQueryBuilder(this, textExpansionResultsSupplier); } private QueryBuilder weightedTokensToQuery(String fieldName, TextExpansionResults textExpansionResults) { - WeightedTokensQueryBuilder weightedTokensQueryBuilder = new WeightedTokensQueryBuilder( - fieldName, - textExpansionResults.getWeightedTokens(), - tokenPruningConfig - ); - weightedTokensQueryBuilder.queryName(queryName); - weightedTokensQueryBuilder.boost(boost); - return weightedTokensQueryBuilder; + if (tokenPruningConfig != null) { + WeightedTokensQueryBuilder weightedTokensQueryBuilder = new WeightedTokensQueryBuilder( + fieldName, + textExpansionResults.getWeightedTokens(), + tokenPruningConfig + ); + weightedTokensQueryBuilder.queryName(queryName); + weightedTokensQueryBuilder.boost(boost); + return weightedTokensQueryBuilder; + } + // Note: Weighted tokens queries were introduced in 8.13.0. To support mixed version clusters prior to 8.13.0, + // if no token pruning configuration is specified we fall back to a boolean query. + // TODO this should be updated to always use a WeightedTokensQueryBuilder once it's in all supported versions. + var boolQuery = QueryBuilders.boolQuery(); + for (var weightedToken : textExpansionResults.getWeightedTokens()) { + boolQuery.should(QueryBuilders.termQuery(fieldName, weightedToken.token()).boost(weightedToken.weight())); + } + boolQuery.minimumShouldMatch(1); + boolQuery.boost(boost); + boolQuery.queryName(queryName); + return boolQuery; } @Override diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilderTests.java index 50561d92f5d37..13f12f3cdc1e1 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/queries/TextExpansionQueryBuilderTests.java @@ -25,6 +25,7 @@ import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.mapper.extras.MapperExtrasPlugin; +import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.plugins.Plugin; @@ -259,6 +260,10 @@ public void testThatTokensAreCorrectlyPruned() { SearchExecutionContext searchExecutionContext = createSearchExecutionContext(); TextExpansionQueryBuilder queryBuilder = createTestQueryBuilder(); QueryBuilder rewrittenQueryBuilder = rewriteAndFetch(queryBuilder, searchExecutionContext); - assertTrue(rewrittenQueryBuilder instanceof WeightedTokensQueryBuilder); + if (queryBuilder.getTokenPruningConfig() == null) { + assertTrue(rewrittenQueryBuilder instanceof BoolQueryBuilder); + } else { + assertTrue(rewrittenQueryBuilder instanceof WeightedTokensQueryBuilder); + } } } diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/text_expansion_search.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/text_expansion_search.yml index dc4e1751ccdee..f92870b61f1b1 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/text_expansion_search.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/ml/text_expansion_search.yml @@ -304,3 +304,4 @@ setup: source_text: model_id: text_expansion_model model_text: "octopus comforter smells" + pruning_config: {} From 3e5c3c523ddce71ad2f9c4d28795726b1aed1e10 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 5 Mar 2024 11:33:36 -0500 Subject: [PATCH 248/250] [ML] Enable retrying on 500 error response from Cohere text embedding API (#105797) * Retrying on 500 * Update docs/changelog/105797.yaml --------- Co-authored-by: Elastic Machine --- docs/changelog/105797.yaml | 5 +++++ .../external/cohere/CohereResponseHandler.java | 4 +++- .../external/cohere/CohereResponseHandlerTests.java | 10 ++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/105797.yaml diff --git a/docs/changelog/105797.yaml b/docs/changelog/105797.yaml new file mode 100644 index 0000000000000..7c832e2e5e63c --- /dev/null +++ b/docs/changelog/105797.yaml @@ -0,0 +1,5 @@ +pr: 105797 +summary: Enable retrying on 500 error response from Cohere text embedding API +area: Machine Learning +type: enhancement +issues: [] 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 c7e6493949400..b5af0b474834f 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 @@ -59,7 +59,9 @@ void checkForFailureStatusCode(Request request, HttpResult result) throws RetryE } // handle error codes - if (statusCode >= 500) { + if (statusCode == 500) { + throw new RetryException(true, buildError(SERVER_ERROR, request, result)); + } else if (statusCode > 500) { throw new RetryException(false, buildError(SERVER_ERROR, request, result)); } else if (statusCode == 429) { throw new RetryException(true, buildError(RATE_LIMIT, request, result)); diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/cohere/CohereResponseHandlerTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/cohere/CohereResponseHandlerTests.java index 31945d5a8b4fc..d64ac495c8c99 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/cohere/CohereResponseHandlerTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/external/cohere/CohereResponseHandlerTests.java @@ -44,6 +44,16 @@ public void testCheckForFailureStatusCode_ThrowsFor503() { MatcherAssert.assertThat(((ElasticsearchStatusException) exception.getCause()).status(), is(RestStatus.BAD_REQUEST)); } + public void testCheckForFailureStatusCode_ThrowsFor500_WithShouldRetryTrue() { + var exception = expectThrows(RetryException.class, () -> callCheckForFailureStatusCode(500, "id")); + assertTrue(exception.shouldRetry()); + MatcherAssert.assertThat( + exception.getCause().getMessage(), + containsString("Received a server error status code for request from inference entity id [id] status [500]") + ); + MatcherAssert.assertThat(((ElasticsearchStatusException) exception.getCause()).status(), is(RestStatus.BAD_REQUEST)); + } + public void testCheckForFailureStatusCode_ThrowsFor429() { var exception = expectThrows(RetryException.class, () -> callCheckForFailureStatusCode(429, "id")); assertTrue(exception.shouldRetry()); From b1a3ee864d7b653448d6764d3ca41cdb8e06414c Mon Sep 17 00:00:00 2001 From: Carlos Delgado <6339205+carlosdelest@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:28:38 +0100 Subject: [PATCH 249/250] Semantic text dense vector support (#105515) --- .../BulkShardRequestInferenceProvider.java | 59 +++---- .../vectors/DenseVectorFieldMapper.java | 12 +- .../inference/InferenceServiceResults.java | 2 + ...tModelSettings.java => ModelSettings.java} | 57 +++--- .../action/bulk/BulkOperationTests.java | 145 +++++++++------- .../mock/AbstractTestInferenceService.java | 5 - .../TestSparseInferenceServiceExtension.java | 4 +- ...emanticTextInferenceResultFieldMapper.java | 160 ++++++++++------- ...icTextInferenceResultFieldMapperTests.java | 86 +++++----- .../xpack/inference/InferenceRestIT.java | 2 +- .../inference/10_semantic_text_inference.yml | 162 +++++++++++++----- .../20_semantic_text_field_mapper.yml | 153 +++++++++++++++++ .../CoordinatedInferenceIngestIT.java | 4 +- 13 files changed, 572 insertions(+), 279 deletions(-) rename server/src/main/java/org/elasticsearch/inference/{SemanticTextModelSettings.java => ModelSettings.java} (61%) create mode 100644 x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/20_semantic_text_field_mapper.yml diff --git a/server/src/main/java/org/elasticsearch/action/bulk/BulkShardRequestInferenceProvider.java b/server/src/main/java/org/elasticsearch/action/bulk/BulkShardRequestInferenceProvider.java index 02f905f7cd87a..fdf3af80b8526 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/BulkShardRequestInferenceProvider.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/BulkShardRequestInferenceProvider.java @@ -24,11 +24,13 @@ import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelRegistry; +import org.elasticsearch.inference.ModelSettings; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -46,10 +48,10 @@ public class BulkShardRequestInferenceProvider { public static final String ROOT_INFERENCE_FIELD = "_semantic_text_inference"; // Contains the original text for the field - public static final String TEXT_SUBFIELD_NAME = "text"; - // Contains the inference result when it's a sparse vector - public static final String SPARSE_VECTOR_SUBFIELD_NAME = "sparse_embedding"; + public static final String INFERENCE_RESULTS = "inference_results"; + public static final String INFERENCE_CHUNKS_RESULTS = "inference"; + public static final String INFERENCE_CHUNKS_TEXT = "text"; private final ClusterState clusterState; private final Map inferenceProvidersMap; @@ -90,7 +92,13 @@ public void onResponse(ModelRegistry.UnparsedModel unparsedModel) { var service = inferenceServiceRegistry.getService(unparsedModel.service()); if (service.isEmpty() == false) { InferenceProvider inferenceProvider = new InferenceProvider( - service.get().parsePersistedConfig(inferenceId, unparsedModel.taskType(), unparsedModel.settings()), + service.get() + .parsePersistedConfigWithSecrets( + inferenceId, + unparsedModel.taskType(), + unparsedModel.settings(), + unparsedModel.secrets() + ), service.get() ); inferenceProviderMap.put(inferenceId, inferenceProvider); @@ -105,7 +113,7 @@ public void onFailure(Exception e) { } }; - modelRegistry.getModel(inferenceId, ActionListener.releaseAfter(modelLoadingListener, refs.acquire())); + modelRegistry.getModelWithSecrets(inferenceId, ActionListener.releaseAfter(modelLoadingListener, refs.acquire())); } } } @@ -259,35 +267,22 @@ public void onResponse(InferenceServiceResults results) { } int i = 0; - for (InferenceResults inferenceResults : results.transformToLegacyFormat()) { - String fieldName = inferenceFieldNames.get(i++); - List> inferenceFieldResultList; - try { - inferenceFieldResultList = (List>) rootInferenceFieldMap.computeIfAbsent( - fieldName, - k -> new ArrayList<>() - ); - } catch (ClassCastException e) { - onBulkItemFailure.apply( - bulkItemRequest, - itemIndex, - new IllegalArgumentException( - "Inference result field [" + ROOT_INFERENCE_FIELD + "." + fieldName + "] is not an object" + for (InferenceResults inferenceResults : results.transformToCoordinationFormat()) { + String inferenceFieldName = inferenceFieldNames.get(i++); + Map inferenceFieldResult = new LinkedHashMap<>(); + inferenceFieldResult.putAll(new ModelSettings(inferenceProvider.model).asMap()); + inferenceFieldResult.put( + INFERENCE_RESULTS, + List.of( + Map.of( + INFERENCE_CHUNKS_RESULTS, + inferenceResults.asMap("output").get("output"), + INFERENCE_CHUNKS_TEXT, + docMap.get(inferenceFieldName) ) - ); - return; - } - // Remove previous inference results if any - inferenceFieldResultList.clear(); - - // TODO Check inference result type to change subfield name - var inferenceFieldMap = Map.of( - SPARSE_VECTOR_SUBFIELD_NAME, - inferenceResults.asMap("output").get("output"), - TEXT_SUBFIELD_NAME, - docMap.get(fieldName) + ) ); - inferenceFieldResultList.add(inferenceFieldMap); + rootInferenceFieldMap.put(inferenceFieldName, inferenceFieldResult); } } diff --git a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java index 47efa0ca49771..c6e4d4af926a2 100644 --- a/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java +++ b/server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java @@ -210,6 +210,16 @@ protected Parameter[] getParameters() { return new Parameter[] { elementType, dims, indexed, similarity, indexOptions, meta }; } + public Builder similarity(VectorSimilarity vectorSimilarity) { + similarity.setValue(vectorSimilarity); + return this; + } + + public Builder dimensions(int dimensions) { + this.dims.setValue(dimensions); + return this; + } + @Override public DenseVectorFieldMapper build(MapperBuilderContext context) { return new DenseVectorFieldMapper( @@ -708,7 +718,7 @@ static Function errorByteElementsAppender(byte[] v ElementType.FLOAT ); - enum VectorSimilarity { + public enum VectorSimilarity { L2_NORM { @Override float score(float similarity, ElementType elementType, int dim) { diff --git a/server/src/main/java/org/elasticsearch/inference/InferenceServiceResults.java b/server/src/main/java/org/elasticsearch/inference/InferenceServiceResults.java index 62166115820f5..14cfeacf76139 100644 --- a/server/src/main/java/org/elasticsearch/inference/InferenceServiceResults.java +++ b/server/src/main/java/org/elasticsearch/inference/InferenceServiceResults.java @@ -35,6 +35,8 @@ public interface InferenceServiceResults extends NamedWriteable, ToXContentFragm /** * Convert the result to a map to aid with test assertions + * + * @return a map */ Map asMap(); } diff --git a/server/src/main/java/org/elasticsearch/inference/SemanticTextModelSettings.java b/server/src/main/java/org/elasticsearch/inference/ModelSettings.java similarity index 61% rename from server/src/main/java/org/elasticsearch/inference/SemanticTextModelSettings.java rename to server/src/main/java/org/elasticsearch/inference/ModelSettings.java index 78773bfb72a95..957e2f44d5813 100644 --- a/server/src/main/java/org/elasticsearch/inference/SemanticTextModelSettings.java +++ b/server/src/main/java/org/elasticsearch/inference/ModelSettings.java @@ -8,7 +8,6 @@ package org.elasticsearch.inference; -import org.elasticsearch.core.Nullable; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentParser; @@ -19,28 +18,22 @@ import java.util.Objects; /** - * Model settings that are interesting for semantic_text inference fields. This class is used to serialize common - * ServiceSettings methods when building inference for semantic_text fields. - * - * @param taskType task type - * @param inferenceId inference id - * @param dimensions number of dimensions. May be null if not applicable - * @param similarity similarity used by the service. May be null if not applicable + * Serialization class for specifying the settings of a model from semantic_text inference to field mapper. + * See {@link org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider} */ -public record SemanticTextModelSettings( - TaskType taskType, - String inferenceId, - @Nullable Integer dimensions, - @Nullable SimilarityMeasure similarity -) { +public class ModelSettings { public static final String NAME = "model_settings"; - private static final ParseField TASK_TYPE_FIELD = new ParseField("task_type"); - private static final ParseField INFERENCE_ID_FIELD = new ParseField("inference_id"); - private static final ParseField DIMENSIONS_FIELD = new ParseField("dimensions"); - private static final ParseField SIMILARITY_FIELD = new ParseField("similarity"); + public static final ParseField TASK_TYPE_FIELD = new ParseField("task_type"); + public static final ParseField INFERENCE_ID_FIELD = new ParseField("inference_id"); + public static final ParseField DIMENSIONS_FIELD = new ParseField("dimensions"); + public static final ParseField SIMILARITY_FIELD = new ParseField("similarity"); + private final TaskType taskType; + private final String inferenceId; + private final Integer dimensions; + private final SimilarityMeasure similarity; - public SemanticTextModelSettings(TaskType taskType, String inferenceId, Integer dimensions, SimilarityMeasure similarity) { + public ModelSettings(TaskType taskType, String inferenceId, Integer dimensions, SimilarityMeasure similarity) { Objects.requireNonNull(taskType, "task type must not be null"); Objects.requireNonNull(inferenceId, "inferenceId must not be null"); this.taskType = taskType; @@ -49,7 +42,7 @@ public SemanticTextModelSettings(TaskType taskType, String inferenceId, Integer this.similarity = similarity; } - public SemanticTextModelSettings(Model model) { + public ModelSettings(Model model) { this( model.getTaskType(), model.getInferenceEntityId(), @@ -58,16 +51,16 @@ public SemanticTextModelSettings(Model model) { ); } - public static SemanticTextModelSettings parse(XContentParser parser) throws IOException { + public static ModelSettings parse(XContentParser parser) throws IOException { return PARSER.apply(parser, null); } - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, args -> { + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, args -> { TaskType taskType = TaskType.fromString((String) args[0]); String inferenceId = (String) args[1]; Integer dimensions = (Integer) args[2]; - SimilarityMeasure similarity = args[3] == null ? null : SimilarityMeasure.fromString((String) args[2]); - return new SemanticTextModelSettings(taskType, inferenceId, dimensions, similarity); + SimilarityMeasure similarity = args[3] == null ? null : SimilarityMeasure.fromString((String) args[3]); + return new ModelSettings(taskType, inferenceId, dimensions, similarity); }); static { PARSER.declareString(ConstructingObjectParser.constructorArg(), TASK_TYPE_FIELD); @@ -88,4 +81,20 @@ public Map asMap() { } return Map.of(NAME, attrsMap); } + + public TaskType taskType() { + return taskType; + } + + public String inferenceId() { + return inferenceId; + } + + public Integer dimensions() { + return dimensions; + } + + public SimilarityMeasure similarity() { + return similarity; + } } diff --git a/server/src/test/java/org/elasticsearch/action/bulk/BulkOperationTests.java b/server/src/test/java/org/elasticsearch/action/bulk/BulkOperationTests.java index f8ed331d358b2..4b81e089ed2b2 100644 --- a/server/src/test/java/org/elasticsearch/action/bulk/BulkOperationTests.java +++ b/server/src/test/java/org/elasticsearch/action/bulk/BulkOperationTests.java @@ -33,6 +33,9 @@ import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelRegistry; +import org.elasticsearch.inference.ModelSettings; +import org.elasticsearch.inference.ServiceSettings; +import org.elasticsearch.inference.SimilarityMeasure; import org.elasticsearch.inference.TaskType; import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ESTestCase; @@ -56,6 +59,9 @@ import java.util.stream.Collectors; import static java.util.Collections.emptyMap; +import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.INFERENCE_CHUNKS_RESULTS; +import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.INFERENCE_CHUNKS_TEXT; +import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.INFERENCE_RESULTS; import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.ROOT_INFERENCE_FIELD; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; @@ -91,10 +97,10 @@ public void testNoInference() { Map.of(INFERENCE_SERVICE_1_ID, SERVICE_1_ID, INFERENCE_SERVICE_2_ID, SERVICE_2_ID) ); - Model model1 = mock(Model.class); - InferenceService inferenceService1 = createInferenceService(model1, INFERENCE_SERVICE_1_ID); - Model model2 = mock(Model.class); - InferenceService inferenceService2 = createInferenceService(model2, INFERENCE_SERVICE_2_ID); + Model model1 = mockModel(INFERENCE_SERVICE_1_ID); + InferenceService inferenceService1 = createInferenceService(model1); + Model model2 = mockModel(INFERENCE_SERVICE_2_ID); + InferenceService inferenceService2 = createInferenceService(model2); InferenceServiceRegistry inferenceServiceRegistry = createInferenceServiceRegistry( Map.of(SERVICE_1_ID, inferenceService1, SERVICE_2_ID, inferenceService2) ); @@ -130,6 +136,26 @@ public void testNoInference() { verifyNoMoreInteractions(inferenceServiceRegistry); } + private static Model mockModel(String inferenceServiceId) { + Model model = mock(Model.class); + + when(model.getInferenceEntityId()).thenReturn(inferenceServiceId); + TaskType taskType = randomBoolean() ? TaskType.SPARSE_EMBEDDING : TaskType.TEXT_EMBEDDING; + when(model.getTaskType()).thenReturn(taskType); + + ServiceSettings serviceSettings = mock(ServiceSettings.class); + when(model.getServiceSettings()).thenReturn(serviceSettings); + SimilarityMeasure similarity = switch (randomInt(2)) { + case 0 -> SimilarityMeasure.COSINE; + case 1 -> SimilarityMeasure.DOT_PRODUCT; + default -> null; + }; + when(serviceSettings.similarity()).thenReturn(similarity); + when(serviceSettings.dimensions()).thenReturn(randomBoolean() ? null : randomIntBetween(1, 1000)); + + return model; + } + public void testFailedBulkShardRequest() { Map> fieldsForModels = Map.of(); @@ -191,10 +217,10 @@ public void testInference() { Map.of(INFERENCE_SERVICE_1_ID, SERVICE_1_ID, INFERENCE_SERVICE_2_ID, SERVICE_2_ID) ); - Model model1 = mock(Model.class); - InferenceService inferenceService1 = createInferenceService(model1, INFERENCE_SERVICE_1_ID); - Model model2 = mock(Model.class); - InferenceService inferenceService2 = createInferenceService(model2, INFERENCE_SERVICE_2_ID); + Model model1 = mockModel(INFERENCE_SERVICE_1_ID); + InferenceService inferenceService1 = createInferenceService(model1); + Model model2 = mockModel(INFERENCE_SERVICE_2_ID); + InferenceService inferenceService2 = createInferenceService(model2); InferenceServiceRegistry inferenceServiceRegistry = createInferenceServiceRegistry( Map.of(SERVICE_1_ID, inferenceService1, SERVICE_2_ID, inferenceService2) ); @@ -257,8 +283,8 @@ public void testFailedInference() { ModelRegistry modelRegistry = createModelRegistry(Map.of(INFERENCE_SERVICE_1_ID, SERVICE_1_ID)); - Model model = mock(Model.class); - InferenceService inferenceService = createInferenceServiceThatFails(model, INFERENCE_SERVICE_1_ID); + Model model = mockModel(INFERENCE_SERVICE_1_ID); + InferenceService inferenceService = createInferenceServiceThatFails(model); InferenceServiceRegistry inferenceServiceRegistry = createInferenceServiceRegistry(Map.of(SERVICE_1_ID, inferenceService)); String firstInferenceTextService1 = randomAlphaOfLengthBetween(1, 100); @@ -291,8 +317,8 @@ public void testInferenceFailsForIncorrectRootObject() { ModelRegistry modelRegistry = createModelRegistry(Map.of(INFERENCE_SERVICE_1_ID, SERVICE_1_ID)); - Model model = mock(Model.class); - InferenceService inferenceService = createInferenceServiceThatFails(model, INFERENCE_SERVICE_1_ID); + Model model = mockModel(INFERENCE_SERVICE_1_ID); + InferenceService inferenceService = createInferenceServiceThatFails(model); InferenceServiceRegistry inferenceServiceRegistry = createInferenceServiceRegistry(Map.of(SERVICE_1_ID, inferenceService)); Map originalSource = Map.of( @@ -315,39 +341,6 @@ public void testInferenceFailsForIncorrectRootObject() { assertThat(item.getFailure().getCause().getMessage(), containsString("[_semantic_text_inference] is not an object")); } - public void testInferenceFailsForIncorrectInferenceFieldObject() { - - Map> fieldsForModels = Map.of(INFERENCE_SERVICE_1_ID, Set.of(FIRST_INFERENCE_FIELD_SERVICE_1)); - - ModelRegistry modelRegistry = createModelRegistry(Map.of(INFERENCE_SERVICE_1_ID, SERVICE_1_ID)); - - Model model = mock(Model.class); - InferenceService inferenceService = createInferenceService(model, INFERENCE_SERVICE_1_ID); - InferenceServiceRegistry inferenceServiceRegistry = createInferenceServiceRegistry(Map.of(SERVICE_1_ID, inferenceService)); - - Map originalSource = Map.of( - FIRST_INFERENCE_FIELD_SERVICE_1, - randomAlphaOfLengthBetween(1, 100), - ROOT_INFERENCE_FIELD, - Map.of(FIRST_INFERENCE_FIELD_SERVICE_1, "incorrect_inference_field_value") - ); - - ArgumentCaptor bulkResponseCaptor = ArgumentCaptor.forClass(BulkResponse.class); - @SuppressWarnings("unchecked") - ActionListener bulkOperationListener = mock(ActionListener.class); - runBulkOperation(originalSource, fieldsForModels, modelRegistry, inferenceServiceRegistry, false, bulkOperationListener); - - verify(bulkOperationListener).onResponse(bulkResponseCaptor.capture()); - BulkResponse bulkResponse = bulkResponseCaptor.getValue(); - assertTrue(bulkResponse.hasFailures()); - BulkItemResponse item = bulkResponse.getItems()[0]; - assertTrue(item.isFailed()); - assertThat( - item.getFailure().getCause().getMessage(), - containsString("Inference result field [_semantic_text_inference.first_inference_field_service_1] is not an object") - ); - } - public void testInferenceIdNotFound() { Map> fieldsForModels = Map.of( @@ -359,8 +352,8 @@ public void testInferenceIdNotFound() { ModelRegistry modelRegistry = createModelRegistry(Map.of(INFERENCE_SERVICE_1_ID, SERVICE_1_ID)); - Model model = mock(Model.class); - InferenceService inferenceService = createInferenceService(model, INFERENCE_SERVICE_1_ID); + Model model = mockModel(INFERENCE_SERVICE_1_ID); + InferenceService inferenceService = createInferenceService(model); InferenceServiceRegistry inferenceServiceRegistry = createInferenceServiceRegistry(Map.of(SERVICE_1_ID, inferenceService)); Map originalSource = Map.of( @@ -400,17 +393,20 @@ private static void checkInferenceResults( ); for (String inferenceFieldName : inferenceFieldNames) { - List> inferenceService1FieldResults = (List>) inferenceRootResultField.get( - inferenceFieldName - ); + Map inferenceService1FieldResults = (Map) inferenceRootResultField.get(inferenceFieldName); assertNotNull(inferenceService1FieldResults); - assertThat(inferenceService1FieldResults.size(), equalTo(1)); - Map inferenceResultElement = inferenceService1FieldResults.get(0); - assertNotNull(inferenceResultElement.get(BulkShardRequestInferenceProvider.SPARSE_VECTOR_SUBFIELD_NAME)); - assertThat( - inferenceResultElement.get(BulkShardRequestInferenceProvider.TEXT_SUBFIELD_NAME), - equalTo(docSource.get(inferenceFieldName)) + assertThat(inferenceService1FieldResults.size(), equalTo(2)); + Map modelSettings = (Map) inferenceService1FieldResults.get(ModelSettings.NAME); + assertNotNull(modelSettings); + assertNotNull(modelSettings.get(ModelSettings.TASK_TYPE_FIELD.getPreferredName())); + assertNotNull(modelSettings.get(ModelSettings.INFERENCE_ID_FIELD.getPreferredName())); + + List> inferenceResultElement = (List>) inferenceService1FieldResults.get( + INFERENCE_RESULTS ); + assertFalse(inferenceResultElement.isEmpty()); + assertNotNull(inferenceResultElement.get(0).get(INFERENCE_CHUNKS_RESULTS)); + assertThat(inferenceResultElement.get(0).get(INFERENCE_CHUNKS_TEXT), equalTo(docSource.get(inferenceFieldName))); } } @@ -421,8 +417,13 @@ private static void verifyInferenceServiceInvoked( Model model, Collection inferenceTexts ) { - verify(modelRegistry).getModel(eq(inferenceService1Id), any()); - verify(inferenceService).parsePersistedConfig(eq(inferenceService1Id), eq(TaskType.SPARSE_EMBEDDING), anyMap()); + verify(modelRegistry).getModelWithSecrets(eq(inferenceService1Id), any()); + verify(inferenceService).parsePersistedConfigWithSecrets( + eq(inferenceService1Id), + eq(TaskType.SPARSE_EMBEDDING), + anyMap(), + anyMap() + ); verify(inferenceService).infer(eq(model), argThat(containsInAnyOrder(inferenceTexts)), anyMap(), eq(InputType.INGEST), any()); verifyNoMoreInteractions(inferenceService); } @@ -537,9 +538,16 @@ private static BulkShardRequest runBulkOperation( ); }; - private static InferenceService createInferenceService(Model model, String inferenceServiceId) { + private static InferenceService createInferenceService(Model model) { InferenceService inferenceService = mock(InferenceService.class); - when(inferenceService.parsePersistedConfig(eq(inferenceServiceId), eq(TaskType.SPARSE_EMBEDDING), anyMap())).thenReturn(model); + when( + inferenceService.parsePersistedConfigWithSecrets( + eq(model.getInferenceEntityId()), + eq(TaskType.SPARSE_EMBEDDING), + anyMap(), + anyMap() + ) + ).thenReturn(model); doAnswer(invocation -> { ActionListener listener = invocation.getArgument(4); InferenceServiceResults inferenceServiceResults = mock(InferenceServiceResults.class); @@ -548,7 +556,7 @@ private static InferenceService createInferenceService(Model model, String infer for (int i = 0; i < texts.size(); i++) { inferenceResults.add(createInferenceResults()); } - doReturn(inferenceResults).when(inferenceServiceResults).transformToLegacyFormat(); + doReturn(inferenceResults).when(inferenceServiceResults).transformToCoordinationFormat(); listener.onResponse(inferenceServiceResults); return null; @@ -556,9 +564,16 @@ private static InferenceService createInferenceService(Model model, String infer return inferenceService; } - private static InferenceService createInferenceServiceThatFails(Model model, String inferenceServiceId) { + private static InferenceService createInferenceServiceThatFails(Model model) { InferenceService inferenceService = mock(InferenceService.class); - when(inferenceService.parsePersistedConfig(eq(inferenceServiceId), eq(TaskType.SPARSE_EMBEDDING), anyMap())).thenReturn(model); + when( + inferenceService.parsePersistedConfigWithSecrets( + eq(model.getInferenceEntityId()), + eq(TaskType.SPARSE_EMBEDDING), + anyMap(), + anyMap() + ) + ).thenReturn(model); doAnswer(invocation -> { ActionListener listener = invocation.getArgument(4); listener.onFailure(new IllegalArgumentException(INFERENCE_FAILED_MSG)); @@ -591,7 +606,7 @@ private static ModelRegistry createModelRegistry(Map inferenceId ActionListener listener = invocation.getArgument(1); listener.onFailure(new IllegalArgumentException("Model not found")); return null; - }).when(modelRegistry).getModel(any(), any()); + }).when(modelRegistry).getModelWithSecrets(any(), any()); inferenceIdsToServiceIds.forEach((inferenceId, serviceId) -> { ModelRegistry.UnparsedModel unparsedModel = new ModelRegistry.UnparsedModel( inferenceId, @@ -604,7 +619,7 @@ private static ModelRegistry createModelRegistry(Map inferenceId ActionListener listener = invocation.getArgument(1); listener.onResponse(unparsedModel); return null; - }).when(modelRegistry).getModel(eq(inferenceId), any()); + }).when(modelRegistry).getModelWithSecrets(eq(inferenceId), any()); }); return modelRegistry; diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/AbstractTestInferenceService.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/AbstractTestInferenceService.java index 99dfc9582eb05..a65b8e43e6adf 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/AbstractTestInferenceService.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/AbstractTestInferenceService.java @@ -101,11 +101,6 @@ public TestServiceModel( super(new ModelConfigurations(modelId, taskType, service, serviceSettings, taskSettings), new ModelSecrets(secretSettings)); } - @Override - public TestDenseInferenceServiceExtension.TestServiceSettings getServiceSettings() { - return (TestDenseInferenceServiceExtension.TestServiceSettings) super.getServiceSettings(); - } - @Override public TestTaskSettings getTaskSettings() { return (TestTaskSettings) super.getTaskSettings(); diff --git a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestSparseInferenceServiceExtension.java b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestSparseInferenceServiceExtension.java index e5020774a70f3..33bbc94901e9d 100644 --- a/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestSparseInferenceServiceExtension.java +++ b/x-pack/plugin/inference/qa/test-service-plugin/src/main/java/org/elasticsearch/xpack/inference/mock/TestSparseInferenceServiceExtension.java @@ -115,7 +115,7 @@ private SparseEmbeddingResults makeResults(List input) { for (int i = 0; i < input.size(); i++) { var tokens = new ArrayList(); for (int j = 0; j < 5; j++) { - tokens.add(new SparseEmbeddingResults.WeightedToken(Integer.toString(j), (float) j)); + tokens.add(new SparseEmbeddingResults.WeightedToken("feature_" + j, j + 1.0F)); } embeddings.add(new SparseEmbeddingResults.Embedding(tokens, false)); } @@ -127,7 +127,7 @@ private List makeChunkedResults(List inp for (int i = 0; i < input.size(); i++) { var tokens = new ArrayList(); for (int j = 0; j < 5; j++) { - tokens.add(new TextExpansionResults.WeightedToken(Integer.toString(j), (float) j)); + tokens.add(new TextExpansionResults.WeightedToken("feature_" + j, j + 1.0F)); } chunks.add(new ChunkedTextExpansionResults.ChunkedResult(input.get(i), tokens)); } diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapper.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapper.java index 9e6c1eb0a6586..dbde641d8f757 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapper.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapper.java @@ -8,9 +8,9 @@ package org.elasticsearch.xpack.inference.mapper; import org.apache.lucene.search.Query; +import org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider; import org.elasticsearch.common.Strings; import org.elasticsearch.index.IndexVersion; -import org.elasticsearch.index.mapper.BooleanFieldMapper; import org.elasticsearch.index.mapper.DocumentParserContext; import org.elasticsearch.index.mapper.DocumentParsingException; import org.elasticsearch.index.mapper.FieldMapper; @@ -25,25 +25,25 @@ import org.elasticsearch.index.mapper.TextFieldMapper; import org.elasticsearch.index.mapper.TextSearchInfo; import org.elasticsearch.index.mapper.ValueFetcher; +import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; import org.elasticsearch.index.mapper.vectors.SparseVectorFieldMapper; import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.inference.ModelSettings; +import org.elasticsearch.inference.SimilarityMeasure; +import org.elasticsearch.inference.TaskType; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; -import org.elasticsearch.script.ScriptCompiler; import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xpack.core.inference.results.SparseEmbeddingResults; import java.io.IOException; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.SPARSE_VECTOR_SUBFIELD_NAME; -import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.TEXT_SUBFIELD_NAME; +import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.INFERENCE_CHUNKS_RESULTS; +import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.INFERENCE_CHUNKS_TEXT; +import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.ROOT_INFERENCE_FIELD; /** * A mapper for the {@code _semantic_text_inference} field. @@ -102,16 +102,13 @@ */ public class SemanticTextInferenceResultFieldMapper extends MetadataFieldMapper { public static final String CONTENT_TYPE = "_semantic_text_inference"; - public static final String NAME = "_semantic_text_inference"; + public static final String NAME = ROOT_INFERENCE_FIELD; public static final TypeParser PARSER = new FixedTypeParser(c -> new SemanticTextInferenceResultFieldMapper()); - private static final Map, Set> REQUIRED_SUBFIELDS_MAP = Map.of( - List.of(), - Set.of(SPARSE_VECTOR_SUBFIELD_NAME, TEXT_SUBFIELD_NAME) - ); - private static final Logger logger = LogManager.getLogger(SemanticTextInferenceResultFieldMapper.class); + private static final Set REQUIRED_SUBFIELDS = Set.of(INFERENCE_CHUNKS_TEXT, INFERENCE_CHUNKS_RESULTS); + static class SemanticTextInferenceFieldType extends MappedFieldType { private static final MappedFieldType INSTANCE = new SemanticTextInferenceFieldType(); @@ -142,75 +139,86 @@ private SemanticTextInferenceResultFieldMapper() { @Override protected void parseCreateField(DocumentParserContext context) throws IOException { XContentParser parser = context.parser(); - if (parser.currentToken() != XContentParser.Token.START_OBJECT) { - throw new DocumentParsingException(parser.getTokenLocation(), "Expected a START_OBJECT, got " + parser.currentToken()); - } + failIfTokenIsNot(parser, XContentParser.Token.START_OBJECT); - parseInferenceResults(context); + parseAllFields(context); } - private static void parseInferenceResults(DocumentParserContext context) throws IOException { + private static void parseAllFields(DocumentParserContext context) throws IOException { XContentParser parser = context.parser(); MapperBuilderContext mapperBuilderContext = MapperBuilderContext.root(false, false); for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) { - if (token != XContentParser.Token.FIELD_NAME) { - throw new DocumentParsingException(parser.getTokenLocation(), "Expected a FIELD_NAME, got " + token); - } + failIfTokenIsNot(parser, XContentParser.Token.FIELD_NAME); - parseFieldInferenceResults(context, mapperBuilderContext); + parseSingleField(context, mapperBuilderContext); } } - private static void parseFieldInferenceResults(DocumentParserContext context, MapperBuilderContext mapperBuilderContext) - throws IOException { + private static void parseSingleField(DocumentParserContext context, MapperBuilderContext mapperBuilderContext) throws IOException { - String fieldName = context.parser().currentName(); + XContentParser parser = context.parser(); + String fieldName = parser.currentName(); Mapper mapper = context.getMapper(fieldName); if (mapper == null || SemanticTextFieldMapper.CONTENT_TYPE.equals(mapper.typeName()) == false) { throw new DocumentParsingException( - context.parser().getTokenLocation(), + parser.getTokenLocation(), Strings.format("Field [%s] is not registered as a %s field type", fieldName, SemanticTextFieldMapper.CONTENT_TYPE) ); } + parser.nextToken(); + failIfTokenIsNot(parser, XContentParser.Token.START_OBJECT); + parser.nextToken(); + ModelSettings modelSettings = ModelSettings.parse(parser); + for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) { + failIfTokenIsNot(parser, XContentParser.Token.FIELD_NAME); - parseFieldInferenceResultsArray(context, mapperBuilderContext, fieldName); + String currentName = parser.currentName(); + if (BulkShardRequestInferenceProvider.INFERENCE_RESULTS.equals(currentName)) { + NestedObjectMapper nestedObjectMapper = createInferenceResultsObjectMapper( + context, + mapperBuilderContext, + fieldName, + modelSettings + ); + parseFieldInferenceChunks(context, mapperBuilderContext, fieldName, modelSettings, nestedObjectMapper); + } else { + logger.debug("Skipping unrecognized field name [" + currentName + "]"); + advancePastCurrentFieldName(parser); + } + } } - private static void parseFieldInferenceResultsArray( + private static void parseFieldInferenceChunks( DocumentParserContext context, MapperBuilderContext mapperBuilderContext, - String fieldName + String fieldName, + ModelSettings modelSettings, + NestedObjectMapper nestedObjectMapper ) throws IOException { XContentParser parser = context.parser(); - NestedObjectMapper nestedObjectMapper = createNestedObjectMapper(context, mapperBuilderContext, fieldName); - if (parser.nextToken() != XContentParser.Token.START_ARRAY) { - throw new DocumentParsingException(parser.getTokenLocation(), "Expected a START_ARRAY, got " + parser.currentToken()); - } + parser.nextToken(); + failIfTokenIsNot(parser, XContentParser.Token.START_ARRAY); for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_ARRAY; token = parser.nextToken()) { DocumentParserContext nestedContext = context.createNestedContext(nestedObjectMapper); - parseFieldInferenceResultElement(nestedContext, nestedObjectMapper, new LinkedList<>()); + parseFieldInferenceChunkElement(nestedContext, nestedObjectMapper, modelSettings); } } - private static void parseFieldInferenceResultElement( + private static void parseFieldInferenceChunkElement( DocumentParserContext context, ObjectMapper objectMapper, - LinkedList subfieldPath + ModelSettings modelSettings ) throws IOException { XContentParser parser = context.parser(); DocumentParserContext childContext = context.createChildContext(objectMapper); - if (parser.currentToken() != XContentParser.Token.START_OBJECT) { - throw new DocumentParsingException(parser.getTokenLocation(), "Expected a START_OBJECT, got " + parser.currentToken()); - } + failIfTokenIsNot(parser, XContentParser.Token.START_OBJECT); Set visitedSubfields = new HashSet<>(); for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) { - if (token != XContentParser.Token.FIELD_NAME) { - throw new DocumentParsingException(parser.getTokenLocation(), "Expected a FIELD_NAME, got " + parser.currentToken()); - } + failIfTokenIsNot(parser, XContentParser.Token.FIELD_NAME); String currentName = parser.currentName(); visitedSubfields.add(currentName); @@ -222,14 +230,9 @@ private static void parseFieldInferenceResultElement( continue; } - if (childMapper instanceof FieldMapper) { + if (childMapper instanceof FieldMapper fieldMapper) { parser.nextToken(); - ((FieldMapper) childMapper).parse(childContext); - } else if (childMapper instanceof ObjectMapper) { - parser.nextToken(); - subfieldPath.push(currentName); - parseFieldInferenceResultElement(childContext, (ObjectMapper) childMapper, subfieldPath); - subfieldPath.pop(); + fieldMapper.parse(childContext); } else { // This should never happen, but fail parsing if it does so that it's not a silent failure throw new DocumentParsingException( @@ -239,29 +242,51 @@ private static void parseFieldInferenceResultElement( } } - Set requiredSubfields = REQUIRED_SUBFIELDS_MAP.get(subfieldPath); - if (requiredSubfields != null && visitedSubfields.containsAll(requiredSubfields) == false) { - Set missingSubfields = requiredSubfields.stream() + if (visitedSubfields.containsAll(REQUIRED_SUBFIELDS) == false) { + Set missingSubfields = REQUIRED_SUBFIELDS.stream() .filter(s -> visitedSubfields.contains(s) == false) .collect(Collectors.toSet()); throw new DocumentParsingException(parser.getTokenLocation(), "Missing required subfields: " + missingSubfields); } } - private static NestedObjectMapper createNestedObjectMapper( + private static NestedObjectMapper createInferenceResultsObjectMapper( DocumentParserContext context, MapperBuilderContext mapperBuilderContext, - String fieldName + String fieldName, + ModelSettings modelSettings ) { IndexVersion indexVersionCreated = context.indexSettings().getIndexVersionCreated(); - ObjectMapper.Builder sparseVectorMapperBuilder = new ObjectMapper.Builder( - SPARSE_VECTOR_SUBFIELD_NAME, - ObjectMapper.Defaults.SUBOBJECTS - ).add( - new BooleanFieldMapper.Builder(SparseEmbeddingResults.Embedding.IS_TRUNCATED, ScriptCompiler.NONE, false, indexVersionCreated) - ).add(new SparseVectorFieldMapper.Builder(SparseEmbeddingResults.Embedding.EMBEDDING)); + FieldMapper.Builder resultsBuilder; + if (modelSettings.taskType() == TaskType.SPARSE_EMBEDDING) { + resultsBuilder = new SparseVectorFieldMapper.Builder(INFERENCE_CHUNKS_RESULTS); + } else if (modelSettings.taskType() == TaskType.TEXT_EMBEDDING) { + DenseVectorFieldMapper.Builder denseVectorMapperBuilder = new DenseVectorFieldMapper.Builder( + INFERENCE_CHUNKS_RESULTS, + indexVersionCreated + ); + SimilarityMeasure similarity = modelSettings.similarity(); + if (similarity != null) { + switch (similarity) { + case COSINE -> denseVectorMapperBuilder.similarity(DenseVectorFieldMapper.VectorSimilarity.COSINE); + case DOT_PRODUCT -> denseVectorMapperBuilder.similarity(DenseVectorFieldMapper.VectorSimilarity.DOT_PRODUCT); + default -> throw new IllegalArgumentException( + "Unknown similarity measure for field [" + fieldName + "] in model settings: " + similarity + ); + } + } + Integer dimensions = modelSettings.dimensions(); + if (dimensions == null) { + throw new IllegalArgumentException("Model settings for field [" + fieldName + "] must contain dimensions"); + } + denseVectorMapperBuilder.dimensions(dimensions); + resultsBuilder = denseVectorMapperBuilder; + } else { + throw new IllegalArgumentException("Unknown task type for field [" + fieldName + "]: " + modelSettings.taskType()); + } + TextFieldMapper.Builder textMapperBuilder = new TextFieldMapper.Builder( - TEXT_SUBFIELD_NAME, + INFERENCE_CHUNKS_TEXT, indexVersionCreated, context.indexAnalyzers() ).index(false).store(false); @@ -270,7 +295,7 @@ private static NestedObjectMapper createNestedObjectMapper( fieldName, context.indexSettings().getIndexVersionCreated() ); - nestedBuilder.add(sparseVectorMapperBuilder).add(textMapperBuilder); + nestedBuilder.add(resultsBuilder).add(textMapperBuilder); return nestedBuilder.build(mapperBuilderContext); } @@ -286,6 +311,15 @@ private static void advancePastCurrentFieldName(XContentParser parser) throws IO } } + private static void failIfTokenIsNot(XContentParser parser, XContentParser.Token expected) { + if (parser.currentToken() != expected) { + throw new DocumentParsingException( + parser.getTokenLocation(), + "Expected a " + expected.toString() + ", got " + parser.currentToken() + ); + } + } + @Override protected String contentType() { return CONTENT_TYPE; diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapperTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapperTests.java index aa2ad72941e0e..06a665ade3ab4 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapperTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapperTests.java @@ -31,6 +31,8 @@ import org.elasticsearch.index.mapper.NestedObjectMapper; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.search.ESToParentBlockJoinQuery; +import org.elasticsearch.inference.ModelSettings; +import org.elasticsearch.inference.TaskType; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.LeafNestedDocuments; import org.elasticsearch.search.NestedDocuments; @@ -51,8 +53,9 @@ import java.util.Set; import java.util.function.Consumer; -import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.SPARSE_VECTOR_SUBFIELD_NAME; -import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.TEXT_SUBFIELD_NAME; +import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.INFERENCE_CHUNKS_RESULTS; +import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.INFERENCE_CHUNKS_TEXT; +import static org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider.INFERENCE_RESULTS; import static org.hamcrest.Matchers.containsString; public class SemanticTextInferenceResultFieldMapperTests extends MetadataMapperTestCase { @@ -214,7 +217,7 @@ public void testMissingSubfields() throws IOException { ) ) ); - assertThat(ex.getMessage(), containsString("Missing required subfields: [" + SPARSE_VECTOR_SUBFIELD_NAME + "]")); + assertThat(ex.getMessage(), containsString("Missing required subfields: [" + INFERENCE_CHUNKS_RESULTS + "]")); } { DocumentParsingException ex = expectThrows( @@ -232,7 +235,7 @@ public void testMissingSubfields() throws IOException { ) ) ); - assertThat(ex.getMessage(), containsString("Missing required subfields: [" + TEXT_SUBFIELD_NAME + "]")); + assertThat(ex.getMessage(), containsString("Missing required subfields: [" + INFERENCE_CHUNKS_TEXT + "]")); } { DocumentParsingException ex = expectThrows( @@ -252,7 +255,7 @@ public void testMissingSubfields() throws IOException { ); assertThat( ex.getMessage(), - containsString("Missing required subfields: [" + SPARSE_VECTOR_SUBFIELD_NAME + ", " + TEXT_SUBFIELD_NAME + "]") + containsString("Missing required subfields: [" + INFERENCE_CHUNKS_RESULTS + ", " + INFERENCE_CHUNKS_TEXT + "]") ); } } @@ -411,8 +414,10 @@ private static void addSemanticTextInferenceResults( Map extraSubfields ) throws IOException { - Map>> inferenceResultsMap = new HashMap<>(); + Map> inferenceResultsMap = new HashMap<>(); for (SemanticTextInferenceResults semanticTextInferenceResult : semanticTextInferenceResults) { + Map fieldMap = new HashMap<>(); + fieldMap.put(ModelSettings.NAME, modelSettingsMap()); List> parsedInferenceResults = new ArrayList<>(semanticTextInferenceResult.text().size()); Iterator embeddingsIterator = semanticTextInferenceResult.sparseEmbeddingResults() @@ -425,17 +430,10 @@ private static void addSemanticTextInferenceResults( Map subfieldMap = new HashMap<>(); if (sparseVectorSubfieldOptions.include()) { - Map embeddingMap = embedding.asMap(); - if (sparseVectorSubfieldOptions.includeIsTruncated() == false) { - embeddingMap.remove(SparseEmbeddingResults.Embedding.IS_TRUNCATED); - } - if (sparseVectorSubfieldOptions.includeEmbedding() == false) { - embeddingMap.remove(SparseEmbeddingResults.Embedding.EMBEDDING); - } - subfieldMap.put(SPARSE_VECTOR_SUBFIELD_NAME, embeddingMap); + subfieldMap.put(INFERENCE_CHUNKS_RESULTS, embedding.asMap().get(SparseEmbeddingResults.Embedding.EMBEDDING)); } if (includeTextSubfield) { - subfieldMap.put(TEXT_SUBFIELD_NAME, text); + subfieldMap.put(INFERENCE_CHUNKS_TEXT, text); } if (extraSubfields != null) { subfieldMap.putAll(extraSubfields); @@ -444,28 +442,42 @@ private static void addSemanticTextInferenceResults( parsedInferenceResults.add(subfieldMap); } - inferenceResultsMap.put(semanticTextInferenceResult.fieldName(), parsedInferenceResults); + fieldMap.put(INFERENCE_RESULTS, parsedInferenceResults); + inferenceResultsMap.put(semanticTextInferenceResult.fieldName(), fieldMap); } sourceBuilder.field(SemanticTextInferenceResultFieldMapper.NAME, inferenceResultsMap); } + private static Map modelSettingsMap() { + return Map.of( + ModelSettings.TASK_TYPE_FIELD.getPreferredName(), + TaskType.SPARSE_EMBEDDING.toString(), + ModelSettings.INFERENCE_ID_FIELD.getPreferredName(), + randomAlphaOfLength(8) + ); + } + private static void addInferenceResultsNestedMapping(XContentBuilder mappingBuilder, String semanticTextFieldName) throws IOException { mappingBuilder.startObject(semanticTextFieldName); - mappingBuilder.field("type", "nested"); - mappingBuilder.startObject("properties"); - mappingBuilder.startObject(SPARSE_VECTOR_SUBFIELD_NAME); - mappingBuilder.startObject("properties"); - mappingBuilder.startObject(SparseEmbeddingResults.Embedding.EMBEDDING); - mappingBuilder.field("type", "sparse_vector"); - mappingBuilder.endObject(); - mappingBuilder.endObject(); - mappingBuilder.endObject(); - mappingBuilder.startObject(TEXT_SUBFIELD_NAME); - mappingBuilder.field("type", "text"); - mappingBuilder.field("index", false); - mappingBuilder.endObject(); - mappingBuilder.endObject(); + { + mappingBuilder.field("type", "nested"); + mappingBuilder.startObject("properties"); + { + mappingBuilder.startObject(INFERENCE_CHUNKS_RESULTS); + { + mappingBuilder.field("type", "sparse_vector"); + } + mappingBuilder.endObject(); + mappingBuilder.startObject(INFERENCE_CHUNKS_TEXT); + { + mappingBuilder.field("type", "text"); + mappingBuilder.field("index", false); + } + mappingBuilder.endObject(); + } + mappingBuilder.endObject(); + } mappingBuilder.endObject(); } @@ -477,12 +489,7 @@ private static Query generateNestedTermSparseVectorQuery(NestedLookup nestedLook BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder(); for (String token : tokens) { queryBuilder.add( - new BooleanClause( - new TermQuery( - new Term(path + "." + SPARSE_VECTOR_SUBFIELD_NAME + "." + SparseEmbeddingResults.Embedding.EMBEDDING, token) - ), - BooleanClause.Occur.MUST - ) + new BooleanClause(new TermQuery(new Term(path + "." + INFERENCE_CHUNKS_RESULTS, token)), BooleanClause.Occur.MUST) ); } queryBuilder.add(new BooleanClause(mapper.nestedTypeFilter(), BooleanClause.Occur.FILTER)); @@ -497,12 +504,7 @@ private static void assertValidChildDoc( ) { assertEquals(expectedParent, childDoc.getParent()); visitedChildDocs.add( - new VisitedChildDocInfo( - childDoc.getPath(), - childDoc.getFields( - childDoc.getPath() + "." + SPARSE_VECTOR_SUBFIELD_NAME + "." + SparseEmbeddingResults.Embedding.EMBEDDING - ).size() - ) + new VisitedChildDocInfo(childDoc.getPath(), childDoc.getFields(childDoc.getPath() + "." + INFERENCE_CHUNKS_RESULTS).size()) ); } diff --git a/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/InferenceRestIT.java b/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/InferenceRestIT.java index 933e696d29d83..a397d9864d23d 100644 --- a/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/InferenceRestIT.java +++ b/x-pack/plugin/inference/src/yamlRestTest/java/org/elasticsearch/xpack/inference/InferenceRestIT.java @@ -21,7 +21,7 @@ public class InferenceRestIT extends ESClientYamlSuiteTestCase { public static ElasticsearchCluster cluster = ElasticsearchCluster.local() .setting("xpack.security.enabled", "false") .setting("xpack.security.http.ssl.enabled", "false") - .plugin("org.elasticsearch.xpack.inference.mock.TestInferenceServicePlugin") + .plugin("inference-service-test") .distribution(DistributionType.DEFAULT) .build(); diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_inference.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_inference.yml index 0e1b33252153b..ead7f904ad57b 100644 --- a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_inference.yml +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/10_semantic_text_inference.yml @@ -6,7 +6,7 @@ setup: - do: inference.put_model: task_type: sparse_embedding - inference_id: test-inference-id + inference_id: sparse-inference-id body: > { "service": "test_service", @@ -17,27 +17,57 @@ setup: "task_settings": { } } + - do: + inference.put_model: + 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" + }, + "task_settings": { + } + } + + - do: + indices.create: + index: test-sparse-index + body: + mappings: + properties: + inference_field: + type: semantic_text + model_id: sparse-inference-id + another_inference_field: + type: semantic_text + model_id: sparse-inference-id + non_inference_field: + type: text - do: indices.create: - index: test-index + index: test-dense-index body: mappings: properties: inference_field: type: semantic_text - model_id: test-inference-id + model_id: dense-inference-id another_inference_field: type: semantic_text - model_id: test-inference-id + model_id: dense-inference-id non_inference_field: type: text --- -"Calculates embeddings for new documents": +"Calculates text expansion results for new documents": - do: index: - index: test-index + index: test-sparse-index id: doc_1 body: inference_field: "inference test" @@ -46,24 +76,73 @@ setup: - do: get: - index: test-index + index: test-sparse-index id: doc_1 - match: { _source.inference_field: "inference test" } - match: { _source.another_inference_field: "another inference test" } - match: { _source.non_inference_field: "non inference test" } - - match: { _source._semantic_text_inference.inference_field.0.text: "inference test" } - - match: { _source._semantic_text_inference.another_inference_field.0.text: "another inference test" } + - match: { _source._semantic_text_inference.inference_field.inference_results.0.text: "inference test" } + - match: { _source._semantic_text_inference.another_inference_field.inference_results.0.text: "another inference test" } + + - exists: _source._semantic_text_inference.inference_field.inference_results.0.inference + - exists: _source._semantic_text_inference.another_inference_field.inference_results.0.inference + +--- +"text expansion documents do not create new mappings": + - do: + indices.get_mapping: + index: test-sparse-index + + - match: {test-sparse-index.mappings.properties.inference_field.type: semantic_text} + - match: {test-sparse-index.mappings.properties.another_inference_field.type: semantic_text} + - match: {test-sparse-index.mappings.properties.non_inference_field.type: text} + - length: {test-sparse-index.mappings.properties: 3} + +--- +"Calculates text embeddings results for new documents": + - do: + index: + index: test-dense-index + id: doc_1 + body: + inference_field: "inference test" + another_inference_field: "another inference test" + non_inference_field: "non inference test" + + - do: + get: + index: test-dense-index + id: doc_1 + + - match: { _source.inference_field: "inference test" } + - match: { _source.another_inference_field: "another inference test" } + - match: { _source.non_inference_field: "non inference test" } + + - match: { _source._semantic_text_inference.inference_field.inference_results.0.text: "inference test" } + - match: { _source._semantic_text_inference.another_inference_field.inference_results.0.text: "another inference test" } - - exists: _source._semantic_text_inference.inference_field.0.sparse_embedding - - exists: _source._semantic_text_inference.another_inference_field.0.sparse_embedding + - exists: _source._semantic_text_inference.inference_field.inference_results.0.inference + - exists: _source._semantic_text_inference.another_inference_field.inference_results.0.inference + + +--- +"text embeddings documents do not create new mappings": + - do: + indices.get_mapping: + index: test-dense-index + + - match: {test-dense-index.mappings.properties.inference_field.type: semantic_text} + - match: {test-dense-index.mappings.properties.another_inference_field.type: semantic_text} + - match: {test-dense-index.mappings.properties.non_inference_field.type: text} + - length: {test-dense-index.mappings.properties: 3} --- "Updating non semantic_text fields does not recalculate embeddings": - do: index: - index: test-index + index: test-sparse-index id: doc_1 body: inference_field: "inference test" @@ -72,15 +151,15 @@ setup: - do: get: - index: test-index + index: test-sparse-index id: doc_1 - - set: { _source._semantic_text_inference.inference_field.0.sparse_embedding: inference_field_embedding } - - set: { _source._semantic_text_inference.another_inference_field.0.sparse_embedding: another_inference_field_embedding } + - set: { _source._semantic_text_inference.inference_field.inference_results.0.inference: inference_field_embedding } + - set: { _source._semantic_text_inference.another_inference_field.inference_results.0.inference: another_inference_field_embedding } - do: update: - index: test-index + index: test-sparse-index id: doc_1 body: doc: @@ -88,24 +167,24 @@ setup: - do: get: - index: test-index + index: test-sparse-index id: doc_1 - match: { _source.inference_field: "inference test" } - match: { _source.another_inference_field: "another inference test" } - match: { _source.non_inference_field: "another non inference test" } - - match: { _source._semantic_text_inference.inference_field.0.text: "inference test" } - - match: { _source._semantic_text_inference.another_inference_field.0.text: "another inference test" } + - match: { _source._semantic_text_inference.inference_field.inference_results.0.text: "inference test" } + - match: { _source._semantic_text_inference.another_inference_field.inference_results.0.text: "another inference test" } - - match: { _source._semantic_text_inference.inference_field.0.sparse_embedding: $inference_field_embedding } - - match: { _source._semantic_text_inference.another_inference_field.0.sparse_embedding: $another_inference_field_embedding } + - match: { _source._semantic_text_inference.inference_field.inference_results.0.inference: $inference_field_embedding } + - match: { _source._semantic_text_inference.another_inference_field.inference_results.0.inference: $another_inference_field_embedding } --- "Updating semantic_text fields recalculates embeddings": - do: index: - index: test-index + index: test-sparse-index id: doc_1 body: inference_field: "inference test" @@ -114,12 +193,12 @@ setup: - do: get: - index: test-index + index: test-sparse-index id: doc_1 - do: update: - index: test-index + index: test-sparse-index id: doc_1 body: doc: @@ -128,22 +207,21 @@ setup: - do: get: - index: test-index + index: test-sparse-index id: doc_1 - match: { _source.inference_field: "updated inference test" } - match: { _source.another_inference_field: "another updated inference test" } - match: { _source.non_inference_field: "non inference test" } - - match: { _source._semantic_text_inference.inference_field.0.text: "updated inference test" } - - match: { _source._semantic_text_inference.another_inference_field.0.text: "another updated inference test" } - + - match: { _source._semantic_text_inference.inference_field.inference_results.0.text: "updated inference test" } + - match: { _source._semantic_text_inference.another_inference_field.inference_results.0.text: "another updated inference test" } --- "Reindex works for semantic_text fields": - do: index: - index: test-index + index: test-sparse-index id: doc_1 body: inference_field: "inference test" @@ -152,11 +230,11 @@ setup: - do: get: - index: test-index + index: test-sparse-index id: doc_1 - - set: { _source._semantic_text_inference.inference_field.0.sparse_embedding: inference_field_embedding } - - set: { _source._semantic_text_inference.another_inference_field.0.sparse_embedding: another_inference_field_embedding } + - set: { _source._semantic_text_inference.inference_field.inference_results.0.inference: inference_field_embedding } + - set: { _source._semantic_text_inference.another_inference_field.inference_results.0.inference: another_inference_field_embedding } - do: indices.refresh: { } @@ -169,10 +247,10 @@ setup: properties: inference_field: type: semantic_text - model_id: test-inference-id + model_id: sparse-inference-id another_inference_field: type: semantic_text - model_id: test-inference-id + model_id: sparse-inference-id non_inference_field: type: text @@ -181,7 +259,7 @@ setup: wait_for_completion: true body: source: - index: test-index + index: test-sparse-index dest: index: destination-index - do: @@ -193,17 +271,17 @@ setup: - match: { _source.another_inference_field: "another inference test" } - match: { _source.non_inference_field: "non inference test" } - - match: { _source._semantic_text_inference.inference_field.0.text: "inference test" } - - match: { _source._semantic_text_inference.another_inference_field.0.text: "another inference test" } + - match: { _source._semantic_text_inference.inference_field.inference_results.0.text: "inference test" } + - match: { _source._semantic_text_inference.another_inference_field.inference_results.0.text: "another inference test" } - - match: { _source._semantic_text_inference.inference_field.0.sparse_embedding: $inference_field_embedding } - - match: { _source._semantic_text_inference.another_inference_field.0.sparse_embedding: $another_inference_field_embedding } + - match: { _source._semantic_text_inference.inference_field.inference_results.0.inference: $inference_field_embedding } + - match: { _source._semantic_text_inference.another_inference_field.inference_results.0.inference: $another_inference_field_embedding } --- "Fails for non-existent model": - do: indices.create: - index: incorrect-test-index + index: incorrect-test-sparse-index body: mappings: properties: @@ -216,7 +294,7 @@ setup: - do: catch: bad_request index: - index: incorrect-test-index + index: incorrect-test-sparse-index id: doc_1 body: inference_field: "inference test" @@ -227,7 +305,7 @@ setup: # Succeeds when semantic_text field is not used - do: index: - index: incorrect-test-index + index: incorrect-test-sparse-index id: doc_1 body: non_inference_field: "non inference test" diff --git a/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/20_semantic_text_field_mapper.yml b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/20_semantic_text_field_mapper.yml new file mode 100644 index 0000000000000..da61e6e403ed8 --- /dev/null +++ b/x-pack/plugin/inference/src/yamlRestTest/resources/rest-api-spec/test/inference/20_semantic_text_field_mapper.yml @@ -0,0 +1,153 @@ +setup: + - skip: + version: " - 8.12.99" + reason: semantic_text introduced in 8.13.0 # TODO change when 8.13.0 is released + + - do: + inference.put_model: + 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_model: + 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" + }, + "task_settings": { + } + } + + - do: + indices.create: + index: test-index + body: + mappings: + properties: + sparse_field: + type: semantic_text + model_id: sparse-inference-id + dense_field: + type: semantic_text + model_id: dense-inference-id + non_inference_field: + type: text + +--- +"Sparse vector results format": + - do: + index: + index: test-index + id: doc_1 + body: + non_inference_field: "you know, for testing" + _semantic_text_inference: + sparse_field: + model_settings: + inference_id: sparse-inference-id + task_type: sparse_embedding + inference_results: + - text: "inference test" + inference: + feature_1: 0.1 + feature_2: 0.2 + feature_3: 0.3 + feature_4: 0.4 + - text: "another inference test" + inference: + feature_1: 0.1 + feature_2: 0.2 + feature_3: 0.3 + feature_4: 0.4 + +--- +"Dense vector results format": + - do: + index: + index: test-index + id: doc_1 + body: + non_inference_field: "you know, for testing" + _semantic_text_inference: + dense_field: + model_settings: + inference_id: sparse-inference-id + task_type: text_embedding + dimensions: 5 + similarity: cosine + inference_results: + - text: "inference test" + inference: [0.1, 0.2, 0.3, 0.4, 0.5] + - text: "another inference test" + inference: [-0.1, -0.2, -0.3, -0.4, -0.5] + +--- +"Model settings inference id not included": + - do: + catch: /Required \[inference_id\]/ + index: + index: test-index + id: doc_1 + body: + non_inference_field: "you know, for testing" + _semantic_text_inference: + sparse_field: + model_settings: + task_type: sparse_embedding + inference_results: + - text: "inference test" + inference: + feature_1: 0.1 + +--- +"Model settings task type not included": + - do: + catch: /Required \[task_type\]/ + index: + index: test-index + id: doc_1 + body: + non_inference_field: "you know, for testing" + _semantic_text_inference: + sparse_field: + model_settings: + inference_id: sparse-inference-id + inference_results: + - text: "inference test" + inference: + feature_1: 0.1 + +--- +"Model settings dense vector dimensions not included": + - do: + catch: /Model settings for field \[dense_field\] must contain dimensions/ + index: + index: test-index + id: doc_1 + body: + non_inference_field: "you know, for testing" + _semantic_text_inference: + dense_field: + model_settings: + inference_id: sparse-inference-id + task_type: text_embedding + inference_results: + - text: "inference test" + inference: [0.1, 0.2, 0.3, 0.4, 0.5] + - text: "another inference test" + inference: [-0.1, -0.2, -0.3, -0.4, -0.5] diff --git a/x-pack/plugin/ml/qa/ml-inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/CoordinatedInferenceIngestIT.java b/x-pack/plugin/ml/qa/ml-inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/CoordinatedInferenceIngestIT.java index 4d90d2a186858..d8c9dc2efd927 100644 --- a/x-pack/plugin/ml/qa/ml-inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/CoordinatedInferenceIngestIT.java +++ b/x-pack/plugin/ml/qa/ml-inference-service-tests/src/javaRestTest/java/org/elasticsearch/xpack/ml/integration/CoordinatedInferenceIngestIT.java @@ -59,10 +59,10 @@ public void testIngestWithMultipleModelTypes() throws IOException { assertThat(simulatedDocs, hasSize(2)); assertEquals(inferenceServiceModelId, MapHelper.dig("doc._source.ml.model_id", simulatedDocs.get(0))); var sparseEmbedding = (Map) MapHelper.dig("doc._source.ml.body", simulatedDocs.get(0)); - assertEquals(Double.valueOf(1.0), sparseEmbedding.get("1")); + assertEquals(Double.valueOf(2.0), sparseEmbedding.get("feature_1")); assertEquals(inferenceServiceModelId, MapHelper.dig("doc._source.ml.model_id", simulatedDocs.get(1))); sparseEmbedding = (Map) MapHelper.dig("doc._source.ml.body", simulatedDocs.get(1)); - assertEquals(Double.valueOf(1.0), sparseEmbedding.get("1")); + assertEquals(Double.valueOf(2.0), sparseEmbedding.get("feature_1")); } { From 2039fb357d7b78b5cee66763cbbb44a4bbc0f71f Mon Sep 17 00:00:00 2001 From: carlosdelest Date: Wed, 6 Mar 2024 16:10:54 +0100 Subject: [PATCH 250/250] This was supposed to be merged into #105515 but didn't make it --- .../bulk/BulkShardRequestInferenceProvider.java | 4 ++-- ...lSettings.java => SemanticTextModelSettings.java} | 12 ++++++------ .../action/bulk/BulkOperationTests.java | 8 ++++---- .../SemanticTextInferenceResultFieldMapper.java | 10 +++++----- .../SemanticTextInferenceResultFieldMapperTests.java | 8 ++++---- 5 files changed, 21 insertions(+), 21 deletions(-) rename server/src/main/java/org/elasticsearch/inference/{ModelSettings.java => SemanticTextModelSettings.java} (86%) diff --git a/server/src/main/java/org/elasticsearch/action/bulk/BulkShardRequestInferenceProvider.java b/server/src/main/java/org/elasticsearch/action/bulk/BulkShardRequestInferenceProvider.java index fdf3af80b8526..4b7a67e9ca0e3 100644 --- a/server/src/main/java/org/elasticsearch/action/bulk/BulkShardRequestInferenceProvider.java +++ b/server/src/main/java/org/elasticsearch/action/bulk/BulkShardRequestInferenceProvider.java @@ -24,7 +24,7 @@ import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelRegistry; -import org.elasticsearch.inference.ModelSettings; +import org.elasticsearch.inference.SemanticTextModelSettings; import java.util.ArrayList; import java.util.Collections; @@ -270,7 +270,7 @@ public void onResponse(InferenceServiceResults results) { for (InferenceResults inferenceResults : results.transformToCoordinationFormat()) { String inferenceFieldName = inferenceFieldNames.get(i++); Map inferenceFieldResult = new LinkedHashMap<>(); - inferenceFieldResult.putAll(new ModelSettings(inferenceProvider.model).asMap()); + inferenceFieldResult.putAll(new SemanticTextModelSettings(inferenceProvider.model).asMap()); inferenceFieldResult.put( INFERENCE_RESULTS, List.of( diff --git a/server/src/main/java/org/elasticsearch/inference/ModelSettings.java b/server/src/main/java/org/elasticsearch/inference/SemanticTextModelSettings.java similarity index 86% rename from server/src/main/java/org/elasticsearch/inference/ModelSettings.java rename to server/src/main/java/org/elasticsearch/inference/SemanticTextModelSettings.java index 957e2f44d5813..3561c2351427c 100644 --- a/server/src/main/java/org/elasticsearch/inference/ModelSettings.java +++ b/server/src/main/java/org/elasticsearch/inference/SemanticTextModelSettings.java @@ -21,7 +21,7 @@ * Serialization class for specifying the settings of a model from semantic_text inference to field mapper. * See {@link org.elasticsearch.action.bulk.BulkShardRequestInferenceProvider} */ -public class ModelSettings { +public class SemanticTextModelSettings { public static final String NAME = "model_settings"; public static final ParseField TASK_TYPE_FIELD = new ParseField("task_type"); @@ -33,7 +33,7 @@ public class ModelSettings { private final Integer dimensions; private final SimilarityMeasure similarity; - public ModelSettings(TaskType taskType, String inferenceId, Integer dimensions, SimilarityMeasure similarity) { + public SemanticTextModelSettings(TaskType taskType, String inferenceId, Integer dimensions, SimilarityMeasure similarity) { Objects.requireNonNull(taskType, "task type must not be null"); Objects.requireNonNull(inferenceId, "inferenceId must not be null"); this.taskType = taskType; @@ -42,7 +42,7 @@ public ModelSettings(TaskType taskType, String inferenceId, Integer dimensions, this.similarity = similarity; } - public ModelSettings(Model model) { + public SemanticTextModelSettings(Model model) { this( model.getTaskType(), model.getInferenceEntityId(), @@ -51,16 +51,16 @@ public ModelSettings(Model model) { ); } - public static ModelSettings parse(XContentParser parser) throws IOException { + public static SemanticTextModelSettings parse(XContentParser parser) throws IOException { return PARSER.apply(parser, null); } - private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, args -> { + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, args -> { TaskType taskType = TaskType.fromString((String) args[0]); String inferenceId = (String) args[1]; Integer dimensions = (Integer) args[2]; SimilarityMeasure similarity = args[3] == null ? null : SimilarityMeasure.fromString((String) args[3]); - return new ModelSettings(taskType, inferenceId, dimensions, similarity); + return new SemanticTextModelSettings(taskType, inferenceId, dimensions, similarity); }); static { PARSER.declareString(ConstructingObjectParser.constructorArg(), TASK_TYPE_FIELD); diff --git a/server/src/test/java/org/elasticsearch/action/bulk/BulkOperationTests.java b/server/src/test/java/org/elasticsearch/action/bulk/BulkOperationTests.java index 4b81e089ed2b2..2ce7b161d3dd1 100644 --- a/server/src/test/java/org/elasticsearch/action/bulk/BulkOperationTests.java +++ b/server/src/test/java/org/elasticsearch/action/bulk/BulkOperationTests.java @@ -33,7 +33,7 @@ import org.elasticsearch.inference.InputType; import org.elasticsearch.inference.Model; import org.elasticsearch.inference.ModelRegistry; -import org.elasticsearch.inference.ModelSettings; +import org.elasticsearch.inference.SemanticTextModelSettings; import org.elasticsearch.inference.ServiceSettings; import org.elasticsearch.inference.SimilarityMeasure; import org.elasticsearch.inference.TaskType; @@ -396,10 +396,10 @@ private static void checkInferenceResults( Map inferenceService1FieldResults = (Map) inferenceRootResultField.get(inferenceFieldName); assertNotNull(inferenceService1FieldResults); assertThat(inferenceService1FieldResults.size(), equalTo(2)); - Map modelSettings = (Map) inferenceService1FieldResults.get(ModelSettings.NAME); + Map modelSettings = (Map) inferenceService1FieldResults.get(SemanticTextModelSettings.NAME); assertNotNull(modelSettings); - assertNotNull(modelSettings.get(ModelSettings.TASK_TYPE_FIELD.getPreferredName())); - assertNotNull(modelSettings.get(ModelSettings.INFERENCE_ID_FIELD.getPreferredName())); + assertNotNull(modelSettings.get(SemanticTextModelSettings.TASK_TYPE_FIELD.getPreferredName())); + assertNotNull(modelSettings.get(SemanticTextModelSettings.INFERENCE_ID_FIELD.getPreferredName())); List> inferenceResultElement = (List>) inferenceService1FieldResults.get( INFERENCE_RESULTS diff --git a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapper.java b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapper.java index dbde641d8f757..ad1e0f8c8cb81 100644 --- a/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapper.java +++ b/x-pack/plugin/inference/src/main/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapper.java @@ -28,7 +28,7 @@ import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper; import org.elasticsearch.index.mapper.vectors.SparseVectorFieldMapper; import org.elasticsearch.index.query.SearchExecutionContext; -import org.elasticsearch.inference.ModelSettings; +import org.elasticsearch.inference.SemanticTextModelSettings; import org.elasticsearch.inference.SimilarityMeasure; import org.elasticsearch.inference.TaskType; import org.elasticsearch.logging.LogManager; @@ -168,7 +168,7 @@ private static void parseSingleField(DocumentParserContext context, MapperBuilde parser.nextToken(); failIfTokenIsNot(parser, XContentParser.Token.START_OBJECT); parser.nextToken(); - ModelSettings modelSettings = ModelSettings.parse(parser); + SemanticTextModelSettings modelSettings = SemanticTextModelSettings.parse(parser); for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) { failIfTokenIsNot(parser, XContentParser.Token.FIELD_NAME); @@ -192,7 +192,7 @@ private static void parseFieldInferenceChunks( DocumentParserContext context, MapperBuilderContext mapperBuilderContext, String fieldName, - ModelSettings modelSettings, + SemanticTextModelSettings modelSettings, NestedObjectMapper nestedObjectMapper ) throws IOException { XContentParser parser = context.parser(); @@ -209,7 +209,7 @@ private static void parseFieldInferenceChunks( private static void parseFieldInferenceChunkElement( DocumentParserContext context, ObjectMapper objectMapper, - ModelSettings modelSettings + SemanticTextModelSettings modelSettings ) throws IOException { XContentParser parser = context.parser(); DocumentParserContext childContext = context.createChildContext(objectMapper); @@ -254,7 +254,7 @@ private static NestedObjectMapper createInferenceResultsObjectMapper( DocumentParserContext context, MapperBuilderContext mapperBuilderContext, String fieldName, - ModelSettings modelSettings + SemanticTextModelSettings modelSettings ) { IndexVersion indexVersionCreated = context.indexSettings().getIndexVersionCreated(); FieldMapper.Builder resultsBuilder; diff --git a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapperTests.java b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapperTests.java index 06a665ade3ab4..319f6ef73fa56 100644 --- a/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapperTests.java +++ b/x-pack/plugin/inference/src/test/java/org/elasticsearch/xpack/inference/mapper/SemanticTextInferenceResultFieldMapperTests.java @@ -31,7 +31,7 @@ import org.elasticsearch.index.mapper.NestedObjectMapper; import org.elasticsearch.index.mapper.ParsedDocument; import org.elasticsearch.index.search.ESToParentBlockJoinQuery; -import org.elasticsearch.inference.ModelSettings; +import org.elasticsearch.inference.SemanticTextModelSettings; import org.elasticsearch.inference.TaskType; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.LeafNestedDocuments; @@ -417,7 +417,7 @@ private static void addSemanticTextInferenceResults( Map> inferenceResultsMap = new HashMap<>(); for (SemanticTextInferenceResults semanticTextInferenceResult : semanticTextInferenceResults) { Map fieldMap = new HashMap<>(); - fieldMap.put(ModelSettings.NAME, modelSettingsMap()); + fieldMap.put(SemanticTextModelSettings.NAME, modelSettingsMap()); List> parsedInferenceResults = new ArrayList<>(semanticTextInferenceResult.text().size()); Iterator embeddingsIterator = semanticTextInferenceResult.sparseEmbeddingResults() @@ -451,9 +451,9 @@ private static void addSemanticTextInferenceResults( private static Map modelSettingsMap() { return Map.of( - ModelSettings.TASK_TYPE_FIELD.getPreferredName(), + SemanticTextModelSettings.TASK_TYPE_FIELD.getPreferredName(), TaskType.SPARSE_EMBEDDING.toString(), - ModelSettings.INFERENCE_ID_FIELD.getPreferredName(), + SemanticTextModelSettings.INFERENCE_ID_FIELD.getPreferredName(), randomAlphaOfLength(8) ); }

    >H!i+l)+a$UCe-AAZl| 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

    )T!>l!BuRq-|U4k*n9 z6`rKbR$HYCer*ybp=BW8G4Nl4agKsvCh2t z^nO@_!({*bHBg{S|0(lWf-Qy8zqe&kg33l}M^X@gMw)wQXXm~lh4XGQFM3e9ZQ0fG zz-q6<8!lwx{vaY;b9>(EOM?kSb*TvAu(wurJy&V2e=%&=0rY_D2&WM5xIV?3Sb+u} z3SJn$hKf?Y2G!h*6XOe8FMz}`L|S>Fro1%%(7@xn-+LE;d~r+aS(C&7Kbfn3DrxiR zepz1-7UBoQnn>;he&z%ai5on|=8dVF9-`CU;VlCuQK=5^rdxvGF zwfoR4jtgL_p>xQk-`y*@%`##XK*JcB(x}yELEW|;_%`fAS%D+ephwe8v0giTy4WDM zF>W}t)Y&3xv1aW{jfN>HY3tdAornpS*E4zw}-g|^QWFGrDKhyb%J|FR=@RzhYiuYUVMf^ z(#g)WzH|(blR9HXhaSaenqP0epOdZ8E;GzkE3qz(Zm|9#{H<-e=YxUfVP&k2ORMbF ziPCV3TD9$ZhRwpv=*tY$O-Pvg^`x#By8Ji^-FKbEZWpbz%Z1%ZbKWU(ElZ+y>?Uft zgxCBBlihwnj0jon;$Jaek?sz+qvNr0+5qNTWL}t`mYaOk8a}0TAKDiE|GlsX?enU>>q4v=O`GyNEFaY&LyK)vgeow4SA z3!95WuM(`zmW@DUC&zY7>9Vt46f@mCr6`||PR!QA#rsIWH!nkT`t}|D?mRJf$gRRc zHg69vHR>2V;&_gtoPQbb{4eKH-KF8uv2@q_8?@Bj7Y(f`d0IMd0?yvA$o$B4_QiKv zjqw^;jeE$bWr?<4wjtc6oGMizVKrkxoE}rN2d7*R<3n5B9)^W5{0qNwi2!jb6YLSs zwR&n(`gAtwt(zNIE|u3oBXgKuoPbf|XGwokcNi&NB8QdoUIdjiwVBkg3%;)#IDIL? zr@tjnBFqt-`amVlq)lN!&-b0H{FTmwO{^F!xV1PI4Gm~fACb~u`|EblfN@?~?D^x0 zL$lP~!vPRnWUG`zPRtK4+nqnjjs}P zNrZeKApL$DO|Lq&@N?4%vP4n!d68PKZ`t9ERNEe3otT*QVx@W5Or>k}$=FZ~L;hI> z1W~Pb?T%_{`ql7e)ook_Ut%9{uL;9O!&gnU>y&8o!^dR49nsEL zi+^oP=8!e}@ZsVuGn#|>=vwAfp4nJF;q;JY4i7l{t<9Qm&+EqX)k1#~=oJ0q*~{tv z8msrQm6nnibTGp+D1ebx}5Ig}wun^o`g9Zui?(XhRkbw}K;O_1&!QI{62LE^NefOO6{<^P< zqNo}6Oz-aA-TkfaTWfi&!%U(e+&{&eW+|5t*nR#P06=LlRMxQ)WSUtJ}cSrNdyxE_~#ZJyqiF&#}ymCR5z~GqkZDYXa zMCa)l%Fvu^XLT9Qk%$vr6Y2b1p7hDF(d9Y;JdGU1V2A=<7SYAatb9^`yPQOd-t940 zV_qk>b*IT>+sR&ppp8nPH*$#YGAtl2vV{ClbH5s#9+H<GSx0g0H_JS4ptZ|ZPCC6M=IIC-mS|NG z7NxIsmkn*hn5j+8RDs`9>4Wd9cg3Kyp}SHbmkjq!7G0OlkaB^IfrDv{nG|H;k;5o! znOQhyD_EIy?2hTs=5+d!&SveqcP4{dXUugWvQ)^M?sw%!bkk=}FawoTK58xXYX*%l;Dr6$#vhhG+L^*x?N1+>a)P4+vXPjMF> zTX3FjL=do>qZd|VZ7ilTePJHLDh-LpMb_j0L=V7h&)-Rh{d(l(`B4A3ma&JojZtEC zZt9ZxunK=VH2!`!tn{?e8!y`eL;;7kS2Qu_?NaeH%JA*nfu>^Fmxt+(1rw!!Wy+O)k%wFe*FC5zsEMhNgSGAMdR~mgb5b3 zoP6@`Ag{xP36V%)Eyku*`H11}Y>iuk(t(yN5dXkgxkv;1+w$Ta5wWS0kTZ)#mv-QV z>>S%e4Y%Q-L*11Qn)2Dv54K&Zy5qKidTF4xfx4^nwf)kB$F(6}QADF5F4M%$0K0K+ zjn#FdYMWI}66-Zrh3Tdm_{3g3_~g6E@nq19(d3yQ^g5=1->y`2A=HybAO->tXo~Se zp^?!^v}<#~p(R5rsD&-r)4SzS^e|EHfkeH#m4|CfY>2;Q;m1Zc;(42&>6)o=n7j4< z#+}~nZ@#G-fn^p{l6Nw;^TVG_c!1blWkku$>u5Gx*mnP023_tgQ5vew>fvOxW8w_f z@{e-M%^-v_sP4Y1www(i>!@Zb+oxEx^C@W|BF1XS@%3+vQXIfHonwL6X=wvTI~IqeSn(nK3m6F#eVU++=LL2yA9{TBbx5zQfuARviQTbc_AwS;`AtNpRmb&u%~X_wn^^hd2Q&8o&JUk){Tw`-AFz30hvtv z==2RI{s9PB2~elpxbpsmcOAfD=A0!+H0q!ylz8Q?3oN$;KOn~@zpvdT!)m@0rQL2_ zUO&F6EXr^ndvZ2FDa#$@DVlKbZY#p<9JX73P2LFhsZdxGK%uWD-vQzmL|M?Xv_GR? z`(1g%E63fa@jp$zJ_UOU!-1%Hyjew)-gOk#QN6+KWaSPW@Ap&ncbQ3H zb(lk_foR0_W=ZWjYpgh)wbq)9V=^}kao1);S=4*T-W5B`f;}-dvB;xY9R|!UmXO`# zpJZgduZ)@ebbD#Zw?RRG8TW2iQRaMzxRDXfaP2-4#+9>X-Rw)=2ycZcri3gLkG&u> zNw>BoMSjrl`x2uii(zZH9vHpLxFK)WF%@JET>*>2dq1Z1IKHAN z@tsn>ubQniBNoMwX&!j{T+tTALP(#A{m>U%4lZC|EK&?);>EafySB_C<#xiLUfKRj z7q4?42+wOShKgG&{gYSoE~ zsM3p#esu&Jx46k7Kl$aek%|UT2_cv9oN+gbv|Mdmn#D`m^C6JDM$Y|yj#`^sH2qk; zFV7I=mhG9-%~uDg`qHxI;Ck z<;#hLmYwm_Dp%)+^-@cs=gcS8(?xaRh6ck~a_sz~6600GF}?w;zt#YxA*d?*fLdS&7GM7;)0 zQs?JSp-RxWeM&sq$Dov?4y4jL|jEnIIr!6&1pXm6EmpbaB=oxj4*@SKC z#T!lz_OA{;-d%nx0xcrxW!K8`KBt@9xgobu>z5_GnNIhR?8V~BnxQGnR7n|f`R3j5 z8MDT6a)E#p{?ucAecf*ApsH~EQeZz4byD-4?OU?)_83+JIX2y_`W=CczL zdX5K_gE>;*-*jHn`;*3g%4k%%JK@c)8D>gFN#S0jd=w2_-z=8xW%@eBh`FUx^&pu{ z?MIan=!5i{oDHwBod&^&z|Jd}{;D|ultZ-Kt8+Stkv_zu#wvrfBF(C2Vv7XtA?ntc zbj~$WUu0t9q6&9EbN-$|E8;@D1{bF@HkqM$a$UEysmyw*etyBQYPh#HSlNzA$;FmO=-RJ&^cYjFrYZj<6lU&Bs z0?$iPUA$tz>g;~G%DD{bq_i%xsg=(t%wZ0Fwqq;Y@%nJKN;EjAJg72>2Pp<_nRs!) z#V&zX>FXjRfs+Lx95o{<3`0`Z0*UvdsTG>ZGXuWRbam9*l~1^@35u8sx!?$V*3~%h z8+(3e>6=vFL$Y5QC{eG}>J2t@p(5u%HrKaPNo9Qn0%QkMu991c{t7xi$UOal!5`~? z{5eMort$mXv@%W%m1k-L9*5?%&UZk73@zn1>VK5UD=+-YjnZ2J_X*6hJ?@NR7B zWatqXCmtkiPMso#J5tn($`mSetUUTSggtx}xZ8(WZSO9#o`{l7PNwr~)g8wY)y(Fw zhnrr)<6>OgJP!~%jbss* zht&Az%Y~#v&*WRDQP+hw{otF42ZfL_IMnNS;S#Ym&Bt|-W&n=TnzW+;w!pnN?aUtS zJ`BPMg)B;4m@B@G4*TyL9gi^&0~r+R%ZHfGWO7m4j&J1gj`c!h%+q<_x5OVSYxN#a zb>i~=CZqf%k*fAsYt@o)b!gp-_~^>~Ib*(MIF=WHtXG`u=Yz&iq-iO8P@Awl`fKD* zWoas54;lw4;A^4bX6Bn7#H;#015eNBdt%>9{ ziDwCv3HALpBZN&fn=Z^>CDn#EkmI^PNAK%5HUEiUPX8#byQbU4^}({(JCS7pn2*mk3@U}HS}aL3 zC5mEhU*BK8-U^Mkv;WB7emLY|11J3E3iJOqn zIIF|Syl9hegFiW{mTH$5&}2*43_gC+9+r^1G~~@>EXqaC!Dn)?ISSw|Pm4*PbFPvb2jeH<0(?&dTx z#B+6%+4+{TqlQ2ijJ=DYGgltJy#d|!Y=C!lUx_o4)8k9>Z7&VHbLUi&qBI4SE#pNkc@5-1a7<-UMMaz6U-<pYQUk#cU3i+t_(y0XzcHt-hSWElnR!`<8o>yvM|e$ zI{Jh@Z_Dy$6%Qctg=O&YrG}0L%m9TWpLdaUC#UiEF(@{W?*-*7i z&h>Pi!D*X}(AhX)M5KElF)XY`3?% z#ch6WAj%LYyZOt4o&#{_a}DBDS?|o0e&Q zUMSM*2~)aAwG9tL57vTSSd)OXqR{w{FPZ)czGB7z%TSZoP9GSStbzy86|_ryFzVkt zWU#<6pt?W_AHmfVW!Lmr*)mCfe@c=Wq9$xMs$PGM%O{$<4*#|8$41}e=jz%oshrCi zzjjsuVEkMzGgzcYa#~4eM!96M@mJ`6*!}fkqXErfq5>K| z!La}@N|xag(iipC{!QP}LlUv1+hVC^sU~GPF_hdFA<(rZt1kS{1Ky_yY5@PwS1ZXi znW`_~C;d8S-BzM9i+ot|c2>^&D&r9G;`DMpZ-;E^@va}w<_}hP6ut6KuCR#msFkz@=Xkbj2BJc*fd+4$$wj-=0bt?k@akU_g3@*_XeX|FX{Nvm~ zn_#b0aWdNu##o(zzZ*1^;qeUs=+kFY_wMl2Ij{19yC`&+58zJJJ)f?1pzwxYK|vPl ztT!vsaU6y*>QFU+^kNJt;u^>4mZt$9{$-s-ElP+rFIVgg7W~meZ_lUZ#N!~B(ghH$ zbhO)JJ(JrXP;@#Kq=_^FC{lclS$|djd-f#pUR6f%vqh3F!OQ~OaR={BZxy+gm-^4B z_0h(j-u!;**~(wYu%f7aCd;ll{4O&>vQT;YbBBV6_(a--D{q;^!v=6JRaI&&@N0-| zg}-!Jn@{9O2s3k3e)enqgV^X)Tf~tx$MYn&aHmbo17a@YrzmcOuyj>Wa`2vgPE`-Y zE68X>-1*LmlV4J@HwKdlrFq$BuWLmW zHKZ`U{(7nEYuoDz{U0p=4dg=|yWTe{5uBlNsCyW|G&3IgNk3DfN%ML;|1iut=P)+& z>o7ORzl8oXW!BO_t*`JYYOnWJMyOU~&foIBWVKPt$vNrLDog%B8}30jG*vF*pYI+D zvd*hUgxj8BdQvPG!t1ykoM=V5f>4Vc@kAG%KP~+_e1PtH|MG#Se`h$A3RGmWkuz}6 ze4ZT*_Wx|EpzaE5o?AgwGH&$*?_v73?bcX31gEU7mI`AerERa4zE)Xhil8Zygiqe} zZRR@y!AU8Gx^gO8buZE?_gkKb83bSXKKXW;NCFW}+EfW1j*pN%rq%pd$~^Jdi|w5< z8YU3kq6uA(l;h3O{MUW5c29(yKGM<7OGzjji{?qWbS*~Mkg^Oy0xU4p-aG?cvfqCYf<~su}mbxt9nbM zHg`PJV6SpM-j`l@7;d=-4V#H~ThjV`oTEa(%}1J{ms2OtYK#A{J(`aD20#tkc?4gj zt>H;9aSlrd@j$BP}6sMmG3Gr$+H$GEwBNIzWd?~<#n2`yTnbcqKpkT#ke zE?Q@dWwmJq_k#6$NH93;cHYRwmmSTFXysiRhl$d4b58*J`CfXbEBSDUF2l)f@PKev zo#>CL2UvSEC*Pqox+19z&(Ex1eq6QmbW+ zKGMo1-Jk4H{09EA&@q}v{+laF5$Xj=z!3abgs2df+g0!Mm7H9f?^Jb(vt_F$BM)5u z=C8=$gG9^7go2w4!L3g=103UMGh`KR9b1d8EWK3IFn1^?J8*Le*umk6ovie*AY< z(PsR>o25a&!}a|iq+SZrVyoe`I-=09ANr`1bGdi}+SO_f@e8ujM@Y+HN?-GStj_cj z&9Tz;`Drcr#?1=Nx-BalG^!E_OrmaPR{jiY9M{@^;v zt7OOMdoKu}M|D#BeJE9^Fc zh$P$r0Y~~%(i>=>P}9_3Y%JWphy5M0a?xW?$CGI!-vo&dup3jA!7By`O-$A+&{ERf z);w?vzGTm(ti|571DAGFqi%Mjc>A&M2{&ybYm%4m%F#4QMoYgHv1WgK$N@9|w&j0e zxT5F%V*9pGxrmI|bqk|Hh!vU~(#F~>q*S2rjiE8nvmJ1$KPwHmaR1bdjuKN*-;N6B ziv#oJYmu@@267?I+(wc}xM--QxOJUcjaj}N7CFPOE;HZ#;hPo7okc>Rc?)2 zc8STohJGxYwN{1erE-s;b)U)14OG|_>%Rf5c*G^e?>t^u9;U~_=Iewd>TWIc`fd2f zwaW#EKh(2SwS4(AK!)7-UO7y9DKwO%bW7vs6(>bsQa=rpI1bEuW=jQEdQfZ{-s$1# z5iw}nT0UjFcKGe-d4IG2uxV^(Kr9<1pH{v8ZejMaduS4UvduYEV)izCrSphB|I+bC zR*mCx^L!_fFY-Pk3fn<++|zTDxr!PPZ5)!CxiW_cBjlSAY}k5EIK*sX=b zIK;O1D_fb2EY+)6hq zR{@2};52RyDeowZ6$PU+G3Ci5?~$7bO!?n}Y23(G8MaOh%8%BA!$bEc$q6bP(%91g zbpIg<$KRn*NA_^&F!XW~t^z^p2|)D4Llst5@W&0YP7`uF776_lYiHG(Hx#HycTn&k z3H)sCRZK{2nQCW(7P8D%xB;cU+GxtqMXq1StY-4%HAy6a8br?-L;ODf-KLuryZJp` zs8gZ*AroppAHy&$RX!j~2);FN&~|i#AIq8rxV>xZ;)0*!9vdAa4_tkFWj=2K!DH#@ zo_yE#-cTs_PBxndr0t~eT!x`5T!zg7-j2?Jccnh-p$fkZb(hC!33eMF-D}}YBlP4! zB5(U&*(SkA{(fX_iMaQd;rP7b54XpgmC=)Z++M0dD8zdgl>w<^2Y=A>zV*_sI2Y#_ zWaP`%0?L>2JrfoyCtL1|j}|y>J5}o* zPA`KI0u29DeeyEZ;&WjJuROIQpE>pUcV7fV! zhrV91S~-B9Aopob#n!^%JHAd%P+~l3KzDOcIafQ{?huF~3azJiG1YGMh;`9+!|{n- zE@x;o-bfC97&P6Idf?Rn6r{zIVApE6h2gMo4NNE@UKg<2-}G`jE-#uSR>Po0H`N%u z%t!Qv*`lqiLGQ+>^$5Ihg9q$8*XW$OPS>yY%b^Si}kevbW3 z(pCqt?BL?JAZ%_meq`)9UWU@#Cew2|TWvBossO7 zV0l(c$Xg3)cwheIQDcYO>BaG}uv^T_x*J4%A++5Lo+C%KOI6k2jI;FXll_}g|K>9* zUh5Ot&Pnk&PFbFwCoa8u^Ube7PB=7W!XF){zoy-5+*U-h=Qethi~0bQz!;Z4X|o^g z?oYB!oOBA2^83@u3+lCCuhSh69aHM#r6y)y7(^x1D_1CaT|KO}z4+AI-$uDGilKq) z(Z=7tp;*ZLg(l<|EH(ZvB?38?L-GB*HQ&cZP5;)nkFr16$$k&$9GWWR;e=EKTKQ$_ zAwWmJ$_Dj<^r6xMeev`xf3mv@21xm0HIIO4-Mw_|6tqaHiTn^JHn(R~n(Vg2!eZ_2 zhw)=2YD0vk?A+F`<4N?A1(KR*^ji&Lp(dUNa_aCIKb-`! ztF_L&Cc@Q{duGKn<%Oim&~9!W2;?3aBK21CCbrFX;+Ed?cJ{9|{L-;F6~TU(Lg6RfYer_YJ&wBJt?TPBfm&Oz0N!WSj5lZ=l=$?(K~q?aNIsVM8ibAww@qpv=24{8?ca?!VZi z5Xx7&G}BK5!iDhz!W-S0*G_`#w7lkU(f^>kd12OjZAP)b+$s%K(V9*$#(tKl7ynKv z9S}@~)&Ty(CmTKR*H>b`g8JOK-0G5TxnsPvpoE>txyyV6_Tv zOO_M;-QgfqUn%e}@NB~22c?bjZgR_mEy=SmBQ~qQJp_DJ-`O0_XrABxGT%5!4T(W6 zP<*GdqFv!8N5Y(UwoO6({%^jzFND(OdV$VXz|E}#*nK}}zmE&D_!tS<>el~@B-o7( zB|OpSv7KA*be#Kvzp?r@^g)3#G`$l~Y&$;+4(0dX?(+Q9nDt~$=oNw5)@ZZkS_uURgSS%c(0|WnA7|*7e$pGk+N>;h z3N7k?fmQ#Wd7whrwS*{VoPhc^HsT!w?LCx#wS-KN)bzWJ66O>A8||K~Gi>cGSB?Kn zBq+=?-}WyY-0UA|T3S3Z{&7{NAb(ra9?-rIxPfI5^3lz|`_=~?mhm%t#4?em&p$38 z6Z+2uc>nDJa?FpmdOWxl!!Lg|j1n{md(|Z6(bgdcLE2X9to*+gD_CitPL=k(dLU== zUsoM|>Ng<>6(oc24Ccj2(-46=Ynh|D8kL{ywvstAsgm(Dni>X!@nmUIJzf8vOY9iz zPLDa_$roO+qP%7Q7*}9)K^L8~yHCNPP0kde%oo>o(?yCx+#1C(fb3)ESKEy(Q~Aq* zLCNwn7W7>E_l)aJB;DisRNZk(v|)CJQk+zc!84^=Rq_!pR*ggWK$)d}b?0pN0CP7$ zh8F<@a=Wt)r5cW2w6(;kuY*S;A!WdpREE(8ce1K1A2F{Axi0GNknadl-Ai1VW8X>Ty6~mDA}59>SM3(rdCawB_s8;>H-kf8 z|Gl6AyOB0D8H2|$pUs@LATIr*<#CnqDU|Ka(reqTep@7MoP>7cn-eZ-nKVucfM4J? zimW|bs_l%?Vz)a;2Jk*;MW#E)p_eM`qYIt;qSM1zOBfJ$hSfimuGW(c@+xR-c`!CH zGk^X<;r=eRuYmsG_%lGkd*h-3<4u}ZN3=B2Y#(w?#M~|r?ryAQW*?*> zE3G4EPi91vU*giki9k4Q&O+vzuVsM>ahh&LmZJP-q3hK)=aI{oMeB81)&p{{l+ zva?z3rjq4GM=RQk?GZz|zU$$icT!WH!06n&;I_^bFH+V@vHp_8x>kpTnOh;QyV#B} zmqvp@r2^CmwlB{@dL3EY^oS+9V3DDAzs^HIA-Ng7ewv6 zTk5$1DBh8K-aAZezE7cE%TvK`>@Egtc3Z7B(gEfs@Bc(K`uwRN@8dZ-tCeON;^EjY zJ@1gwom(h(y!Q0KoQ{%pl_&AOv!eqiz(ceXb0^%*%yipz4(J+w7#l$`Wi}ip4a270 z19rWmhD*J^D4vaKl&v(nLzpasfvmyJ_G6t|?2y|U=TiaOogX}bm2>7Q9Pd&e zLj7!WwT$Md1*X@;SbT;^rRuw=CLJUyR=UW?e*C78D*OfG4Vo5i)9s1M5C|B@<@}w= z9Eo>f*pVS5yOnY2$n0>XeLzNDMAap!KT+fpnD{FY584hO?z-JwVAK9y{=NJV&jy2; z;F=SlK5x4$uQI+256yJoqP1TTWPUL6d!W&i--(u&vI*{;Y@&4T=*KWz2J+6W26G{B zss4pqc2v8}KxLU$mB0w!S5j;K4-WuR#%LPMK`S6?P*XTt@3kV$#~^2{NGY3}#uA9M zS8f%O40tqd}lHa7>#z@%1#Gs$HJfgSc3)hTqR+V$s{4h=0n!C$=&GW2dYlaY=E@vZy7|M_ z<>f-nA-Q?t@WCy8gMsyrHplauW3G+SV=g5&_bCzB5(ee!jQ%vb@Z^#ow^zmO*1T!9 zkkk(lIo|W=N6U^=vc|-$=XEyf#@(F7j(Wep4?yCi0w5cuZo2@DvaXi7s z2h*62X@y4GboxU}7(${EaT`;_b(SH76O}!eylvDJ2p4RtGV*r@@tu>io9~f5u#yuc zqXefjpAP9s5_h~(xtiChJ;-#}d4C*C6&fLG?|AKR!5@Y&z3Gl5WE<%U1^-~d!lvc^ z5QIWP_55fK9;!8%{2u1KVn)*gwR__EGB_1y@ah)3XtwF-_Nh8uc;q0VyDIXav3M5Q z+iLY)P;}bbj3@Sn0-ziJb(VP8e34eaIug7KFTnWmV+G#zh_hRq`1>Ai?Z7ha9eRL9 z__6Y4nwcG^ltR>Os#b5?T5sTD+D$$I+tBA1_#@xxb!*MYYBKUfxP>1|VsxpWlhC)7 zm^38NhnmRP>+WVNLRQN+5dZB&lBbNA zT`Ge~@toY6YF5c(>bS=(Yk#4jp)+Vv`JX)=Q_c#!k14Gu>fRbrW+>YA_vDkk$9%{7^X`97cy$DC6~?vL~KxbDymn9MfK9N>f)G7H-Db$Nw{D$ zT8K#3O!M#e?k#SRZ0)i%eMvxYE;}RVam}^=Z0^VPCG_^Z&gg5B3AHOf6}bxX*9pcr z6CH}bz#ao)MHOwVCUa~D`UqJfrZjf^(!QFB#GIhaF*9OmLe2~I>+x3=zb(eoC|^f2 zz@ey+oC8()$|SK14~T-DA>(g&5)BI;LjV_bbaEFV(zB-8Kq=*r?Y7eC@ENROIh?mu zsJrO75Kv%-&!G=G163EVevS=dSAN**oLd-fY7ZuaZ@GI3QgFdy#EeAF)mW3DVGY9W zRxwFz`#YQNPtSvih#r(uMCM~-Wpv}nM{7`7J+u}iOh#E3<>Q8~(oqPZn`ZuOsq{mh zw<4jcK*j>(2_I1|%;3r6xa_AI*#ZS6H;XZKtV_{{?6Th;hUF#xmT(q1 zs4;6OYgEAUY+al5x-Zz`ZCrbm!YmhKFgRwPCIEepMxi|Nce^em|2%Q;dg3xtIPhY- zc=@qpwe{j^m*88VZPNL*%USWK*IzZPrxeTmb=zafD0P8l6i{nzn7+nkwH9N)G%*hMRH54n3|ymM=r{grxwbyM%Z6r<$k zx#9KcHLa)j`p++2%u+9`>pzhst=}!RF)r1)ly)%>l3d<$n2tWAoGVVBk{=+K#_i;$ zn*(gpDyQ=r`p!*kiY>Eql2PW6)cgBv`luLGnrA$2Ju){kbekTQnQNQ7uo}H%>y*cV zDAT{mI|u}t&XZrhX>bpqzThU+Bjj(Ts0Mysa)O# zbM<%5HIJS4qahy>Na||W{Y~35#t(!L7`D&Y&-&yNYfDm!N-gKmNv0;fAm3c^JCIQ{ zA0FS*jV_+sHlrPD#cAr0^QMu)?nO&`u(vNl@A{lDQ6uwGddIHtP!Fwl^=Sx0dlGpJ zuTgBZI4>ePaQUro-q6wIq>bX+2mmUC2Cn2|^}&xAX!ACKa!DrwEt zutE3&vEYxI?{Xz&VLbX<#wXg$i_vr*f}?F+Bs$E#7Eu$hMIdxWY%^EVDn~~@=qXz) zTm-+PY^CnOR$WBn`RZAuSygy3p-3C)Eff^3wCjt`?&6-*8O4k4z|a}&*&|*7cda*6 z8xjq%QyBB#O<~pO8hw`nm~DrDx!~om8!Svnm-7aVtyL)J$ocv-K-9e*8Xi-#nv7R{ zyOdh|@j~1+oQwS1nO){om9)XnjiR3cyFUF}ttDKFU(|@1ui;J`Bq!7r)Jd-wanqi) zC^U)l3K;sjytGm87BJ;jU5gT^KQ{Sy*^DHXLB2>QKgS(tu^HZ5oIl#5PSx%`uQ&KS zpaQmdiuJgN;h56{7LlT7Bee$UTeKy-KRb}@wyma3KZLg-2>R_hSC>g4Rc z6`ZBV3rg(^ujadfWd8zIxd#Co!CC(0Fwe(sQ}4=fR?EA-AwVA#lL8HoJGXi|QIpb9 z8c+IgrZksrz(vNm#PwlV-BKO+58!diwLDZQz672!baUK@7z9~l4@m23P0KqsGN{Ia zjFdOLS2}Gai^aJ&-g{|V(R+bUDrJ!&0)gmxGi4&7T61t@u)D9iSe6d_se&bm*4iYo zNfb5V$#6cKK2~oV`W&sj%`6*O@swXNZY4s}EWGKj=PbXmZapA()788-!=4J}-dP{d zKqdcK+O&scO3+x9i9Bx1(?l~qw1iXa>U|)a(YHjezh`P48>P@a4itBGi7_0&2B>nA zfM!PqNZlgw3CV`_xI}*hhFGaF9@HhLr$rx{2Q- zQCtPZ1K05_kkmoqq-vS1>jW-^563GWf1EyLCFkjgTs!1~Us_!xMmARqSunQ!a5o5p z_5v;IXDnA;URMov0TPxWcDkU0jz{Gy0GY=Y#m&-8$D09d4>=<{3~+3?KK` zMUjhMz`6KE;Idq6x1;RTn5Vmlfdxa8tENVn;V8fo@cXI03FbeIu8D0^1n$hsg+8N* z2W5jA$khG?v;isG7|06e+fieEr)eg|?cdl;Y9x)hCjPtMquO3qbPf#bPB@lZ=Q+q6 zj*WC#L0(O1YWz;+GF~Oh-p)enqmAJ!8+@Y|_tb@A*dc~;lb_51F7{r)0trLMz2zK_f z7h7ee6W1G{t>GufYOu9j8qxGXuf31jJ3^wJHJD*NoGSYU6O@a-kSRKgbi#ET3E|R* za4)QR>LH1^c{1ic&!qE5hq~}FF1^TT1pr3C!?j+XyfzrY*XQjpT2u=URr^)ffs^7@ z9r>&GNjslQ%f_E!cU?qvl;!h}N@q8n1a%Gys-a;4MS}gdk-rT{=QB^8{SOsUVs14MV2ehhp)S#^=>=V znKuq;iIcgGUO-+^-qQJ06rpzSBFHMVyrA-dy-}Vs*=vT>3vYPVI8af`ZP{cWJH$gr z=@(?nR0ovG8?)cxkFnx1$L8*Ii-GEx26A10UE}-$)z?Alz$xv?bGw%o2g2g{hW1UF zTmJ^XvoNpv=6dgYv)7Yyg=ZiAbqe$;?{R?{kW98S3l73PGJbB|yWf)X`d^DT=pOc& zxf9F>-l#qz&-&s`x^I}bZ^-A5z_lGaHuH{|m-EAzEP1HwM>@vLqWCta1Bb6{%X@R=r?lXg zipGSj#ch~m<^l}Dj<>VO6doJal~>T_?>4;zULn`sse4270E!&#V=VL_7<9`$v=);A z$-Z$~{VK1%ZI%gQ-+k_oFFH`?NaRq$U+F%XipnUa8V|L1^wq$}Pt$7+tQ%frb}#r*=0en!x4)gZZC(!cn5 zH52mv{U@}ohW;P~+3ni07a`Vo9RtIr^+&Q3)Afng)SAOu&Ba*b|<(TN6P83ve;(uEi{uCu^u@G%{#ZTv^YcTL3 z0hj?7+|)K2Hur`nm>gM4f^Fe3iaUW20}#;&6alwkyq4Q#Qc$To#6Rz?pN93rCIgEe zQGMk1?7kM@6!lr3C;0)6^VIq&^n^W4k3N%@w)y*S5hR`QZT;1#Mo1KjU!=}H2qX6e z65ju=X^!VBX_?f-T`s&RzCU zwv|EoZdn;g8tQnm>z{SU&HU8_^t6AkwYkH#Umh5z#fBt{@O|SinVrtQpy%p>2@632zVd^c;@Bz9!JTIqEyCYuX+w3S# zL;(NURqseV~*BCVod%@qN?>7&?QtOlL2uM^*GXv_4CJAv?^l5Vs-F#7r|bCfC>629N&O z`=~lPka|Jv5EdG`ib&_N9qFMgqVnEiwQAQVVc66_`tfF558MtgJ9@e$R>LV`Z#}X_ zuSE(J-s#aV&cNim{5>~HtwPCgQV$2R!fE720rW$ZnXL&Mvo}rT3sCZj8OZ9NY2Fdo8+fY#Yo(baj&v~5bJA%*@ zUK4GMa)I{UERCg~U9HCIq=Zz6gIaZ_5%vQcJUkfse)j zppV3F>q&x?E7lE@64@3R$}AP)pVT5Slx*mQOla}W zp>xM-IeS(iZFfcRUBg37Io8dSc>kcn`FOA7`s?Yr5AEHOfR0UNa5y#&=iUtfiEd{= zt52bW!`$Im^rQsBQJf*}-j6w^GdoFB*4ErGEuzXDL`KbT z(TT3|vs1R89=oFL1Bh#IX5Hfr@;62IjdQxlG?X*Fl;gAl7!k%MjUV-78)tlkW6?knP=3H zz~)bzvxeP;Cl=XdHl@bR{`j^hikha7mZ}O*X1RP%w_{XxX#@tc6yU;#AtJ+ai7o3l zjhudOMXkE+w|*XFaXf%U^tKPKKfKr(N`_U(R@5{T%UPk!*jMfvYZtt_Vkd?j`86~f zqMh>!QRvpneoMIcQW+N+_k$1RAStm{SVTw2p|7fYm6sE7J^yK!sSYgZf!EzM<9Mkj z)g2*-h5w6AgDu0~NgM=J=%a(ZbxwX*(|Zd~j!$zI+Z|Ii?$(F;gG<}jDk%fkx;up# zquE4f^!`up`7+M(P>Bh=rJ{ermtd^Jb zJZ#kA5+*` zBERSKwpR2$L?68N@X4^C4Dsgf9$sjnqlwVySuu& zx~u9}RfEaz*7XINtH(26nhu-a(=wQmOuPp%lboq=Qk^_#W10iE#%srWK8 zpk`$}5^Md%XtMgF8vvsjf9K3(>*>g|-ks7Y)AKwn9PLgw6=I--Y~GF4FB(jH)*E7t z|E1P`^R91ro&w~^^ivRie;7yFm%O2}mD>AHHHOU{pUA{9bb;Oo%z@=PTU&yW!nSLdlj9+Go9(lmqzT%(CiN`c}OYPB+aDXlaYo6K|7M-xmk)f6rI>QCcB0JqFNAk9ulZ#rzM!^1Iv zLO+M!mEy1-kZw(T1AkaCXM1zJmaBRK1|#o|6&l7nBH`Qen}VkLhsU}B9pXuLBhL)~ zR$i$e5=aJ8YO5JZ#XPb1)vh*iI=MSmw{BuYVpL&oLhB=>!7~B_hhsgv*~9}K_2qK8 zFYs7bX-*dn{t#8X`vbe1!)o5!g7XNq_x3sto{fG^0483<(SD7u{rmX+~MyG`!D!YE%x~l>^Y+BF;E`w`!W^2fhP}rt&?6KMZV=pVU^G! z^APs+2zW~_4fahSuVA(_QM@NNoRL!?yZ8o{E%|n~yhW~ZSCCqutZ43KrOd{1lKhGF zNNXyCa~P}YYnSud9wid3O|Q3O&bDCnP)>34p{06+<9Yvpltv|+*e%!loSr<_KMazi zdNR+lI%R`!z8g~`(%tz>m9@;~Y2JwBe8N6>jZ%~Kdbjarr$Tz$J2y=xo_@UvtT_SX zB5?^&A52rUVanjCh+eBFN6Y=sE_mn!0_F*C%}$f?((yqyYZP*g17Y&@RBrTR$HW3? zal8b&1u)RR_74iWRq%>c86PE^m{(VJE zq*uCf>(MsNy%MKF;kb~3jBrkqHxfgxSruwrogOXUEMg}s_}LC+`-jD1u+|f~H1Vuv z>~|)#VBx)Dmk{*aSYF+`$0+0$U{04@lh-*d`Z~Xso*b8(5q$-KBI%7m(9TM5`MAlw zRX+-fO^Lt4cFxl5o`RPA-gu9OwG*oQaJxMWauI|kep)72w6IrqLvtSg@Bws~;}us; z6?t;@!^k9cv56w%AvZNbEB-fuO7o;Tf!C^P0lkQQI*+zui(JS`zFD%NHC|Kr(7wWO z3ELCj6$sWO5a~W#wRZ+w!%*toOg;SV*AF;UsM-ubHk33Ej$TA$VWsqam({wbeS5_U z)q+b$h_S!-zz78$rJhG$>xa8AFbyPZBy4A4SyEw3=YN2X{AcHx?g8xAKd36Y8qUy> z#_I~1ivl6j1fQ#yHq4_1@i2ON7&inl8pobbVP#C>>+Vjyj$rkZ>wSXC`Be6~V!qRc zWo+fT(i5z0iQ6^rxnu63jcW%Z(Mt@&d!Ms>86P|?%;^;fTzRB3L&0w=y4S<(DWP4d z?wsw5e>A-{@D@nS zr+jm!GaSX}?9%s+;sD4>Cx$xRcDKqQ@_z6a!fEOrhHPw2P7f_uX{Lr9 z@~gispD$?eNg9BF5QgTi1Q{`1a%_A2yF&{#y zWBPiID*zEJw67E+Rzz2B{bk^p7~q&@oz!6W4;AY#OD~X;;b&HR49HEVzp3!ATV;W=mAr4~epkux=UAZ_ zasYtHz4kNz|AhRzP7e$-CgUT;|1eBpcX}Y-e7&|v+&{_lxAE!IJz$u!3iHPB`~~{v zX6cB9wN-xR-#0gumxqC{gyvS4GniUm!!9_X?>{><_swslTb6 zbe#(O5%%0hT|KSN!H7^Z3Ym&8H=HwXuNJ|keHROl!cLuviV2OS6VUXQgsc}G-+YD% zfbqP6B68S^Bv}p0wxe8vd2`jyT|hF`%(F!x4lkF}^4J1T)(dDQ5H?Ikw z<4fW%&B5_{C?^641*s%cn{)bc8e9o$7kYQ+nA@jQio7 z`IjZn@zI;ahI=~Q>vWW*Ht2s7@DT>uO_5_MY-(M#79dZC_Xw=^{-9aeRoKndxe?E z-rp=qrQiK0*dgzxUn)=+PkD2yST3P+E{y5YoB7#gm6Od^GgYdm@+U$YrmA`8Hi~9I zT2)HwpeL824_eVFgsh}C$MPj&_#iKU>}hI0kzemilEV7S&`ifn40!;zV4m!651jX2T|fTVTo z!->I23IaZd9isLSx_eI6LaSj9pOxEfl#Csn+1phCl`L<0hnEopj%O=AN~O(EyW^Og z&J4L(x5KfUs?(ZT7Q`&_eX0J5E*S@9X=Pl^K17GkejjUqGE{fdWn+l|teS$cbQojW zeIDYLl6^MSc-A&&sHwWj2o^mb7^u!UC|I1S(5}Ne+Rq>``Zt^WHCw_N_}IpCI65th znT5> zS{utF^HmMhe@L|g%(ssZS8#VjGgA{D$I7bA-9clK;abKpN(WVl#K?}Z&^dEBs-i_- zfcUXoG~G{+==Wp%Iyb}wu1;f`x%_7wz^Qhw@VR}v#Qw~whbG@1ZZJ%Q?6*$cj4pru z8-e;zx(>y}{n_U`XD9mn-L+~owg=`aE!ymB3=4XI@BfX+jV2uITbc6HuwWq9f$X<) z=io`!pTK}I312OMg@~I|k+P?@#)@j*XdYi&7fRyUn9f;z)A=9zMUt0_3Q@%qd z+7(-?0OZf@9?UaAC8aWjtzz!w>Fv?XKWx-B9qGks%7fpX<~MjewF>rk3#z>?Sf zaqFYm7*L3>8Jj_#_krk;8{h`D!{nEid~t_;@%`8i_~pmpGpcY8jN23^xpdC@U4gPr z-fLUF5xE2h%WQ+b7Vo}I+>~P-%6Az!lb$4ATN~95$4nwoiALZVQ(q)qeCL8ccAlFs z8jVL18}+)Cgow>A8zmp!eV$c23YsTY2ZLDu+UNh?Hvpirx3%ZZ z`T-)BC$Pi!9ZcbWc)MYg*!L`pT*ggb?OcGBU4|AY5-ZE=fxKYcGnsE629OKni5Ghq zrI@$v=4athqjfrLOpS=O^|dy82jSZ>hO?zR92l=#s>VF?oGn)Ix&QQA*I1I%yH1ar znrE~5dL#*(C&ELYTHg#lMz91>dh=8ZQl$jB)Gmh-&I!C^xMC}(?b=W@r!YU|XF-R6jYuR~g9$PR&= zP{R!6z}KVhsi!7(1>C)z(Rh^>Jjv3735P&ME16(3R`tsa$ZDn-gJ0olZ3c@zO3e|qG}flCm}Bzn?k z!uwlatcUYd6`z~!hE8CxUY@1I3r|Fq!WPP89AWsF__ZXR{`eC@H~|>i3z| zYjkp@R(gdX9rFw`2(pq48GJD<*Argp!v-nGhGLUF_jJu%#8@fd(*A(1W2cz6n<0gM zz56-b^^PX;HITWUI`p~mVUbjg(?&ci93wzd?x~9L1FVkCXI^7k@QNZLcxBC!HH*Uh zwaXV7n`f$cds&42fG%kIRjcjJWR`NS(bDbu!FDaTntY(cd`-y_eihd1wuBLYl3c>y z`9`^DH5Ca)ETx4zJgkTXJ>VZGTJ_wTmDhX*BvO!a0=fyZRTnFjA8UV?K1_H?J>=S7 z=~Z!uV#dT6aBhFG$U{WZt7=;Z*;2nCCIHu%-b8(NG*xb$rMSy8@Fg!E$Qz^N z8%9%Ezh*urQpNi#7l5-`$?KZ}OtNH}yU#?Og;9*2@qoQYMdU7lZs0XXdMC2?bsa;k z4>U^YDcNYT*u3$*4lVA-T!SKtV+Fuky%n!T7#E-OiSf$BXZtWCzS#EtSO1NhBux4 zyu#-)9K6ESs_XeS&63v%*X|YpnG>r^%+91)lza#W{|tZq*BKi~?}ojW=MTO1*&!r^ zPWrpT*K0sB+rc zQ1nE$g33Lc&kPhlyv=+4iUh{o+J&a#gqYyW|I-U-wYYk757{ius%11?@H!ThI%(jT zRj+${1;ih+$_q$bQddfEFM^hW;8WR3}sg>=~^4Q@N}_Qi}~`-8`XIz`Q# zrrckv&+xo;TV=CYLyl0csy)nDZl9js#LHJKsMLMHn}B|seW>obQliUe|-7@|ey=EJE9MQbsN(lmRYAS`FD>+;;yuy}WaHxb~!% zOFL4dhWu_0Wh(;%);-6STqUX92Jdiunc4g=wefo7`QL|`zP8|QV03S61PD1~1<`M< zV9(vsq3*zE_GrWCoP8nM1mxu(c%O7z%fNn4+7hS5c3JMAxyrM1szBuZ-Fx&LDM#GD--RfS#NWbo~SK8Tq_S1Gm^_2 z*{Z+{Egh7-&wM>ufKq5p^);V!AI?GRrWgEZZ4}41j)iEHYz2jBb|Su*PVw zpa1p^tHiBwIRRT6d?ea5B@#8gG0-DJSOuICAwFd06KE#~jB}aN@uAGe*PMv~s`}>6H^!lwzeU+# z^26S*D)G1sIrisDSXxcr7rr(*>#30C__nKqM#>)(P4o!gC6Y=t49m?cEIum}jJbObS z4qBXG1OXn2{R2+<74X>+MFbT0(D{$}`(H!-Q`M&q{i$2x(5@Lr1L(tbaj&2=Al;1~ zqlCW^H@HWH-Si0UIvf2M-Gj>Yh;rX%pp1N2dawPsKAYg?8c0q}WN3Cyx0!p!4JQ5? z!z)0Dg8E{thGVtaS?NZm*NbZpp1H)#`{`p$&J-Zz*a4T#e9GoXsfhn{GFEp7^cDTf1U%hZLGcQP1i&-=>KJ|#Wur;?!N6_f_y%1I} z(o2m50K2B(rv^`*A6M`-)BphKui6bV}1IAio?EOY)d}@J*=;>R;T%vs;0V?>xVGf=BG9h*~+l5rF@Vlg2!dB`$AUV z%fBA{%S!R{C%!y}#+jt2LVwb&5N#(wONz0iuF@>aSD_TY)Z*HK~ zW?-b0Znk{SZQEhVrCvyE@T67xOAQaP-yDS~S{5SwUzF*N$QO4!X9}hXLF+#R5B_%P_qoqd#(>~!c9T2Vp zuz5?V_qwbVVQN}Sczft+m~>q)%{C@7{hqK^)<=y1XX3ZW%ylKFE{jb3GH1xZFblEr z>pT|GwEcjHe4m6)?7MlDi%H$n6mMWihg-xks&h)b=4vTJh4tTGw6&s+-_iyGfi%M-Mk;5X4)zA zcEk#!M`!>3PhDz2Fol7HYeIwz1Inlm$`4IJJ8>*pL^DdRTLRxR3*9_ zms-Gy4SVA50b$fYC=#6QnSR&o{h23`t+&OsTu*ToukUt-va+hxUg$h6DB2fB@PLo1 zbmD-FLqP!cL;COs!MH&m3XdJeDk%qYOxAaz4oUu`GbwY^AF83=1md&b{9NXYs30B3 zhaz+*^CT^qn$8-^)1)KB^(5n2OYqhrg|@u_ln0JR`sR2cwzm#DyJH_NIlr2I{{cjk zP%i!aBfn!z>Nmo=n1Me{Yr-F~$@#3_>K_U(E7Z3KmS0hX$nWylofd%Pk8+IXSG~8r zUg2v}bn7IO>5MQ(bt}GzZe4ZUO>5B3xGx$TRk+LmO*IZVwP+SG%YNwza8%u)Ptl2MkL;lWdhD^ak0^q(9^^areH1pOM6T?UYIWA`8K?gr^HI-ZpX*<+vcj{o#a z0A~o0`83ftq_@lJVlvQFF^ z;o`W!9>Ya$4Nvc0Z-v<*jfPywvbWuy;7K z48GWjLjd0Pp|;d=o25Wnn{Z+v>g{}|VK(aLh=j2TB*P!Q)(B|;YLhsm?h?6U>IMBx z#X9KEcfD$CBrj=!XJL>`32yg&UJgo#q#1MZA#7KiJ?>+slNS4>W8pKSDPNQ`yd>8M z$$#1?Hu0RDtm%=8zK-DE;uN0(s8pa8;xPtS+F?V^kU(-B4kH4#r98)uK4ykGVb|Vbva?x)4T<{lX=ILp4z=>P3kY{jqtyq0vG5QyFKm>B(~N~m9JgY=8yS# zu0~QnnvE59bktee@+4`+A%Qu|b>&{^3)&ygMi91#w24<9hsDh!S08?4SHbvuEo0lE z8NW-%zfZE(vsF32A4Zm_r4b(Ri_d^iZ4y$w5;{!EPQc3bz^>g*jpS_RryGkyp|S( z#_KUTvp^J({#Q1Fk30CEX6;To?CeCoF8V&=uMd9gHxm@RO;N#r?^lY9l)$QrZ+)&~1;AgylTZEhLKo_3{x=swWF9<|dqc)$ojDP(Y^^2@P zSee91-T;RI@`h5Y~w1014za|6VyeBSnb-pb$e+~U_LHCEuB1|WI^b^xkh~l=uY#TSaQdI4FhPm83+HE zaoU^wch{_*2+uOzVIx%(|Mw7q*F4esRHwWrzW86m2d;U)1iEA8*q`z2ze)E=cn_eR z4>t|g@2-*G!aw$D^c4@g`)StP>JKWvsRC#> z@Atn``M*>7|H7&aB_F&?V^ay!Zu>akZq~Ud+FUmq{Vs3t_x4kxlcQz{s?DO57ZnpQ5mE$1$(9Hn?rC`%i~t;B50Rv zlb1`D+8(k}>=54CHSx%yCclp3KG=aRQ<-)gtTojT=#|#CEHf@!@;!chfk)gq_H`r1R0HOg^{u+ta7udz-4*R!#7q^OD4%>9DbWo6wWI9OqK zXC9hI()#jmSw~HZ_Nt%9$T+|_S413ATu`^2&FTF?NY_n^9`;S0<&VrXnPViN`rq`5LOs2F|E_Z#OzPpp|i}>+`pol5}^-u6Rd1UOX39|MM(Fxs0He8 zkkEyHvCm|yM)(GId>>hj5ZML~_gFb>3v#~B$w}{Yy0R<1NfDm1wW~VHJ^A3Fm1tP1 zWZ57`$(gF4nJ3bj#oP-~txj;}(?&}!v<$~ikWZ)P-KaJJ zbE?BMho?s41T4nNTPF>0zD|%Y72H#kPAU=4jEj519pA@j1lD(zCob$TnU5QlX;kNh zd19b8Y}anGbkeQr>tBMW7|-oC`zmvL2EZgG@G`HNZs)0!LY zR;PmnTHnu09!hJW3{pom8I@bT%eUZS`{Ocj_$JVTFy_snQ47B#t-%&`9e5UvOaz5% zpoa6%iyn(%seDLEtFPy|KBupzO-2StW%lr_7IAmuv$moZ9+Cd5@~itBHNgRtZQ~Xq z!~bMn4uaCGOvv*Y(O-(u@>}#7U1=wz(3>2nuv0A%hD@v!i|+00uv(5qF)fgog^g!B zwAbTiOJBZRm)tlt-d_F@P^h#OdQ;lIEsLcxRS~s5urmu1qikf+i?u&FjUO=|PlM~g zaq%n_r7KD^=-eI8?7y*~%wStulsd<@8<;M!PC1zeSgdr5U-xi{!wk-+IV| z(!E9!--XyZyo0t%y5?BSbSw9A^64Ms2@K?_;+mu0Nqisb*_lw9rGCh?X@GQ{Il&#C zH9#zio)}CUMD0%6^A?H8@T&KYR`|%M67#^EIfM07Mo(YRjb9tTj4i1zcPhc9rx+17 zNe!EtIJ&exdQ6{DVYuVAMXadqZ=^0^k`|s9q!(__Ny-NzBe_hf_+8uu*uvM-63Tton2Ese9jH@w6eu!zpy&ow(`3#L9N3bGJDz z)qLKkZtu%R=T}S$75$itEdrNa3_^#y7?>B8Q+7rJ;nnHor_PsBmEw~mx5yk8r4~T# zr@U|9-LP+afAgzzYM{oXP>tfPN>qHg0S}iAXZ&w5y+eB+v0fxSswtqT`guk|Ei`rwWm7 zF)+qm5)#3n_aWj&45c%Y+tOUzIrtp4J-EfDH&?Aa+L^p_xm@kfJb6ZNKRpYae%om-$zS$;dLSGp=i+0> zjR&dd>t~i#mmv|^`Uj-@kO;e*WY#3YrjnH#2B9w6hn7XljQMJyW1peZq{&(M>CdF?8DKOk>=`t0~N= zW2G{7kdT%svOu@IjZw3Wn&r0aDz0}WB?1n@jrH1lUwup42c6J8Cr^_nhHsLlZ?a9` zPF_|BZP(XmeQI)-bf}IJ+Pzfu_zr@?3cjCg7goXWg2X0V70+ zIGf4NdSx&N4=j~jVhTQKL$rDT~FWkJ?A1w`wyTl_kve8}%Uondn*8T2A zEQFXNa3DbqJnBA4YF1Cgp;}!z#ffJ|DA}F9Q(p1Ck0Uyh7B8{j2A?%QU{m4NEi_kp zjSsKI>%1as73iRWJ2ARDLqdZ}d>7BHRirNm6P9evo2^^Rw-$rvSYqNYoIOZcT~j#* zJ#SP`xmdcD%d5XKw~Ge_*dLIxt}YoA)J@s3^@LLu6TL91*eOiKbl1Az*IxZ}zG5nz z`^d(0Vz%odVf2GD`f1%7)|oF;#FC_|l8I@okRS)g>hg^C-Dri`oYr*T;w10qCmvcs z+v?xXN0R2}knSE@7uIRra7L2q=Mp(YDz64c)uz!&ukB+6pP#X4mrKh!J4#)Th7}cm z#q_^5X}Qp1#=Aqsq1U>7E}UGepnCtlepGP`Zbi05-$Zy+neT?v)IQZ-d1Wf+CPsy0Khfp)8+&0GW^qyxNV%$Fqt ztP-bgESo%RuC*=Y7Z+@#=T2Mv5;Y*@w2jYXbHUXXtWNey(1eJk;&urOM<$_b_bo0Z zM-+$hw}%tr`I7a@PODB&Zk>49XR|Tq_7T@#Bt%5-gf_aX-*f|2&w|%*%pb0$0mPq3 z)=yAep&G9G0kl;*(!Fx2n`YL_cwSh?5+QL_&?Nm7G)l%KMBKw!vCIft{=}pK;0V~p z*~;bMCT^Qe#kEnA3%&NblB-xxwDxf@=>X>TSGI7>Er;&t0}nOlSkNxD({cyve(6l< zJ=1Qzc8jJwh?VS{YYTqyqaY84N>5c3j4#*kUZ@SS&>W|jDanq7iLp8res2Z^g1kRn zp77gsGIvrKy3ZqFj7&OXTc;k8YL^*dY#bTyb@D7C^lfT&r>_M&e;F$?3C}@H#><-1 z_G-e1or+e%pA~N`oLt7NF-z=sR^KcorlqHVi{7x$BWvHjyA+$MsbzUq>ohHB@3%=R z1Xd);H{DKinphf8+arp|sZw`cib9DsslL=v}iKs?#6x+3J!Inm8 zRnaTbscZtLtfn98c(ek?H6P+Y7Upq#zU5DGxGBn`=P2e{O&CO`&2rDEkZpRIFKvmp zMEPa;NsdP>(p$g*=E*`5As<2zGdi~8g;DBXzS*SIb6R=m`bNX&%gZv5s}(2zCPE5L zOd}S0YS?t#u*YZ4S+KX$K1@h;4fLL>a+tnwOBfbJZrRN4b%HRf(;ZAAI$*_oJU6e} zFK*pxa#3z&sZkQ~v!y=Umy*-&A78W9TS9Gxay{GK38@Mv+pskw;~rO$+b;W1vOTr% zy}kRTT19m6q@2J5>lv6V6RG-1Pm10C%7PltKhgXpZZ(55%{KJM(q`tTtj)^*<4AnecY0rY~)PU7ddw$4E4`9zHCoBejE`%RIM2B zzL1+d^0e2j&{ttn4@J_il%D@sxm0*Jx>-btyX0)%j#wzF^FwA0N9nL^n^nd4>8`l zH4Oe8e&BMtLA3v+B*|`2!?l};GcO?;_9%c(ApETSRM}Og0-7mHDtrq?ehAT0ABSBe zhA88b1#F9$ukq5uif6KCa>E;HRPx#0ZG$q4N8!UPi_BXsXUHm4E+Pi=x~6YFDDAsY zxwh0;q0;FS}1 z^9WIoP|3j3OEt$AY)qqQi~$>dVa3-9;srq=s%6h#T0Yxxq^(5+6w8vinwQGs6;N*9 zPu!r{+jCednAK{1%*jv2VWJAF7|M>P3XMvD7Dx{8wsP@NzhKKAd_ME&nk(Y_1g9;m zP#|t38LDho35~1e68gAkzRU7@nCvb0ICDzkJr;h)qleQ7Ax+D0^EV%*2VJ0;rnxIO zs?pSoo2MUmmHGB0Uy>ekkMWj4`P3~n(>Et7jEiBMOGSa(riNc`ADb`OQ>+r=dNuUy zIaa$_6pV0=i1ZA+*2V`)VH?3uHxC-MgqW0o&?(S^nd>vGK=*q?iV^cj$EDR<9q1H|9egvI5tTI}Io ziS}jN!Km}QR@An8FF%(Dmqm2GxSQtfH@fWyyQ&^M+bYL= z`chk$Sh(=|+URLdJG_*JGI{CESoz`L2q$sTNA=s|JlXrgrDjk!(_+xeR))z07H#+eZUpyKMg2nWnn&)b`z6re59dq5Sgmb~SE6X3x@(&q;7Bl<-cTrG2@9|I*OZwzD|OAz)QW?x*4o zN7SxG$0)@S+eUAZac>Vj)$9}MXRmMRnRsMVG#oowJp9ko1pX5>>Sc6o+>I1(yOn|> z#Y)@pZs#PvC(@BNh3x6p#gnp}=JxO3^{PA6_JxvLzciUdr+w?WOsJZq`I_cc)Woy8 zf&D;gO$u%4Xvq$o?z2UDY5j~6a%=~!L?`Jpeu8*JjnYdx-szIAHZ*N9jrCyiye6j{ zed!eLlSzjahrQbZeanIS>#O&L=w2nBac0HNsP)uB%(v{L!X7x9=Zz6waGG#WB(_hJ z%nhWOpWMTH-eYP`@ExXGh)#0s6aI3Qx3YJuaPd8^?KqEOc#iMFQB^{OfRK2r7N4XC zzs0O+j4a_BTF%Sw>e;QQ&EMpVmO>wk?YpsLos&(gKAAZ)_{PF$`C&2`CG-7Or-JN8 z=YfiJ5!f)r*sJC;n;xsh(H1H5WEp)UhW5K76xJPG5X=5>#Rw^PX&6u&7URmQnWQVB z#w0>;)!-f~`;(;TMRwtQf?;s-syuZ3%S6b9@WRDlNz`4`g1Oz$w#SG-3Ug-m9dT6S zP@SU`SKBds<%RL>gf5#)Y6TIOSudmxKsL2#Fp5Q#!9Pq7kviU+jpNT>9va8kiJK8?J}_n?Xl4;lK{)* zg54)rw%*%T?~xMKiTQS_!-C$5;QYigkey04kA`)Reyo+PXCJo z^We`GAlY%;WRecc8dXs^%2D)h1LHNQ3-p|?@Z65Q*wzI3W&cn@x(ZZ%*t}o$q7bL# zvy+Triw(MHNh79&CtjeFXW0A1(vp2gLAH2!&Y{qwmnNQ5_~YL+Mu7TxaRU!=v$5>_ zPDL$0POYuM8wH;mM4d-+ue!+L4xnUOzuc7JYTM@ny_~-pAWLNW@V)W;x9tX&pfGrzjVkE4z)$zy_Z)<*IWC$?cGSMquxqYyPUnofo$3o+BUFxumBjantYWe; z$)Og|T#tTZkA`sqi-U-V$eh33j~gw-i+%@)RQu>Ei;unuX4*Z|%)Q?<=W?V6qvw!$ zYs<`~gQGFsbk^+ZFiz2OM0#@?05d7EiB$3zZJ4%GT{n!>C zo4(@RDQ%O1qu{Nmi!_$)4iU=axi^xEF~cuGqaiCoTI6m;!KTg~jCBXc$F$tbOK@ zgTA>6hZsF;J9d_ce+}C+&bGK9ty7`sqjIY%|9Iqy9Ml4#{Tluxdvpn_wDF_RLWeyzib5sc3eitO7Vm!ujIABBJG+L%KA@4M|LI{3zL1T$;tzP>!KwuNv{%p4A z>%dA${E|N&3E%kEjdV}g@Mt=yF}d4lHO&d6G*DZsy5$mWIkP&NM|okej$fTv!fM)k znQzlq*MEf zJS=RRnP{(=bQR!*vUu9}~Bb6R5P+X)yS98_( zE1y*A?|Wb6fO<5{oq6}t)~3SBBDx_tn^H%Q(+S)%zT+R4>dp{-OQDTf5v&JCD@+#S zYSyUjlzSbarD?aB2H0ISTkX|q zocA^q>-qP^Uj&-9Iuv5h5Panqh;5^ASkvWXP1xt+G(Tx zxzZ_1CFc0#2`+1c`up);H}o=k0*wo?iNliS-YB@5O$L#UojS{Z#9!sn=5ATw9J|;% zKEIZ}HJGXyt22`v-YcWeZqw;~nD48#(q4#;elS^Fv%17PAXmw|yb3O zU^*dN9kV0;RPtN`D}*XXTcv1#wFb7nSrKkAG=A?gXTeyt;QFH1G&1}4>U5oPCdl|g zX=f+Ed?%SJRYsGuPD#45c^i8T|5d_R8*BFy{%3vaXBZ}6}yOmlHD(6Yx|-FVQiwG^N1 zpk2oPogOy@7heR~VS#z;8u!V?Hajt)^PBWUB>jdI3F+Z!NY9WF$2n1i+e~S`Edu^= zTxRN}Za8si^kF}BP;IJD)OgJd3%0E}Q^_ZqXDaV^{W>eX?Ha8Hbz+@1Yn=})xg#z% zY|ePSLNlWS)oTRQeAy1}$ZYTRJkV&f^-bhEj^oaZrLoikXAG+KX_|Y4sc*1B`Z~3A zq829_a01CbE{WARRc*Gj#ck89BC1QVSZ9Pqv40kiMdnhsm}cE zNH+%@9=@&$(?e+zNWrPfH8`&G_-5XtD@L%N$JRyS+|{oBDK+I*#S>u<0`AyxCDP|l z#s*r}WK%e@tHJLacxh5N-@ICnC3rTN3{(DAcqTv^Q}NNrCLg-|GQ<@ODJ*$PX183Q z7`=GhW4CnS3*yN&7UAZy(9oo;NJ-Y%#L4npzUg`~bVA|2zB^ZH8nYKEa&Qu_&g6bM zXw@KsPIBPF5_x{o$U?HD%e|fq+kPy8$k3imo9QUH9vxquC0!oy>ZtAkR_Dpf?^qOt z9~z&R?=Oz*R|ZD6edHkj!Y79#T$2amFpVN%MU1{5#G(ab_|X%y##iQ*m)v5?;RnMt zp67U;zw?w zULNbmrj%O#xRh)$1*`S;PMsS!e%a-CdwMWt{hVB6V;173Pi zZi){A(h`cG0ak3GE)l{FS2;o)B)yt_T4DK}TPZ5KwS}!P;j);18Ozxxy1Wn0J*6wZ zie*~%dZ~!@Q00ghythmI0%p!P<2bz^J$VDgpGr%pjWvuhV|HN=(e58*zjrNS$~HDN{Kj1uj4{AhY64ll@b zjnxhEo@a(I$`o&?KH^?T6)+^*(D%FZq)M!{I^QZv%_O18-J0$d7x6d$WB#E89!2K3E*51(r`1QD&#D)Pl=xfzh>EjXx85;|2?l z%5;3hcD@g|2)V$B`c^pp0dJ(1VuUpK9~wB_J@3gn<#yQsjjM*1UnnF+MQV!*kkT+t zlhg0;P@%E_glKJQ4Cksz@K2;6b<0l(PT@5GbQ0F%SN%IcqmBm9pKsdKSzcFn|3BaV1_U&lYNN|`RW$sGn1tR3 zV3I@&B;M;+yzsw>fc<~pFsT_@Hk9=cN84~V)k+&WdWQfY7DMi6Yf@X!jq4w-`r*5h zk{OTNUwsV@4t`>s*&ZCJlDi(mZ%7*S2`**PIk9lQIRchmxd@)@^uFD$QH*i8peUq$ z?~x*8T|v&;%}kRy&E$-;87SqRHMqSgP++I7^j3B3%RlH#h~Jj+PSfMP6$%A`+USU_ zVIG+@nZo_a$BSr{=eJwR4%55iutO_9HCL^VN`~ZGBc=2pJ9}Huj z45b^H@CEpKiT3@6sBlEK^j1;RKEn@ksjty>%Jw~4;{}|9yh}|@80{Ic%NSK1^`B_2 z@=wAo?8W8nkle{ay`5>-p7D|FXjX<(F;M3E%^Y|f?FPaNXq$+(uT0iw@HAUTDj;X5 z4BKT~MT?AU%eIHP+Sz9)D~e_~tG|M&Kw8G)Duew9(z4&|3hK%c?o0lF`u+{2*sMjY z&Vg*(@2<=qQ?;qUCU=AqBL-3|oDVC*ThDX+kGudiI7^c$EMXK1skRoq(i)@ZCs(Ki zlJLm0g06COj0Dn1sqABV*Dqu&syZM+`S~obK^UP*=Jn;+ad5KRHapevL|D`YwJK8l z3|;J>ORajb`ld*4zn$%}8^8oTX3S%`Lzyvc$k^8s3zbltQK zIEmjw^fgw^(dupHaQS@$%N&h}_`(-gRzB3|R!Rznfp;mW3`yCagV&m;0?oy!W+aXr zxcfx(y?W>jn4RLdV~WCvAR>lvaF}^_Mdl`6Fp2Th{{RD>1O@o_|<8RKAFpeagViJ?qTevV73;*-Z zV{0}S5fM&@psVgU0CYZuQ8xO7yCagV%^;HY;ioz%iT4qhjTKEB>!RS})w%w~pHRo2 zqG?At@bq^Q_%ji3G2AokcQSU56@a2o!>AFz!;NAy0T-EZn0|*0o!11!XqEPh^?-jY z^Z!U-d?nzb3bE2vDf8>Lrb+-a?pW(S^gE0zbq;Xx2XoF-G$`| z{T~c^b#+Xzz-rmm!r8Rfh7P(oQkDWC7=%MPoez=Y^I)A*v%XDOBr_jAaP^Z#0cD1EF^gQ)I z+T*VykZM<>OXE1Wqm$|4ntrq(FVuUzq7EKKRr_gLJyYdLWt{8Wje`hd_B?`Y@2MQ8 zMbgd0_Y;xn7X;XQUOtnT59b5>vH+cMT{g)WU90)%-RL z|Jt3(8U(wm<7_DtvA6Cm=91q01n+7($?w*2>rGOGtoen6ebmmEL2Z(V%}u+pw!GRA z^WO#McCjvZT`)FpZOqvII9;RbHQKDC$Eqh}?%8E)6iDif>ri?d=fbUrV&1rxN-?Q) zWv1%T6yX!qNiR>gU6{hc%G$=xmbgT}k7e}TjeJN|yFVjuKhXU-pvLrHLXo+GHR77+}hG}dud1JlaRW1oZ7~$ zJg?^+v7kyyE2s_fItF*<88`cKe+{J9O8TUcGuV=eHfxVOrn;sBR6%KhgCO$sA? zcjB5Sl#?7k;#X^HDjl`ZTd88JQ)3!cy*tmPQ*9F9gqmgJiUeU|S}tR@ysVG!u-$oG zi-asw2g<)M4NiNa8|LaW#$^94nmtxP*ld|TzM;TAu%7%;3Xj;63ky^p^T_DCrcFfV zSR;d&)_G=9r^r4v!U$pVh&inY`|lbt=%G>t>*emx@kKyu35AVHi9;lfkX`lOc53)5 zFC^EWO7JgPR~{fh*|mOrU&0az7jsCGA*g^Pys7u^r`iE^7HC zD_?D@_vw?6S1LD|RC)m2WK9Hr_wdb}j9aat^kiYQU2DvOMJ9o+!SneKX^;suFrC}B z?YzjS$hgTuP63u49dk6HceKU4EPkz=2e*>sGQNG+dJ}GmyNWmw%7-%e2s?N9-y`&m zE9zy=pXw(N4bifYdzQ&cMY=o#Pe>WRcCTp9z3Z5`k1{vHhJJ1$aYUfwLzGO&2QQvg z-YB|Bfz|6qCr4abil@GMbzZ;)l8xP?O(f0(S(@+Os;X1m@~W3Y$X4lU9w|Ss92pI}&DHUAh}1ZW@z2nXf`4aH)E^_JFl6cJ@yEPUznP zs2?LZz>6)N@5(NR%u{;om-tI$Z8|OT>P+S69LIFNaXRn(fv`{O=|Y~g1DA|mKrnS( z{XxMcxZXh2$+&LIhXlBPrNBZ|ld1-JQll1i-Xz$uem^B!%eTK^}+Hdotqj*Md+53npJgL2-h_dY< zD}LG0-X_N>{T6Xof5caY;l}dv01Ke@YrFYsIz_O~(NfUQL+?T&6{RpHvd?anYM+1Z z`uI0D4X}KfFYu^KdQFZiF`dya{n0dCG~Kn2%!dm+YLdKnoRbbi&37Z?Mem$AV`hY* z^JdXiHYr@-+F_)5a|IgLgd`Taw1p~Ab~%mVrgbyY(_4UVZLOQcJzZ#z=jGJQ4a4~y z!?k9*Z~T{%kY-})W@0v=oHM$`#`08a(%m49FIz`K=g`e=mF~V9WPCP;IpX^a39W0K))y@tEnA}Z;mfURrwmenDJ*YKOZ>07 zB`04R$P1D&YRH0?`Qj3kQ=&pt7u0~GD9S!e!fQ_~wrsf^G})oco>CXjrgUUp zxH=M#o>YiIA#OnFJT1B_O=WROZBN!%As zkR>o0|FU+xzuPll%g+(hP2?4%qOTjQ)o;JAlahUs;PXPrp=N$8A(}bm3GNn37pW>| z6;|zyCJg)d>Gr7eXwkYN1SK{aMlnx=SzA)-D@LbCx!6Wa5vRft8AY;{z!?+4g&F~t zf_B%lAcR06co2?g5SNnK50?>cmMo{%LDV1jya!w;`v<#vWJ`u z>S!uA+Yzkz-dfOGW9?+A%4-mrH_o#~nM*Pw>f>N8J8;ZZVxujuTbjEVJTKt;<#}|O zNs6;rYQa#dQ{Mt2q9fMT6RS6ISkhO$Sn8l1dwz4dyq<%&-hglAo@IY|>2^0?^U0l5 zYh_TvH^dnOdgSy7J@hCkN%cIL=#!QH&yx=1L`whkEj9>?ZB=FLs`BgXgIoA9wZdjc z0K*S)QZw4pDnm#2aj)s#p)kB@EJE=0@t_pXM|+@`?bYoPcU`iz z8&ir2IVy*e!lSt zl}C2TZZK7)fyyFRW6!M)m%^o6tlis}gTyuYa>>dPc!lmRd*!NOWGOaBRGUU*l^K8Y z5wnw|#mC*$8<1*t8REvKzGY<)&X4|W8is)(qa{XOk@u8+bmhYMv(*e%K035>6RX?y z`v@&7J%26`UsDj0ivDECqJZV+FLaepA2L2ik408K4{KAJ>Ru2)BJwZFa~$L_eeusx z-6f=ey7+@!J2Mg>szXUy8kY;4S@!1Fkh>UTZGceO_t2sIC;UJUaQ@s>=+2 zMvIBN4b1gzTTDtoV58Z*ivfs&u~#w{ep}dYd8hYBx~*TX5=b*My_OkG1Hcq0$Pf@? z-=xTlpd8-=0pLp5Reu*IL0_+oPrV7trW>_N>&Zqy z^kqA5PT*U~9Mn+t!Fa1a3G}MPTUVOunN=I;t-Srq;RM8PhSi6} zi_Ew>0YQsHmx}**%aujEj=QQXm0xX|MVJO;nDEYeAp{2Z^o!Nn%l7Xkz1p!BRjj(} zWP%N`8LmsTd*96Jv|)sRPk{XB|2kzYJ$kvMBdhbJXS(cwA_Q|`rj}&eI4`9{l0P^>*`N@ zuqO7M4BWySj}Hl$M_LrVT1DVsxB1cuR?^vIcN4Da)Jd?Vqh;sGUz@{NT~u#9oU|_Z zki9I#;MnJF)t;*W8AJo{?C6QMWv{E#z?CaIZ1nHrOCpB0L4>Jj1bJ zdJSA7ptt624|~?bbp7w>>FY;H=7Wcj$ze>I<)wFG6tXp~dVF{lqD6~xHO*t85rgH) z&q*I56`+30}U2X;^(&Fw3(spHTl|vb0s{ZE4#!%cyBb(=z}@m4P^I>(Tuk z2Ta0iWe*Aq4ZW#An|fM)D35cc?%kUb9b!_JHq`2r9L|z>>uS0l9#?~n3nH^1)btCw zcF`RVRmjQCeqYu3V)ph&eB0@WTLRmBlxkz=YoA{Z7yU0B{#%H&&8ykx&4Drh42n*n z`fpjM%^qqT)c-cEf9lU93qbWf#^(QB9{-32nG{bm70WMEMt<9Nf4$-HX|sosK=^-M zbL0RpAw^!gf&VH8zpV>s{2oc0-!;k41%Isu49C%PI#cj>f&25F(mkim9(Ij5|La=Z zX|snuVwsYE+R)Q)c>~mYYZQ~|{nxb_bznGgAmQxb-*@((U!`0O%w4k2(0>-LXPox! z*#4qo`(Fkt8JN3kn6H}ubxrsboXnsKvHCCM@+vpbx5I+5^gnUL|5?ia=a%y6n?6ru ztq<1pR2A1cm92KRhDvztB2SL7vpde2LZ`wieeH545KG)*W0SLjg!Sh|u$EY-%RP8d zk{MTXJl;N1BjdW>84)MpwPT;t;*0$~C6ROyW<^LC_9+uL$jTz8YZa!hB!2}fB}Azv zaGW8Om;BgkcP*SG)~#)UJGx*=HvW%&{aQN-H12GvjYz-6!tm+U(C&OsNc?c2u~zA~ zg*vv&6otPOy7W~7u6*^JC)xdPW}+%z#{23M)Vi*>UH3B!%kpI3?GnkH?(lSn zzxE6aaxEWcy|+5dR&iKoqnLN16dR6{vK<#4h^I81_pHBrl~M?XaG`Z;Y3itoQ&Z@L zcHS%Yx`>O5nrKh#pw@c!Se*|C%LiwOlDas$1%OiP>!3! zoQSLiB+21&Q?ZfwkdHM|VFNRZ3&iQ;G^a{gV3 zX_b(5+zo1%wz7FGGtE4ko=8w+ww6*4^pWawSGw~VVMo6bQm;ABV(VO+JLy{6+8kF5 zgqRI_bJ%2#=}6lRZ<-2saM3%s4UE_-5|(f;MPysx*R(OU! zz@m<{$I!ko5=4Ps;O=YJH$cMcl>(~9u}*uhiJ#ar^bc7BppRXlJQyj&-hdo~jqlJq z2g}|R9x$MM)Mr1HYlAg^@f$8A{g_a@3vUssDY^GGKC2U)pwr~Ds1(id8B-=ojnor0|a55>; zK!tmWj|{-XH;}sCA{oO=5%NQ`J22s|CcdCKB#G|GvuHgQ(~dN*x{ol4ujw(fyY>6? zdhi6D+*Kh{P~AOT&l^cIqxoj%DQk3yv=G@jATxDpC6uq>hh2eN*D1~<|9H|?XFIJv zzerGBnyI4+_}479;H!q6!}V3--ajdIJD3kyuGc~VN2yw{9~FUowc^5lOyG0fdR?Ci zHK{7#6IG|_;&n$i9AmIEZnKSX6o%Lom2dka9%Bl@ z&#f}{V2NkymGur%2PJD2awEp!D0G`Wl}zW7K3^EOICq6 z6G9G{S<=^6|CxX|eHytJU@f1o$0#%ZH9bF#{{P(hL#Y1m&CpYhz`M8cfV^*#wAlg= zwbaRlXknw|T-_2~LEDKrx|;jPhrPv?Y9fV`p^$AA8QGofl?(ZrbVR@cWopBYv3tm| zimfzIO=WMY?Ccwf>uJCI7IhlHVFS_(ked+(hR8_x=K9_Vw{g?E(ACvAyo@TyJV!yI zY`AaeVxrgmSMRU$n8&-Mo0#;oe<&-={Um7n>S$3av2CKpD-A$$E50{bb0V`G831Xy zdB>{Xc(XMFENso_Gv~WJh_HW3n(&)XqplgFiQEZ~qS$3H<-Itoj*gBhTvtFO z*`CpIFfX3Yd!wOu?Zd;kLmvI&r+^Y zfyDnDgWz_#YNV=T6}Lg9zLYh>jno0zLOa*5#@TFc*BGSoSC=$^t^jzTGu1}N=MVO0 zacwUMrCK*lIZc{7<9lncMv$e?fpX1?@$%JY^@fV)ce6+ z(%m@BId-9UoC&%M40TEC4Glh_wmh%Ul7RQV^1*Ccl-$mWC5Qn%Q)abe)!NY-vCt7K zZ)NqS2=27lxjE+4)$*3N`nGZ1VPfA-kH49nVLK;NCuM@~p@_%g_+E|f-fEA*LPsKR zbhOTdD3JL|f)MF@e?Nd6F0+XxI%Q>LMQq&?;rvToz#vq&uUD||J^TiS+1E+vGSCQQ zOE*G+|1MyZ_rWr-r!df@&9V)Uv?Z-;uO+xw{1929I{yX=|qxN+UrEr_d2e;IBLNCgK%8i2q! z)>|FTe(G^QB}0EwaClhQO$p#cX*f4_hPUpBgOD%?vOg`+2GWlE2!u^@4| zH=r@ayLVo@tD!nY3rpsG&!Q!L()%+*^JXbaBh(Z{8lo%su39y{)7iw7O^W64mQRVB zKOh!TS}G8-8Cv=%V!Gm-G2ohm;!bnQ2stF^ay)F5POggG(^ek_u^PB9#_yTdlwK_3 ztL_v<^PBha);)_%geIgKAcZ`2q_E!axCVK7xk_wDpD2nK7`|Pp@!DJVrEq=_)++kY zJ_+qQS_)5l7`O{rEszO>XSkjDnN>|vCHCH*Zr)rnRuWlsZ=%j<2^P{vk8MTFr`o<+ zZBE<|1T+RT{`raLqU~Mp{qLsickOmrHs-6;XKPQ=rMuB_PMx95*xhi}L#4Gg4bFME z(B#Lgk!)mdqfEHj4V5nJB^W^!uo*`ya4NDfNK^BqPbz z-Bb4!YZ;+OvJB<8`|&%g_e2)QoKo#-Dmy)CFX70bM*;e7m>o}M;fJnPW#cgd){!+~ zDz~S3%O<&+>F(7N?mwR1IAvSBA>TjR;x*m;m_<`=nQ`_Y1k#accc~3Ss)$>l<(1FT zuPCWb?ufDW`@-g1m4uE)EbaH9q4U51RttLq2fQ5V&CR(Jph z(R1z^TXq+mBgU^?y?3&bgX5BqgLaa1mp znIrkWDPwMv21bE;9E9(Drmr8?Md7ZqurnuYk?*%vdG0dcV&ztYV?aL@@HHO5C zwoWxPxBBc&q^R`P+d#ru6?*MmqGSQfUg`Zu<=U)OI2s< zh)G|pLLFgZ7VpN+NMTx*GX@23F)nW&t@bGL^%9pw<>@eIS!VSXJe08Qib1@<%CfMg5Wyp@ECWLe!+wr#r?OnvXks(^?^H52*NgrJ) zmk&5kPvPd0{^g>&0PJ9ks;xzD%|Wp$wA01{o}8hOxE9>#)~8S=3s67elA=h|C*jsw zi}$P#-bhE8(iW|s07nBskI@?TV_(8(MhfivH@OA+G(X$!6rCEmyKZaV=JB@B?VgPJ zf@TJXgs5gjBzG*bM`mxZYXYOld5^Ow*!PK$ea{$sZD7dbB_))^ap~wf5s@c7r+PO77CmJ{G`#kB2 z$R+xC}~*r6V6!(Q_zC9v3??lscXqd8|0(M6Hw9D!2hA*#pS1 z`766kxHf9f}@W`|2LA8oG>z?oCo}g+aTZ$dKOHtZtLDa1}C%>y^4T&+ZU= z>R9!sI>InAj6NSJ_G8ky8D|7GG^SFu0r#Dc-LN4i@X2`2+0VX;$F8eG>~7;OiO=G; z-)_wZl<Ns_jj>l?}qbmrX`mBzVR9edrjmGPs4^le#;zHCkwzGxj}7&*aiIvhKf3EMG@?#-r9W%7-O5&`xPBlpuSPb( zd*%lH&EzLR90k^cJO^7OWuDwUwi#$dm2RiV;=3ytu+Ci;^+*bKw1>Bpq;R(hHcXA4hzn-t*+R9WRYr9HPdZ1=!XZQI&FVY-Vf#j{zuc{~T zzj@*V_L+}(091#si_pJa-PNSs4)zuBYa+Nq^!4(n(fCL%Arytf*L<+b?sR%kgOi zKbM<$a3REw{O8LGvvs?Xow4;%>=wE;o*IVtp>DlA!IV1O93-dqef&{vZf_D9xL*tWPdy}0*Ak+(&{f*C%ufec0+Djq9 zTe>jY%@UhS9bcUQzc@)8|8NJbm-K0&W2-8wtl!RWnl&@uZp==%$~C^~de5vZTkyqL z+-qh}FSJ%(8!mPoE`El1(wEl4$R@CIrA?mLdEG6?aGY$c*m)W9i;|n^kC&0?TX;0T z8#)oq3#86@>SmPgQ=r{xw5;}B<0husIvRW|8jazkZk;IIRt0Wny1Y#}`y(_M z)V*@omc->SE5nmJdBGBSLkNQ!#*R6DwHbekS#v)*IKTlXWgj_+pxf_N^SD#q65Xrb ziW`?Y+VCm6PG6Q?Q+)_U$O3Gmco()f_hcYb-A=seeY}Gs!f;rgO&BfK_!L{|y3CQn z;9LG^*0ZC>QI;!no|PEovNAU8VP%}+vf8iHDSXVoSV4fl*p-Ml$K>|GNViN$>iCyoia+lYZ0#2cXBF2U~1aBRd(^!Cx^713u5a>Z|Q~+8+P{rpV(z{_`1z$ zomEmYzRg%(04dR=?m(%IP|JA|nimq)wr+hBs=k^67Ubdysd}K4CooOKu6Ofp{MyR7 zLt!VB&APi_NQ%q)rKqDspt9h@^kxC+pGZmeE58nM+gV%Oal(Z-3Q)oGi1Vd+5#}cc z?l4{oE%b-Wg00<^)z<@etSa{s#9WS+jN^QAW_aX57v85!hna|n!@~QM?oZgJkXY5qn@?_Ke0~_%%8966&5ec)3Jp&p=RULm+Q{2G=8#j4p-7`EII56|= zh#8phM2gw$Uv!It__Sxn-ax&v3p$U7MqyMehG{n?+ilJqCHsRk{S*WZvwV-G(PE2) zbpQkhgHXP9t{?b93J%LV_>4sjbaj4FRaJd%6iYe;>@Okr=E{v^Wc7S9W%2-bQKo3Y zPsRjkbv?k^6xffr?S{knn%@;9wlQ!$NOaC!&q#edkt!-5$>q{?wun!O8{m8j-_so> zC)NH$)OB?Vy23T;_x*D~Q=r-Ag)W=XmRQ@d(69?n&7LJrU$YDm>{M52 zH5)E8Q9-LIi9CK%Co$r2(lH1|5>SEW+Aa16a3_noMPRnWrg08QoyNVqI z&Cv+wZe8Pi6qpmY-<;AJ1oG)1DRf%oF&E+EQ(^F30*L?$Ic`zofC`4F&Y5fxitIfi zAdat>S5=)BN6|Fv>RIO84^Cz!C@F-1m4gUbaIPL2FJ^;nMrmG8<>?ePNyKew7n#A6 z7Snv58^79F9u->kc**H>u-VeYG+baDsMVf-`D;!6V83ObNnevfq4TmT;@T%uI{qbX zUZ^F!8gG;GN;b=jyv+nXrsBkq@!dWpDG{{Iln=4@Fju&ps74ihBvAHo>eos}RwprR$}i+MX0*n%_V2sU^eSg=NQ^1T$m+@>$Fu6I!^Z{EO= zes{GT%PbgkT9=sEav;v~n3Wr@Up+D5&bn!tH9t96DU_#Q(W_%R>mqSH7Y;#V%CJRO zD{YLQS8y-nqNha`@z1~EwF!I@9OE8axmJ{)N! zn?$##zYvP*rO@sd@(ln-q9rHkFmYzbrf76kzSKQDSPbeED6ppBfrCnPn}TWC3@V*N z*Cg<;FOX&4jYQG>%T(rSd)0+s=Z)!qpzQ=l<4k3?qv_)WG1m;WOLJG@4YuhZU>%fz zc!*;a8IBiWPVlE^sLlZc@Wj?WC5dE;*l~{GVG$4~ub3?DT1%-5hGuLuSM}^8_q+JD zoAu`}T_=CE_dX*rQqm`c-1>kbMiRHG#w%?ynwtaCQCsXZsJ_W{#-t%>n?|78rPQ#9 zj@5#g8E3fCp;vs7hS~aPe?8R)yL%s7^Zn5JuF)5KbA$H`5+#fq^Y=$FK`uz|xzOgh zO?li>@YkJPF?-}?83tX_1N#tc|YEi?k zqSWjqkF^T(_+wieM1`nAPV5tlGi`}-*7Lk6w~*(~Gt8P6!S*IK$b@w2`T7rVqg7i> z?Ac;|vvoDRkp^?xd5^enTBv_W`Iv;yoA;VXvz|GvDS-1lS@LB{of7A>Fa!NNZ4knA zf6aNa_v##PUR!+DOuOX&BuL$(={DEm+80of%ml*R%hjyZl+tV{I^n?eTIV);uo=7E z!e8gHzPD-{>W;B*vas&T1>#?%ntY_(@|!EovghAWaSKx5d2`eBOm2U9vOH2^UDf9$ z&Y+`O{b<|e+w1Ax=52n%A6W+riVdZvI`gi@5VALnzU4)>f#mB}R#tS@?`T|tR*x*A zN^Fslwq~po1tB}mtm$Nu`x%$$MZfV~Tr8(}`6rgL?# zch+3K=gR@4Q{>#0wwvn4Q14u=Oo>?=FU2UHyI#9C;2i#aevi$aWRZ;QXM^_*LKq}& zqEUIF05rD0VyTpNIlWA~{4Fcn9+-b4-Pjc~Z!xbDEohtlu6ph{wW+^KR|#v&U{X(` zuUfIac3ndZU(dukEk!;%H+P9AkL7wAPlds%!db+*P6k5)%>;hfRRuwN-k0gkQMh+e zt{xlcEpMs#p5~Z%n&yJFg7#_4HShqoIP$PO;HrA>@$btT%dea$*gAoc1yb zsw>Fb+E&MV1V{WLzzNzhjJWS{O4XAgnvu9Q2 zjhnHms+D(>xZE3Yfq{TeptNCTef);AX}?Wg4%wc6V()|AjrF`2yIy_p*zCGj(3pFl z?dB$OBKkcBQ~eh*Us_)elL`-}MJkvFv89?E-1IQ&)Aa`lhD>rw)E?bgt47=P=qus` zgW6qsZoGF|^lWZB>=Iupd&}ireEa6ZYsFOkU$+*b$yTqRH%3NRC@8pSY(0}LB@t!M z&U7*mWe%nl-55dUMi)H}cwD(uN~_>!Q%!hl0EKF;GK+kKo8Lt8?tyLhYQc5Bs_N>u zwiYC8Z2M*w|THKrQf5l8$}5|G%i`b zENC;d;pb!xai!;}Fjvx3;)UNaZTslz(e$N&NZ9g%ChI4oT%7yyj;la(Fymw%S1&;g zb~_Zi7R$`S!Z{bA1u9v?Y*Y80GaIQs4jJuu_DKu?Oz|kl#W|<0Y)`-qiWEz2zAIjF zz@6KB3+1WuYs)<)xDayPvFH|o?CO}eX@Q}}CwcG)OQ8u4p>ckVyS;rKx9)=5LiQvt zX7$xU!JKYUq{d^ryJDODYH7W7P^qnP!EPbhpk3fp-CS)3Zwr`9f}XEa;F!xICf$7S zCF;FmV$4LR-lXTKHRWMsuCJ&i4!?yXS?J`H0|>nj zU`v+*y7EBW@2^sp5?{9@Z-DlQ>*i=mFfW1~U{FWxb-*#E%*xK)Bsu6i6G#7IOKQV_ zRa(f#U2kehpW=>gk!yVWC*Lx}2rVm{#)hiCK9|qoHVKbinWWLoBM)57-dsstjhSs9 zgFe*PDEw$QL~L}RgqZ*O&ZF-)v$Ejd=Nm>2QJPz4)=4UdcD7x)p#_%~+zN4lPjuUccL;PDZ3z(+Eai!!JLf$rSCvq{h}vB@v`atllS4eP zE|9!>%FgBe6gUge%obeNP$7e`(=RO^@&=3U^_RLXNd3$RoWZ+65cUxDG@nRR-RQAS zG=L9{5jo%sfxUbc;zc!13O%;i32J)%&E7mCiqUac$kn+E_5FMhQSNis33IumI`z@Q zn3w&AX|%TqmAuCR4OrC~M^tGWPmSl%acj}!_{0UX2j!P2Jwi-dhA*7}`Iy;P7|{lV z=vIH>JPQO3FC(}VRgAU_JM4%B^Pl~M#3cI-0;N)-h6Vv|zskJ3czX*hqQQwXe&LC* zjp1j%(>zo%2%@qodu^8}Zdxe5yPpyk9l?Hqj6Uqk0kO%#JDoxx<#+F@TdV-_!pZS9 z6t=K1g7u6j@=~*b1wl<=QDMg;!uQCXLANylnX63S8hSe5?(E{@43lwMsLnuJJL*7sJhR<&2e?S8RUwYu&vvvQLHrc z7eDy+peMmh5h+kd!^>+oVKKiC8BwJE(qobn((tWE8E~&}g(V?KK+&#+YSkYG@Ks5|R zL@X>KI{XJd4tG5=d~O<~2laDuaLkpU`r7 zE4yx!5l7YDlb+@^A`Vl2(ecI+t78W$fr=tFL&_C{mm+{db@RZLK(#DAkgIT25ij0F z;fvlUk6&tdp468fsgVvmpgdj=l*>`PkJy#4eYh5vbdA3YMJBZ>G+9;A3)ib%BKHqB z*!I%SxVk=EfVug3ori-iubGK|&Ivgg9+op3ZnljMrz%+h z$a6FjY$2v+N!+dKH}t}wl*iWtc^1w0sqQ$vi2CV zlrPla^L}ky!LPV}o9X)PfD2@i#23IrmQhcpa{4kwRPca03b~|M1+||%1eox(*Dvd- zL;&90*T+R)C~oT&JaG0ui+dbcA?X0sJE6ka#vg#r>Bdv{ue6ss^8A(j`!j^N$u1p& zcxna7Ehq2psk687NHNf(7o!4wyM56Ynil~CZRSuMN`x|Z=Tsv;~Lpy|03KJS0;byk6=y0Th)ZB+K$&oB9N zK;%G$?kmwIEPcAquRZlqlCtt5o#Qy8zwtNBOebr`_MUCqH-qr?%jfKY8C? zyyIVk0uHD8QG`5cH1Z$49ByR-bVdA{@BIHk_58DxIM9;urv&_b+eM}iNkHoQ%63zq z%|9m`pq5OWvOxd0Kd@XeDXq~Zs-55$cYujJ{FD>2S?ZwjW5$BGx_f)$S~@Z?KPM1y zR31G8^8DX!i;({@>d?y;R#qh5EG#d5q^q{Hor;cmRv$vn{5I?dMEeO+%p1>WH%-_w z$@^f4kNsECy|L6=j{bTrtf&77bEn9J?`bQ@$&*NJM1;*8XWQG@yb)p@`9*sn|BAP} zWiBVT-&H)#e=q$Uc$nH!b2NsAdh>Fpg4rJoA2-7Oe)n_W=;dW2u=|skJ(*kY1J`@= z`9x0XE`Q9ebRfa%V%IiGQGtGf@5?n~(T#|3jL%eA%8Sc(Vu643QDR!a7Y`0r)ta<0 z(29!NI0s%{&O#N4Z|}dU@aiD=OE|uB8A?ITOxzP?QU*`}E8oSE#9M>W{sZMJu_x$z zUp#(Dn5e9D9XdM)omSgGyWiH)dJ)?m4JV_ad1h^OvrB6ziSE}9VM_IrbQ_Uc80kv6 z;U=nZ4M>xxsWimdtUrXBx{O=ioXY>>Bfh(vcpf92$omDQR& znGvHi{nS)JIPm!|%F|hV(OXGf@rOsz5z(x!ox`LUcN&gH=PCKo(G0D;fz?;9ShK8; z36A)M9iINNXvrMPT8p6%Wvm-Jt%u8!tiR>4)mcYcfO#cccM1+)(?^~AYYqTj2$!-? zZiDAq|6?EYg^G3(bwVR?wCN#`ZHAY&c2Ha7gN4qZC#%;f<^^713d%EJxlY7)56yf;>WR%hBxiuZM2f>~<;A zYPG_Q=s*{Ruyi$JD{?M&_QTL`YULMJ{iWN9ia#}|$ZB5TJ;up1?{`MYO(BP0s^T9J zY`dkKQH)s$P8#j(C_t(wj6FdEa!%G?4qCpxgr7Nse^vI;ef88ilLN~{@1SqbLQWP* ze8GByLx;*U$2R|MA z+|PY`25*hLyO>&3l}UhQSbQ*Dx9GL`Hwz+?!Sl6a-&LkK{;kf>;wQ6^SFoof58!qF ztwiMKBmDx-z<4iQee>Hl{lT&aw&WOaR3q|u?*rvu6#;mStD8Q@3uvErk=2ZsLt3jz6f ABme*a literal 250047 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�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 DBJIeb?@5?gR=tlAN z-4Y-2?3!X*%$AjptD*)f(Bax#@Jc!m7Y%)gDYw|6z$jv*oD6;-YPP^;H30Pj$eGBA z)RZ2}l@{VGhZMVqYrvRDFq^5BX3_y%bZPrg(&7IeDi* z!m5S6tGqC5jQfa?Av08kk-`LlaF|-73KZGw-kfHRw(HoGKGV%@)rnDr54ldg^s=#2 zgd879y8wwq%I~u?LP3QFb+E7iFjt9;gkSn(_LT@9~Gq_Kv2sWS{Z@kipbiL38mUkH7F%E&mmcffz<$jR?~^_L>7&uk|? z-n_SZYoR@TytO6`>l~RfZ+C9+Pms_xg|>6N_v?d%WqzsK00Dh)3PD4f z#ZQ+R)T=ty>eg6=gs?4Caa5Oem{g5hadLx57y=y0mt{vvnDK4TrKh97_ABObRb zwjPzKNlXd(m{We5Ycd)V!zabVsQda(2fKxL_{Mt&kSo^OG?;kcI9tTVSDGo7?JocN ziDR-Ljmp0dlEZ6RA4X0;H5G3)H3!#@&fn!Yl^yM3#ot=39p#L=7<`9@_JdwUMI?WB znGGc_yFF;MA=XE$?h0I*+o}&cqDPzA9)jn!$O9f%C$_1!i>P_{-|#M+Gc=uSJ@p+mlS!U7+scxZ zj=lROiae_F}onr7E^^eu@9kk;|C>#6>wOE=D;{Bm>6VA}@0$a$7=&ok5 zSqYnLk9L%YI11LyTu3STAFtC!a%8d#I*(Jt(@@lXGg%<0_4S!|6bq#X z!~h{Gv32}j_j$%8t#9Q!6te@P>diAydj9-4D*Cs`hC%*z0je}xsXOv_W$e}m5!8gb zjx)4+La=60Y=X1;5{ykztkn8TSgDv+FiUsXG@DH$pJnC>k*LIXVm4S~hO0Pxym2@{ zkb;6L$b3rHdC@a}5<}b74z_7$wYt$e-X%T7kqbUNsOuMFYobi}HWPf;O#~dF^L_92+ll?L+Pytak%XTP zA0gfa6lIuj3;>!S1h9jh=yR?aTVZ4ZooWvI^=7?S@Py?MbdC*)mdiYL<~hr+EdmZt zAs8+E@grxMFAvO{M7K!$YT67#{iAxePkoPnhF!!s)ECD}GC7jZ{0vz1Sy9UNlKiRu zeJih_RDIX9JON!`I9E=F{qBGLW`chCjhaBo{^>tmSz_<}LIH9~5ommga~6RD_Y;-nB0`jHc`O+CIdN zxYOmbvX^VAf_wB{eV}}Sk8qFka^aOmag>QE%*%q)LZ@=xbQJdV|YTSOdvkYO?5=X8|AUB9dXW zqX!&(!8ap0bDHmPqlaj%N_BKa2^nH#*T95W-10^2Q-lK}?uQ96>b%^eFy(B)nf6o7)Ks%bIp)3Xj|0yn?!j9Uvp@QayQr9JcNeoxvLe{ zEXM9_Q-bcN!z1`Kt_BIhtBxLryHNC~@ji&5NW$V%h*7lBQ0zRjM}OU0jlIc|y?^UQ z;8_E%FoHego!a-Oqs2YMjwRo|n+8~SGgoeout03*(ypRXD3nI?Z-Bz#SROxlkI^$W`Fw!6JLAY5{^)C(2oxbr1RTZ2zW;|0%P(%6V~#n6=APFC*Xt9_o?Y4zCc zzo`M_|8l2f2;)Xp=kXfX>ad8A*W7Fk0m*hXGhrYGpU&p zu-W{_C0*nsrrJZ3>k}6o*;_3)vRqj=uoiG%)h~eX0O3LzB2XM^4f7J^YKc121!62F zQ-#%H51~i`U-`7AO&|`xQmDQbYo%PkR;|`J^!f3~qNx<{XbUCM<>n||Xv?&z84V=T zZ}tSVV*g1uv5D%5)vOb)+UF?i3=@upZrz#iOwfe)+z$Mwh$f0QJ)duq>B?<$>tWom zi_`FOh|bS`nL<8gw_(w^Qrk=SX-gsq!S^jze)xNSE)BNJ*O=iL>SR7|3tBdL%7l1d zWeC-sS}!!)wSwu{lwK|IJ4~?*jaw4O^N?1KoBHX!na?!z!Nnb28rIq@R&M z`!B$fS)7^_tE&Wj8QN|}@e=7y1pV6$dW1flHB(bc(?6Q~dHIthVW5yliN7ciMO9E5 z5!dvUcfq0hJgU@>@j{@`?J+rW`q8=0s1zGU z8W+*0`G?|TyWS@lJ;%f!SdJr_pRCM}?U-F~a2$7+C7-my221U`@?nJVxQs3lxb!Z} zw7UJoCtI9MFu2{lek)zSom7Jx-(3(;&;JDB-|mUmh_y<0#|ILKd~z(}b;YE=r9T+2 zEDphWIj|G-Q*UuBWH6spgFwKb0;2Nf!16k}oo-W{ie5S=TfSp0v^W*J-2D;O4e$n> z-@f}fTH7>RWOR=6=r7~uII!msZrdc9hGQgOzHnJH5VXq%*yHZ#4;xA&I@7>5V#8Zi zhjoVdK!ihfLk)DVD75UiHbEgAT%l6heM2#2-kmDfYxchjYYr`-+UlJ0VIF<9YWu86 z7{4*nLc1OBr38k>rd@(F^Py1_FQ8PZQKC02yygFtMPlK-4IkQPzCIy%b&_TqIsZunZ_Ka1?M9UU1lN z-#$CST1*YtXor%-;=eP zH|^qp0Vk=+K>eyLK~ro+@-^KkD=Wj3R{!{m9=y-H81K*rA7O5?sgl@YW~nTn4V4e4 zgViwpQ!fO*SH+~IGvLU=lB2mNgS-i|{hhn^?8qiGnsgQwD?7oY9OE|Fn4cH>Mq1vt zPc+(~$s^OGq0eO!f|;B`pYRy&@nrBaJ;i@FjXG3P{5ivnoQL6_be&NAOZq(DFw#q) zkehbHJCqH48)|jQ3Fa6YoiG}W8IQt5zC1@}RI?^#cjPTKBNei-v#sZRbThTHWwrJ0 z=ZC&@Ro6ix_jm7z0mquu7*rnzs8KE>oLPy0A4gvq{{g<{TxzsG3CX5P0s|?dYc+=n`^$#IazYRJlGaML<9#1;pG7?+9aT1D>lcbE>g9a z?ap`ZM|O{f{iHWUD9QG&=RHfVnDXYgoHEV&JLo1Zu_8O44&H+roablDvqQk`mr7s< zmvQ}!@g7KFq9DhR@rm+D%`kw-W5Kn8*AzeZrgu3dxS`!LYGdBJ+T(J6zL)blS{hBx zYJEbOaCzaD-F4RyfK%w%aJ+VKbj?hKsDjB?@g$wp#rUYjzg$0urSU}6MSY~M;UXOS zUT_aDutd-JO>ma0^H}HNj3TBEmT9?BmPgxhxg9=}v$ED%qnF1^)Y`ykTQry@Ajdt)Zf@bYumEKaUahSVBqtJRJ@ zdG5S#8UCtuiLWKVaA_BV3vs>akb0MkYw?SsK_7d7tqM79V5`s62EzbK30Xk+4Yn+} zmGT*sc1j&o7x>Qcstu3Z@C(>R0ApuS4=P3oPeYa_-uH8=b#KOM)p+=#WY08*jvC4G zGE;39zL&Ek1~b$N27evf)>Th2ruH>ViyupIYh#5|dj%P+SE%$()k=a`Bw&~zw2t1$ zH_kW5-@3VR%O8zqnvDC;u8J73PyoNh@J|J|^K~ufz0|wd@^9*vv4gP*Qp*{*X5}8w z8{@8gx)WKx2i2007jNgV0*g9*5(U=08Y5}+;VQMpna?vA(D%Cln|OWVbB@dR=y6WP zXujMbh%*354iC9@{~-QPA0FDYYV^?#h(ck6%ls zxrRCM{HX?Rv>T3RamLKfjxA|nkasA#;F)#=cQlt1zsO~Bt-2ND&^}d6EmfAXAV&{& z^U5z(HGueALF3FVD;LIO<_;|A@oKA!08S&) z-?JCE=e$1;heGHZ^(`a&y(;V#xgjao4^dMV9yW@2>IH3GavUZW;*wI9iK)$8C3UO%hFZcFd>?qG&FzqanNa}kh z?Db9@lb9sxZuC}|W(!TCsfT_eMX4Rf@cJGJI?iCG@uQd!l(d2qXe_rq&`yyj{$*fW zGu3zgB143TNkXJ(yUGquR~s}lRzzNQfYTC;b53g#!t}!2)6k|q@G8I5-sy*opZJ&A zWH~a~%aoAB8;GlW26mrzx2G~T#>vxPOwG4Xm=JH=Xw>Qod1Yj>#CwhDt9Yg>vA(Q< z95lQ`DXfIaLAmzv)5VrCkjO>}=z+K2^8eNX&>M47cF`m!w`PUcqI5x{D{Z*g77SF@qXw{djCA@472SmR*tr1TjtZ zOJ^UXu3Ah^Nis{nQ3L3>#o=8ip8Q_3n~QX{zGUO{TFycB^V=zA=kZA&Zg*|;Ff>0u zu5@S;ymhmAstSW{|2rFouPWExofO5>{AYm!);T;00?SvgZcee=lE|mmBMKI6fjtUsjXX#U zudOC;4!d`_4G$-Q{shqW}h*m5If-hV~*VDRVp$z#)5 z(z;#-ui3q!Dqs@Pz}`AGM^~vO{$0A-2<}?VAI#z8*UFsxV>AvX2MtV+Q71=Om>q(C z#JF>JpK2%ON1+g@v{{9#IyfOTpgjy`DHE3!&`_@6eF~d#zbrF-q2zm_4JK6=`_(;d zH5rpALRg+6d?jA57d~*ONIQ!j3L_IE>77ddBuMOh%&5O|gS0)JEtna@<>u@lJnz5g zMhJg3O%BM15x5dWndBiW$mUpToJpp#iPP(kDtwR8;x+-Kmu%@kmM_N?fV9YLjAoMw zs~+w31X=41%alU&E~6YpiuFux*V5eS?isZ4gm?d)xPc2IEhrz;_mIe(-+KpY#Gg|; z{Kq`#@aOTAb}8tyHCC;pGkjNB%2D6`v$;S6Z^PkMW{p{^HQ;7sckul23MtNU-?7wW zR(}cYCx$mnQ2c&R#tHWQ@5s*bB$GLN3#LMtSk@ z(aFKqSeqQN*WyJ}t=aj}Ol9}g><_0pymwcl6WQI8%OxtcSwy~jg7=(DHAcbYz@Z^R zk*!9SYi@bRXMyIK9S)l<;3bDhb^s7X7j=8QRlG|we6;L%nl6y$MDeQvO588e-&ees zSgjAUkP{KX{dZ0COyl9{%1*D7=Y9YM`Yu_rj&^r+N_i*My&gusey)K*2g~~sksWL8 zEW~4P;uBD-$Jl(8aVM)VniCEAIG$-3 zwKOGyyFMpzuapGxGT`GZ%9t(HWpTefCflE_4ka}R#^ClB+!p#begE&465O*72?ReG zr%yVbs}B=gL%*-){uhK%w_w^9A;Isx0FNopyL|V$%_-#z&smtyNzFEGI9QL!$Yv?* z4j>~57u7|W;8IVw8~MMSjS*@R8F=+1AT>o8O%!&(;sZd+CnL7+> zN-;77U{>3oIAd~c!eoEfbma<>#JN6Y;9Q6Mn$s)Elo3V5A2W;e!HYUKe#uT7bZ7%= znRMA&)(4u9T{wa!6DfsBTxDz3H9F>KHVdI@e9Ri<&-&DXkC!Cw*)1dT@t=D@qmB|* z==4z?zDRIq1Cn{JeD7hL&(JwI)BTppHE{{wyde>k2#v_#0GQF&jaC0M3VBTWV3j5@ zyQh;A-gf)jE6g`q8NdJRsQ%My=J9}sFl7iu{JHhJ(^G)@BjD!NyR@2oyFQ(XE|M$M z<#@7*j$n)e8q>!Q)}G~F00Yn%-j)VZr8ct+gDVdq!`{CTLIgkjrQc2T=(}h-PKuXD&NGZsax~-a~`-?3X5h|-c zg+a^#hViEyFv5}lQK{&N4Q@|NPlnEGEWGfet<82v$9hw*+2Igw+Bxc#u1M_KlG+@} z7Ut)HKcDV!<#+*%5;1E4Hf$N@@9k+NEC_9OupSuk$e$JjTB`}M!Q&02_3o7iNEqK< zRHZO-czxR3x*tf=^7H|EBnQX=fi?1SJ%uEqGXMqE+~{&g<#xZ(Ct$haY!~A4KX)rX z4Vik{Xroafkw94ncK0ihV+dWCL}Eyv`}Hi698VtM^oS$An&?|ZiDL5N%@SH}??ba$ zomUBsRQxc)Gz1VVp|K&-bSlQ;>W{h_I+U|zf)h3AJ8!sE>#=gJ~@(agVI6dZ56qakc1@0(&Dv4^(>Jt{fJNZ_j|w35n*_bVm_hCNuWS2o2@q0R z<6xymXRuO@=Ex7{XuW%BW|+0HJ&}`WZ*^XEC-dTZB7Ra^B;6in*bhC-w(H~4H@T0& z8(^&p4}E!0)kvBYwkq|e0yl?p!N@`zXy-_pA&PEJrTnUZ%z~TLtx1ojiP1~%SGfi$ z5a&9lEmE_sX{lAuF&hH^3byg^V^iF8&?)E za%6-myQa576+z`|#&$xU?8(*jqRJoM(sH!F!#t*}&B0dch1%5DJuR*^B)y)lxYW4z zXiBUkGZ-aJRj=@G^};#5jQ#kALM9VH?8cq;c>D5`O06XaYMxdzlzwxBndsr|m;@IA zNEA7lxoO9$tSCI+5BH$;A+SzwAdnv3YHMf}hMCQal(=ypnLTSbdC-d##l5 zWi(exJ}(ngFkfxT4IqxOxxOFhM@1`qe!9iYV>iw7sU>0S&zrC6ZB$(%d~Z)Vm_n0j z5W37`yoExg7Lg~BDumpg%(ql$me21W#)1#zJTM|xtz&3C=QC*+-uHoaeNsuL$+S2~ z#iT^|GdZjyfjm$jA>jmm0_PwcZ`=yL?hBHaqsu7(jv8vg?y6->Nr`khHu%XI(jX^6 z9&fGU01l9|C-Q|!0xyD97NSVHG-@Iq5&?$>+Rl^7Y=L+D8H3*ZQ$LB;Rx5;x0MjO2j-=qYnopfl_hYTxLiiA_@|Jd=5TO1V zJ6ooFT30C?O?-iV#+02L02mM^mn`gLEAJzJpQTHXp22rUl1cbR)`5f(-{#|p#W9h% z(sG85u%Q8S-EULPf0ow(6>uNZAQji#YnHHg4B*cbAAfq}*OeysS=r{e=!0AD_V@bm zi`zsrSuA9cxzSjnYi-pUkMyz#Gj+Pc$Q1lo&G?cex0SVzAf~$&lvqNIIDokaqY`eQ za&PNZ7aj1g8C)D(8)e}dQ2tr0b7M~Bbl?Sij-9U(A5aGScG1Z=!{8^oV|KcHML%)# zx(>z>(Z^EBbMX0((t!Ou=mgX?p;e#($&b9_1v)`T(4lVs%t52;RX2Z9kU4*GeW8mk zE>?d|n_CWSJ>+EA5)?3TNDhm&N`D~YrNkY7EQ&Sz1Ah{Ok&Lv}Eu?m=Y6)X?>V+4k z>kRo$>4bT@)(2mATyCo%^GYM=!@+oJ=I_0cwNA%U?rklNq}1#Xq5UBcIu&mnNKLK} zK_W1e!Dtk^!MP~(hHxiK9szxuUwFIU*$`!MWrVc4AWZ7v*gS)(R(+EWcd||0rJb*| zq2?6@T_lfigM`Pj$#ZR#8RU3}Zn_L`a+fJoohs6RdaNZzI2N<{#BHj}e0z_cNJsty z5SbGx5>IJvqVa>OT-AcQM<_K?sA<_B7FCON>wF~~{&u0_(z1y1h zIcc%L#Qkj`%9%-;uHEH)DU!l~iK%t4IC<;Vcyw=w-Q=s~+^L)G%cD37EC-mDj9w`} zx6X^PR6X%mwM)p(U!pLpoGn)oGA$apLTO@xtm^gVOSzv%j%vHeCcD9gd+V$;hN(+} zzdKDq()D9`4v_A$UJ~nR+M&-8_`)=l6h1kfYBt$^&*g7Sm@?^lW&*;8)JH^Z+?elM zK_K?VG-a+(&&(3(@as%kR+U-O0 zc^l8XkO~n4gIr?GvDRcK`8n^EefS({D^_3Z{4xBiZ4EEO{&WA`E>^koN%!tp`z)y@ z)p*jvFD!hPn^}N!hyMQVfB_z~H`bJ`Kc7onYws7L#Q?EsgJ0E_@ehO84`MMqSR{?f z;^mC3?)J>~0UmV^veN~g#ZH0wv^+0vE_U+h`)rmoj_>I2lRc;Gi>J$!dla&vu(AW! zB=g1Jjx7f-hss4A-5G5^&~xvzjp)#5P1@$>W=&~5pL?!vryjR}{DQb^9-x;~Do;yQ z@6YoMe#>|Wjw^J$!W~`;!%oY;jDxFaY=Lzhocm@D#-yh&Jn(AeZ0|DDlQWjmd%Bu_ z!Owa(uPNV-;7|U>vTM!jXs6dVh4-U$(`cF3FyqYLCo|s_skM5Rsl#IIxG`AH@_p~Z zl0%Uodvc#Y6K$=K zVaN=VEkCZCn-hyi`Dhr;C5g~VXYKT1*rYzoYju}D$wf)QbKEHjOb`C)Bg#gn&ui24yT-Z$#4G{+;RU^LM=X)z z3Dq5ahYZ*#I-y5z8j@&JYWV^xI6!PQKQ00mKU(5D2AJ89Q}xAUZ?#i5^}CTHxPW=+ zXm`3-=B+;H2=QG8=g=zyiR1xd&E2%qoOYAuQEJ6MGw$z+_YKkV*z$P@%dF8dYD#g7 z1WQxDQn_z{Ykrx>MR(O}4O)|L#Or)QQ=B1y*mZ+ShNXRRpAdVqQg~`KWW=dbdhQ#t zJJPHjK3rS>-J7A$RQdXDOSeFntRBBjKCB7h5AX7;(DZw6Rv>G)A!Gn?4XzJFjb}#f zM-(qvgK>9phFrbS!l)9s>{JbJ8jP142QGQI6HHF~eXzyw@HoKgI~Ahbeki#0c;di~ zsq@p-SD8XMT5Vhjlg*lv7p6<%{D>of*z^;v{deUpP(7YkmBz5yQUvZU<-Iq()sG}u zg+E?7Vn`g|V=Z2@a%+bCDyE%E=LV0%3qPCa7!E{ml{yP3Rq5U?lCga)qfjh$AV{*V zwy`oi;Gwbb9O2~x8B6^U!o#=DNj9Wggu`uE6@tMYSW$VW@QlqJ6aEk;&cu|y-%K3H zgYM1Zw;L$W({bQ{K#YpcJ?sKC>j}g-7e1>gUwz+AWsI+N?J@Q?1O*0eY+ap-?Jh@T zYafK`&f!f_LW@)PS}3qtLhQw<(qAFAt9@OluqsB9D>Z(#gUR|zF&n@n?nM&>xeSPok!jcZiI+ID*RI4RM86Q_F@YCcaz}&Ahh<)W&4ImC- z9h|8OdCB_vW)tcTyN|N?%@BcR+^Y#mp^(oA`_nnaZV6zNuG4BeZ~!-1p4-YNJ;Ade z?`3~^h)0Ia5-xXhysOX;!C+22t@O)I{bs+?_a@ykb+1`CC*JIgh?>ZHavnXzV(L|uLLNgcu3W7v z)7pzB`y2${S-v^Obrr+Q@{{JUk4pwmweD0d$IE%0JGdd`tExj`-|m~qH%U%qMM1J^ zB=MAz^-xJ5Em{bR+tq%%;I&qJJ**5&@=s_n_~-6ja!V*UYnU&7)Lw7~4qS9t`qMD5gT+#_87K*_PK(kc8Oe2V22= z-L>n)t4~zL3T7F-lCu-i;3G*RT1k3l=_V4jCi1kKJY|_v*~~%dIZy5*3AqC7P(@UTA07VY$qrrI@is3-b!>uf4q1KYH@z_fG_1i%owNns*r zrs>9A=kx16!|V~Pdw$pzS$4!;x=T_mS z0(Kd8z0#&$hdHmmj+?L#e)?mo|Lt!)hxg2wGhOV)`eZs@f#gyy$?R>&cW0R;-3M<5 z7p48;=MkbYw=Ir2jCkIIwTsqY{vM0z85D-qc3znuJXuSK>~=fhWb*dg(RU1d=A0@NF`Sc^o~PIP{1$RVq5& zO|aHzMvSOO|9zYKagJ!dJ%q5gtUsHSzE;lxQ&WK0?>&Gu3^-cn1byJLNe8RW5pnBR~$D-OVEKbS%G16=$xV;1*o zSUMG&3jS;;9aGZywO0z4iv)?hq8XojyM!G{7IY*{uF*_Zp$OzA>dRc#UUvH#<)CuU zqBpm%(R{2vJxvN-0rOE)TE9kpb$E7KD;EQo+ml-fgC|4abh(8C8UZGFQ(P@-C!Caz7S+uD2NidqA0SR`C}<|L4ITqZH~$B zlLTnsy8NI(_tsQ+jE%UNN0OK(J0%nm6}!1#e!ei-S$~*A0FtVW*Y~1ueXW6>*PEq0 z|IQO)B+H?B`H+MPuZKUsyznr|tF{xQXQgG(ehVHR#PGuUN&oQu)u+<`kFK{4i>hni zzoi=n5m0gf1woJ+N)Q+YX^;|WknZjh1Oe$#V(66a?(XjHp}YIt+|P5@_xHSidEhZH zd#}CL+Uq*6&vjl_`aeFY@Fe0m$t-1&$H6)lV=_r$5v1o}Mu-)n6kU#1!iHvRpOurf zFYG1MM5L!^g>KAjZh9<^v-%B%*~m&}OUf3e$QdX!)?PZRXvp`Qt>4d=ik>%_pJhdA z-P;LcwOO(})#_c9)oHF)1}-}luV{Hf+1U8wlux8M{{m!)YjMTy6 z7Ly#qWdLdjq%Kj8$yC(stzYh_=X9w!$1<{Hev@IU`;b@!@`u~npF3)jj;`=nSd-y> z!SJ4*I>yCO3}vr3d^$Zwz^JW{!x0*1iEKeTti_4#HA~v0^8NhL(Q^~oX)(9EqDE=N zqP>|sE_=I8mR=)J3&>)<^gj=F&Z7$RPr(g1$sbgLK_~jQDl_^W`EKGgjpkY>#Aark z2GTaG8&=amVa=O(w9H{6DN-8}H;S75WheuYP5*}qrDM+1{ZpjHTO+_rGx=4R|xcMCm*IAPqa$dD~U_a zBR-b>YoclEhy3aeGK`EVgn|e1e34n?`uA|Xr!C1b2kAuSdI}F;=s8PX6P#o@i>Z-- zRxH-9tei?}YjhlZ8Ytfap1(3ZGPqGd(e@ZlE3yy?FSIx8!+kyhdpGOxX6Ka}A0`AD znh>)dAh6F!c4gu__WOWv{7V2sN|w+e^@*`B=Z6LgAN~{qSH!7u@|vIJ;PHZi?;gUG zr~A8U>S*$B42!fAQtD!#58t^9;x0uySqMdxR9)&q91w>^VeDHFOEuhg*lZC}Z0mjZ z>Oc^9suE~_#Q>(9$e&4>-yGRl)Vv2Tn(!1+y&adn0U?_48>%!<5Tc(@yB@vl6b|*B ze>|(-@Xgu$GwWCQGxT@IyuJ|6&*u@|mn+5No10afCQMB?b1*Z z^t}YtguxqK839pbq-5A%=Q}G&v5(=%nvf;%Y%q9Gb2+aeToZd_vMI{C#NV&xYFX~$ z5)0Rc?1Y{=E(}gYsY8iBmByhY=glJ~Qt#KwhnmnW6UA9Z4Ld>QPO+YA(*zpImZqI7 z67)#-h^}Le0_{V$Kgb$6*=$G;TQ5*eOr5YYv?U%MSG*;jd@dKjb-b@1ZR2?Cdbl26 zW?sMKdJAOE?c@=iQKpQ>XQ4|6Jw7thw2n#YD8=}9MxYFoSej0(aEIOMzHY{+t&yB) zd28ty+j!gt{bIZwd4YxgH2P|@zWTXe z8v@H=e;hr{HO9?*d=oItp(uuie?)m^ljCew~$kN93+nh{_*~!Tq>-YEDc|R zYBJzo0+Zf)O1&g=5_!Jae6!~$?yEVqv~yP6rPeL)+`5L&>x8ZIsD!}*_GLE*bvHOr zw-aQZZ5_<9&?`OEx2D004}~8H&Zjb|TrcunFI%mPQtWzby%tHoV0XVOr{j%aQbBb; zDA%UVYROgFHF*izH-&wY895}NGa|osZ80+a40K0T98l4VMYe(`$GF_+= z>4WBaEU+A25+!OVkG4Z*W+NqI$hIFHsp%Z?No^^>4ebiLO)8lt2$Iqs2;$50b%Sq$INdFlgQqUlKC73dFcIbRFZ7ztcH_dlADrf<5$CT5^ z^v65cg`3j8E?j!m2FL#7**+T2>+mxRN1FAAm>8M<7Y8Ou)uw48Q;pi6Hvl(7ulekyV{I%7_Or?9kgaFd5Bd86yJ3C_Uv^Y;d}+w+nswMeNdp*2+}(O z*xP997@4e*HLphp9gL#u=V#e7)>|jaWli}CMS*6Yb{){9OH$=-o(Xu&MDQwc8~Hg% zQX){wI>9fO2Y9h;giX)hzkeTN`6dsJAi4pdV}%yE&G?FxgN_#)O?)-(G-@-_N{`qu z>B^y`1pA}Yr!Yl@(`-UFw(AF8x7!2KD--j2!FA4|@zf0k12YA4x5nl=U&f@U%CyFX zo5^=k!Vi9FU5G-SSMn@zZd{VSFVAwZNtb}`_H zdBpT$SF?E*4#>E}nLF#NIaU#dfROfKRGrovY!4T$*q@bc0F(N+m={hB(dqRi)O2=f zzY1^k+#U?m*^EE;9i(e%|6C$+Z3CBlrVtPI>viV^ImVDDu8h96c!6h;Pj!Lf+GUXs zXY^|cJLL{{x%@Ha82HiBnhZI^-14glVJK4F0(tJiYzy54^zm~)3$pz?Bp9KmV<9Hl zR!V%RtG3!Mqq|HH6;4I9FT|lf7i;irG3>?@nCVxyQ8})Ajm6(~a0f#%$>&cVA`8A2%=Z{C z?xW2cuN367SSxe7_-(ay?`LNQZD5%PnVoH9Nr&282_pC6+{l!67hri*pk}TA{(Nrs zsu-h{-rE^F9$wmqfNUwWa2Rv?b}sN}vy@YF{mdt{P=HZ6ihe4iZm zbcOh6PP_G^kX{rzy|^VO_A@SR5?^S)r=qaFPJMLs;aT`KnIHFbFjtKd*1nzAg zMoi(KSqYzb3c?yJpXI5P$hIl1>MZR_eo#om^RQ&B-lwUu_aQ3X5zx#%yn{lVb zE1#p39c@qgu7?c2CgYTl|DW9uCjpgMiZ z9ck}tq7njrOGqC^cL3_b<`aGQMs6JnAZIsBW;&~sukb6WE*&Lv^pe3b@%m(|dw&&u zEJ_UA7tO^wz{+hgU$HQTO#J*2oL1D4o6Xmr?g-kuZ$m?yUaB9=wfjn=w(yM;jtTOW zZ-|o{=K`z;>*NY;xBT(!uic&Zm^!B#_XDH6ocg0$sVWx=&0?YSZrmr0ou<`U3dz_^ zOB_Vp&3un3kDr3Gs3UuvMP4fD+>3C>6RahNeGu`$_)~)d^BD7%2J0nvovuM9=BkPYIpG(rK``u^szMKGxmytJ7Wc)+}V! zi}kUy0eSVa<76&3LrlL4AS``9qLg z4UC3X9+f0HxHzN(^^uz|S5yk5IfVH(SO=X#ACX9Lvr1MBbw2SY1rhCPn(77SD}hvsjb(=@TJ9WE=4|y_pw)gM$1RVouF(A ziPDIoh$BaV#CYq50Tpd!;di&meI_~_0h4KfY2>aG*>iPPK2}Z~0l-pG8na)%j=ik4 zGM0Dh4Z$vXQoT}!9!AG>t0Tt*CT2CnDnlg$e0q7q2MAG(r60D1xRddo=T1@8Q=C5z zyxb<&92ML2zv$n#19lOG;i?r0W49r~FSkgWIViEIpjzLsFd_fPNB3bA3dyFG@bGs0 zZDB%MB2WL*lXo(9<>q6(n;xF+AonhQ$AZjV`fo3p8OGgYb|D?wZXAiV8%EOCyyq!j z%(%{>dno@QT>)2_6KnbE@HEaB5ki2D5%8IFx+GE>umPu+#w zSXf&~JRP2pI2ny(oY6O^7l9p;l9A$oCPT{{Q;#=JWeSkG!s%Xwx~fz+%xM}qzq`gs z(*+~Dp$1ocMq;yp2b#j8-^jJ4@P&Q>T$598O=EDD$6@bMzs#LyXlRaHu%DkqqdJ?N zm+kHiC-S?f3+wgk7EEotPW;!F4Z=rUXXaLWuRB0ewI72&rV|umryEg*bj0`&i#D!& zeIy-4#&e}QHb$Y&3xebcrQjjl3?Az5duBH`QP%4iItXiGrDaqGehrnfawP*Ojkylj%LvG$#H}mc0=CD2SXp9T&$FdLMfC8 zaj4M&@uTaVVH{VlqDWTw5d8_-GkGOB-#Q-nb!BibsBb3yostkDK)f(V5NoAt6RGe6 zT%JFuZT)fK|3wfQy&Lm9_D4=h9WEXPB1h8N$;k)K{&iu%s4fa1PUJ5vA(>NT?-O@L z2=6ni^skFvFQ9eN|3fwaK8J=V7Om?wyT<%2Ik{*nR7=45mGXZV8}JYK4XDup)3xI3 z#y-iAY(f6}PTC-EwSTTjKLq5*&mG?xaWui2$F?hUl2GK*|Gte@4&;M)#092sTCq-k zSW4oR{~gK7G`hIQ-(MjJ@_UYGB=_RQ2E>QoRfx-CEbC8pTYncU{?z2P%*(FFY$*Z@jwFc2^H zEN|P@tKDV}TL|k_`3{9n5{1wzTcM7OF_2-&zk^A?YxJ+W)lS>_TZVjh*62Z*LjEcD z$T5{6r6(W%yevOebUsH=H~hi@Nah4n=E!A;zRQxM4K9+#oO+@&DAD%k%zmKuDyc2k z_gV^>1AGW?Jf;&D^?%&1)G9A|`)+eu4&!fq-ZT0g7;=LDFemodyKI4g#rcVS zr)Vu5+4Qj3p0_-&F!!ET29)AFfHun90QE{M>cq z3yJm)Q`__!V08yI)?Hte9Z}7#54@*Mq9`)1{S@L+8x}VI*QqXq3z4P5x7-Gi-{`e$ zGxC=1Oe<8^IT2irJrG+4GwKcz;N{@5$-oXFpR&E^w2I|%;eR0(9zDM|)P)hU;g0Th z;`crHLPhbqyT{DSp1?Ix5mUsKRio^<4A+dZhKFG8n7Vp9AiInz8s2w@Bx1O_VezDt zxy@Z$}+_wAv5-$jpo{2YCq%l1v45 z4Y7OE8DYQ%Hl2=i+@5|5=>G#)&Cy34M58w{=TtKRPzIJtF$TqD%Z$_8?yd|eSjCXP zya7G)zzE>#*V;PB$7%OL->lMUTT|t#L)=GE$9*FFkk{bXMgkYSI`15i(b>I%$=*_c z{$j%3xGyWj(ZbGl1tg>C8V6O2={fUS+FL{aC4fAvRzHv*q3%e+TQuy>tFtZf%i}f9 zgq7(&{BgA_w<*m?h}dJ`ck@rVUY{B4erGC$t((TkwfUI-SVjo$oqn`Tv`>--5LB;q zBYvQV3yft)W&+KJis}2@t~4W1p~KmgGuX<$D|?gT1OHsUAmkJ@&(Ba!fR22he2DC# zI1t69bONR+dq}Ge(i~Lg;`kHLr4UcKO8*!5D& z$NYWd-8l!*GJ1nx(?ALB2jbJr85t2N`d6bXoCx+fS#u+?irJjq#qTR#W{YVok_Ro& zJ%T9c~JJ-$BQo z_*ZC@Sn-3et7cdtOH3yC2@?}K@cJcpX+HV1ARQ#ASh~Eo>}uo36zF-t3cS; z{*;IzdVub@u16_9o5$^D^_h4$H*3g$p`Ue!Mn)2mx90_FRUgps$kPGux0iHRKwWy(g!lO zmq&!+Vbna@{KU+kI`#NnYB)d7gcw|WnXc92Z90+f$>^H_>^`3u=l@#yhPH#ek2CFi zH(z^M5R29pK%Am07T>p@>qPE^J3>e?0ssX^DiJ{}uj@N$+$%UuXS;(v$>A*zh*#Gg z0KaLR+rJvA1lqdgTz;ZXLr<5An0}F`{%E`$U=p+J2#zuIyPpkbmPu?WP6CEkj&*z@ zQI3oebP^eG?ICIuwf4t8F%!zUq@VGCHX=#$FLEW?9C0W6L~Py{W@t0jHtpM#n=1d- z^-sZvv#xiznQHoKlT-)w6&9hkt>4QEV-cslySc~V&pF$BD~drtYs(fhTDiV#$8Hdu z`-yw>Uhc-iu0opT8}qU1gzr7qL6zS<;pdk9*{b(uGkotI3qlU>2LOCpeob6Yefry@ z-s2!xc{X<>pkx05;{T^857@IouIR`dQ>E5p&MJ2kBuHhxl8utundPCjl8@auSKr#* z(`8<|oT9FU=nz;?D;?GSDo&Sps!PBbWohWfAt^7O*+fVpZ&_*ff?-jJm^9P?CLkP< z*(V1xj^AI`B%+tkdL6Ivsa{imKs_y$Y(iPcOqDBhnj?qXcZP3wx-KMRAkLsQ`WjP! z#=?HvuyvEvwTtp*)36L-OuKT~yPj*oaqQRZfj1QJr&DkNS~cmv?$5af?7E})?{*}F z+hf?ZYTqAuspiL4grBvpJnZm&Di+~zTOBiy5Q^g=ZBtFx_Jn-7b% z*i$0Qm3AhQ=BHjC5o0dh6ZfRW=EjiNJCvr&GUOy587Y^p;!XFH(lLScZ7EgC48?J_ z#s+203zCzQPgVk}O5X$;Lg-p6Pdo}TW3S(+t@!a+Zw?ct@{@;{Kxr8kmaA~QMj63q z27MZW9(Q%)WVw5p1F<4FTjNQe1~~10uuq^-BlK_Od0hrO?FS?+0J>eu+ghq}%qyH@ zWp<;T-Dxc*tO4K{mDDA`P07jPaA(0l4}W-f?a#m6I$!!&Mikv6bd`T|_ieH4M<%~# z;m+ieQ-ME^X(8R=71HKd2|Iy*zb*m#LD3F6>GpR;poC9)pzJ1*W76?1WFKfB>-#Bx z%FufWo~hg**W%2Pr58Xey@da^k@(5<4+6wIuOip`c=OjL5gG=n{3G7*gE!SQfj489 zhvwxTML(Gktgg?wxs8Gb_Tg*mAR(MuD{?YKY*<2Z=Lut~e(Z-xgX|HOodjP6L^w zE&>D-*&RSIi3f;gGJh%!C3&y)2s(4(+SeaTG~d}OHUf~@q{m0OdZ`@!Nx~qm52*q%f~AXL-r*NPxp8_g84o@Y*Yk=ie(3aboc`U3GGu9El5oj@uZw zx4IWf$bGMPV=5LMgdxxX43k)Mtnsk@%zjAsm=iSG{ca7JMr}YOkl+Zwv6L^XhBs-d z+sEx3&l7H?g$UquaIS;Kd*RS-&tILr7J=+>BP(@{_spjng`BT(6$ZEc#=n%8yNs!z z(f{D_u@n6FxdF_Lk8a$T0q8HRSqeXX6?~RrUuXO%UBc_&+C>w7fl_`=TJSRBsZBF; zyCg*PS4DNh2bT4(TJMi0x=$f1>zY3=`vVi#M~(;Hn)Mr}wG=cwu;Ct8 zNT%i9eM3ojE;*dk2$H8Cg6}@1qxc_>M%azj)d)i08g4e2srmb=FoxBHTsZ)Z=aCd6 z$f6NRDB{_xQFac%hoT^unS|DK5ErS*?=HxI+H9j^KSxR(z&kJ~LH3p)ja*Ua55xM{ z8Ir$E&~*rr?Sy^2yQR|n(g5wJ0TZMI%*^?#&e@)=^P?d}Z%l>voz}2n z&$xQ2=_SMank|6$DqXhl{XlSqWA@DS1`I~zC*zar`^mA*DO>kutXDD1C6Mc=ZmKR- z`va;?7J!d246jW1NMr%Uaj!3T5Z%=aBd_aJg@Ja?$<~1d>!4m!76(B&Y)X#c2lIm7 z!W(5$+qpr^=1Re1)5TC(hRWNX0VjwPN`1VB(Y80QC`V|QpK6&g5{G+naK2ZPY;jBn z#_$=>xOpyN{+wTxPZ`1`OBAc`Eg>kQMZh7nAG{9ed)m?U+GKPxu&j3XJ+FnyQpS>9 zUe6+A=-nQ##Y1(Ig1n%l$KjZeaPxarTBUqb4YM7OFMJ;=zc`JP5P1qQhpt*o2Qd}!V>E9V51-+?lFUGO)pjY33OLbc_WU8anL- zU3Di{sMb5q`x1G85JFnU*3V^ODh;pu$?%J{^phas*Eje_8ckiR{1f209yZ0($Y8P&Us6Voo1NFA6yi zNqv#g>`S%l#yB`YN9Bmz6q{GbauO)KNu26p?v2k4=Z|lR+Z)g8gmr)JX}1Yu*=4jL z6J;@Tw~g^Q97pV^0-5sf-1LC+t=oz#3vHAaxK7G0mGo7myMyeOr7>oA?9=Cvidv>XWIEcnrmce~Ag7Nu;fKa)Fu?04 zZu$kr@TEXy~XZNG4^<5yO2T&G;F`(caNs$q>del{3Y`Ej^R%)0C(TQuA({a)Szgj zD_>aIHWx34Dr``@e*6BFG{!3QPl4%=Ckr+Np&PTm(UkRKCgL4}k=<C zc(F}91<3kahBGE#VAXSDda7S3J!5wY^`+uohEbzayv5{5fuK&FobH?^}GT?Q?bG2}J<|VQ&Md4{%|k0yVbyKAz~v_=rRG7S!E3LHB3Zo81a%OLoz% zg~ykk1b3y0c`-ZNy7M;+d2PQ9>W0dURVQiBs6meJd686*zBbWz*Yn2$k?$6ugFV8|H#G9j7CJ;ZUJ}>BER+E8Znebn zW87n@==$3dGrB}aCzZMqM~AwwQlsYExtNiaC!&5%(ne;A-ZNp0=pAox5qtUg(ect+ zPu}EWe2#0~lrBUyERl9Fs>Q);y^($lpXkq*zFEj^Z{_Lr;4+=g=%r=-K1e+#@Od=* zHLb1vq*(OdA|*Ovleb_THEb3khr^DK908XAVj2l8HB4zzcPe7Nx$z13P{JYh1$lCI zb~$y5C%Z(X~4+>W$_L@RPCub2^APL=gq zHk7mP_LNJd_(Uek6r88^Lnm`-J)Aonfw-k~qcG+KvM`)3l?N0D9++!r*$6P<_mQX7 z7+JgKA3M2>ktlACWZ1!8FzIBX&=IM0(ekFKWljiT!m0T8(52!jR*?Fjyk^{n&FTg= z^Md#SDmJ(_9g@~tFCb4uvba2->k(52)xu#gZWd6xMSgeV13)B;=`s=B*O@H*dxB3oLAoT0vB z)3Y|*(B4z4ac}8bk9 z-Wax*0%ixBsKpq*go2BLvjkO!<~e^?X8$qxtz?KHC{vLFniM`P5aA>vtxTxZjZh^I zo~vdn2dmB_Hk4R3&qz1PACs4vO|%~(q%ND=5l)TGewb)CbgRxFhPN@W6ny0&?4GIF zPYTd{n$`h7qBYTj%B4x}4G-8em&@VC))dm{L}zL9f3@72sHmC7rlI6)n`}%@;H}sv zi52tb(bpTfNk+*SpOj_|G}pS+di#JY9bnayL&_5lHPdru9lA z;iI7~zAaL92IVk)ay6*%!+kxwK6XG|pJ-#ScEV;(tVq1B+D=u2>+U zkROmO!fko-^p?+biIs=GyJ&`yKsQyCvUhUM0pG!HS(o5 z01TG%q{0pUt%`z$z=eVDLG~c2ZW!IH(P5;KKusxIBnBo~7mnw*L%~?GN(Ca>M+6KY zz-~Yq5$N5(edCnW`=X&LRyvu2Yz4RP+uEy7*J?~RfWZx}4ICxljs=2s^t}w>@M4Sv z5pi(G1w}hb*#v_Wn|DY$^j@Q`$+Fp08tqtS>hBg@JexP$Z-b2~`Hjik*h~X^CXtwT zquSN00)8=85Q%XCD1>?RK-O%oc8c&hM!hA#V-ieSp1#9v>x2hlTA#s_wITu|O-HeDNpicJEwfxj+~*DQ}QMYQsw zM21<_L_n5SBuxsdXto3u%sJza(lJ*Bz$Es2^O+R8uCPtpE8}snxXub6#hv{@J({>+ zs4?AXG%p@S<(1EsU2bD=+Y_OxE6P|%hD<;?T>&76R!_t%_i7OC% zUm0vmrxO;L-x85<>qaiMk{b1S$!8W$HK8JJLS#{_aY^$Qe%+>rKZv(okbbrA^%jRG z5oui8-~vE?%})Idy;aGkFdW0av@Mmz+iJ^jYF%F$i&HK!FB!qGcqwSoo@~Y~i%ZKt z9uy}VwuzcyImiaBSKf(MgSAr$pufwb`cxlS*YKFk?0(xT{YN2!Hgd7niPFNe&zx@A z^|Gp6p($GwD-SVds@p?#D8`P^?bo0W-@Ild7_8h-nz3Ty4tI0V-8#0HS3}J z%N4mAsmjxPz>pB}R7^p=9-|Jo)hoJu9zCO%CvOmgB3O0kvUt97_acSvd+; z_kUb{UXk8oI%waW>YfnMNsD%O8_gvY~0@Xq6KN$R!qb~ z3ReL=R9G^F>|y<#ztpxRhIsyKWOyOEav-V37NH%{5p!l&`qI4X?B0z+o%Q{1yO zt6g6#eUp09iOeJg(D4jEdp0|sR(bt?#OmGG3@ELmnYyC*TjS~P(`H7>``C9*E~}fh z0LMObN|2R4HL@K|e1Vq~0$COA-3eBiXGex2)=gb45XBc2iEwjvu6274{>rOS(Cm=8 zfzl~W#MWkguF_AHe@cM>{y6BP(a}H zU??g%dcjBg+oEm3WUOPF>!iVS>hbHUrR|M{83)55i=MHL*aE4XOsADkjh#wyA(e!hXH(&k2sdk)m!&Ho5t(RpCr(5vV z=R=ngRgoyC>{m<*-~wvm20afgd#@vo7;Rz;T$7Il8L<*zCNyk{rQvnKfmfkggOaV+ z#V!K~r>qm6=ZO0`1tf|^M&#|z6PZd%oF(MRcZpK5oB@WhEo7ey6uE<>2{_@0+3Gnz zt^^*r3bNto^ocXQB-V`_Hy>H5{4jCzou2%zz%UE{?3=&)uYbX|UV;bIpjQwW;6BcW zzirju~-yRq6nj@f?vK_MJ z7R0sVKm}Xd&h!b%PYqM4IWV^%cN%_$!B`{+2xpczGkFiv8_~XqunP)YBB$8A*71FiiE*%w$bfm|Yv-pjo3C0r7AlT4vb+{-jmH~1sL4Ngx}Q12N+VLI67_Pt$D2+`xs?bR8W z`_p*Jc8gkev;GU9STO@=^S5QV68nQ|3~AnF1chMLeXE*pJMKQo@Wv^S zZy^(@93LH7O2`MS1tG!NB5&plBIo=UOm;^#Cw8+KEml;H@i5?aLKFTRC*RJUuk?t> zVdL71k^Jf^h&=gHa`@kQzB-`#l0Bm2ZQ{muyl2Y1^yC}FZmTxq?;4C101jon@uw~! zc8UdU+X*?B+-`ft8S@PG;wqPTmHRj)5>7NEs%?-`=zE0YMsgh#|Dw(S`l#wKTZ zwje0CJB4&&M4?5zAf}|=gR?{Lce!Gk-N0wVXsh0dzIgAauy@j?H*~+Sr$e&jg(poV zq0ubql3~s3oR26kW)Q>>aA-^)r@qtDK&dVC>X_hfw2w+gWd{h3FS#=Q02JebO_Zl`ie^NFMoi^e*X9lwDBZ`?DQo(=NuE z>Qrj%i{@wC(xNSK1CgxX25g!W?LD<>9hcZ%NFWg&lK{`Fpk5=n`lj;J67$}}AA&cHjt|=;! z0wlB+u0H`9<0FIkk_o49;RUaVws9u8f{Q#WJf=VgE>(4x);pZIjaG^*kZNLE%+lFk35NkGV6AkU6)Q_NCpQ|d(--`MZ`0E-vobV?8Z{on*=_a%D|t2D2d&S=ZB>YqRp;C;pQ z;K*}+A-au>5Q6?(TV&y&Wx1=gTL{owX@glw_H&9eFUy|sqUKu%@AtnV-%{xUc^FsM*b)G;iwkQxTrvu(qTT;J1N9gxW z_d(V=662>~Jo4Xx(TK?CPXpfi9|3QlC!T8xVxezmE3E?RNd)|t+X6%^Y|RJKRDXa? zl5K2KtY_m)e%#0>Ep&8Nh>HI|qx;8AOCv8MPuZSs*}bv9c@qD|?KOBZp3h0;Co##@ zOYj zXGNu45`)+;S%wd|7r%e>z`$Oe~l4{9laH!m|j0Az^!a=%BIoKc0og( z-lKlDy@ZPk@K2J)qvTJMd^S84E;S#CA2pIYSLkEU-mWK3i1%{p$&^(0=2op~>+1(%?W*IJd0hV7H1t8~xMJltIdezG(m^e7;WK$(_$?q;-a`1MZDN^ub zr>obT4^^p%Jz7`(gn9n%4T znMlB4i=1$Y(A~Fue>U|%6ePf}_aBQNX@DuR?#2=j7YQcIMDh*(Z5b+MGGX@VJWD<^ zEp%BpZdjGlerHyKt11zwChf&MC+C)Yi3Ic$ppUw;y?Zr#?sknk^vqr;0qC;F(ELVHQw@KA>PZa<$u|yd%I`alh(ys`j|&zh3Ho50GTV5?hbI z7tQ?#m=RkOp6Q9iu!>=4S>gj)B#@vz0ouyGP${vGU?}+5JbFXbGG#37}+ng$H6PJ{w^Hc3ClxX>A*;0jG}XQa#=W_M+XD zKYH009|8}=B4SFlBfXP7Rm%R{PPTUFzO*c0$iH5Oc{K{yY)bP-e9Bjm>A6?Yd5(g> zzSc3d#iKqAIhhJQ59F?aVMJu`o)Wt?qX9k9rHR9FmI&x>g-EA-!xUrh3-I5b>Z+Tt zTnA&Ov#!wMqrb`d`cx>fSg+4C=;*=~sG}sgyL8O7(k%b-lRDsI0xyQ1oZ0ve%E|hI ziF&veF6VF6Zz?fl9n306Xc6u%L8agcNIg&nk&b$5{M4$xmr}F6Z-HmeqQ0{>1oi2^ z3e^8zKy>(i0--M^UA~Xx(MsQS8PbaS?o5>o0($#TqOPVE5kh5zlytEb=MQgYuLAJbCI3!fk7P@eyn#_94ni^bo6IY(mWM8}5OeYrI05C4%?>iDD?P!`@Mr$$eI(Y8o)A$+zheM2Z)JCX4`5%8*!a+%aQyY!I9Zy@LnzUjP33x5esT z&+6gTYDMzkNm1QQdIQXu9H=OamhjZ2A`^-&ShI1=N-zpvFpm_>1+vB(VLagP{Eii9 z=~{^Bh$G4VxkV*ErE-a0D@zl0M`Zw%$P{5}vmmw`m(%xZv>2AfIxd{Y2v!eo4IEhc z&}-UuE!v-7`);)EP?}M6kxS#me_8UM;;8U}>A+>i3bGS^cRL2faNlP(ifo2wT*Cr%MuzEmpqU{r+$&{-O31)eX^eq*{^vJq&1!^O0+{WQbuCVD>fv|CXmyNy+7x ziR4>VyEc7U_IZ|!md)f>UTi?8r1u34aP=DB5E27Jow{>ruI?U^&no)&#eTnDYi3c# z=L51t8lg5&c#?+~Ydp<}f4tk$L_=8rbN~o^vTE&&6<@f1@VJe)2H_s{|0obR?tobx zI|N_DnA!GI2?me;I@SCgR{+O41AI&Fd5q;fXaFLqooEP`28SOl-#6rq`Sfq91l)1U zJ~}JOhbJ0|=BPv7wJdhqlgWA18V9urQd4{w-A@4{a^w3iueOfum4F;GMe9RPvQFGQ z3ej4}!{5$7F}EmGc)^vufPK)1Q*@k{7mtrV2wDpco=X>a6G)*WJ)5XAL7Qdh1^g;g z_foI%O8usz{b)z=79`4=(HSnP*#bC~ax^om0VhESct`68>ZR|k-DXu1;_QtlWiIgY z$etvg;ad@cP!$3=mATZ>;<@og$E`yAgU1a>suQi(MA5;px=8_7MR)$k!-y2$#;^P_ zJ2#{}o(KKUSKp4vLwP+-hxE%xE9clT1TN|%9)(}zCNo`2G#v_-qDAjsOUL6QG-uh; z`!-|(W{=sW33p#jf2*YPt@VbD(N8hitGVx7h|%l}r@v~i8-Ii-R_xYcPD2*^Mzw;s zGbvUZ`8)pR5*#pr1UfK(+lli>N?o4S?4;sk4#c1boUE7-qJe}XG9UM^accl#tnSaS z4vnRa5GD_K1#vBHR@Q6wt|+h^)9n$MrwM zh0t33V#sUGWCt`TV^afN4Edo+S<6AQ^)|ko=CdDa&i4!&gbb!EMsY%>Ib$X@@mv9K zy0P4!-Xk;MgN4*G+iWEpLjhq00d$7EA|!!#d(V}Ece>*w2r&1Cg%usEOK~-2$B%%P z&bm0()SLX|XFnxsITjgL{33SwFkBg+!zC8u<)#VoLGO8ucdMCfH#%kcjrPc5 zFxYC2YH^P3?t~Qb|9sWbYz4Ib1*re?Mq6M|T*LE<+iG1afe0d)3Qh66yRMJ`5E9D$?aaBhdP5mkvk(>j&PWPY4uuYIR$U zh+@OVcnl8VoMy&^c7RQ)gP0fX4PedqL6w?>=6P*be;v(ZkZ>r30*(z?p6!~|^hkXn zac}G2p5O<4x{t@@B`pBi1bZalzM(~V5N(%ad+j8kh+-#{8lRp zq=td2))M1$=M?049(UM~+o$d3>2`)n$9#v z;9JF1QS~~#5kH6YdWnY3S}*iKN$(U#PugJ&M;keaT?*nE>YOdBz9xfJAGi4VwsUCZ zWXi7yo>B`CBR7W%o_{`)x2Nxy|L5U!85c3Z;S4%B031kP6>t9Tq6;rmzQ!dhM$IxH zv%@Rid;_Z%{^iKy$TE9BR8zNLN#$($IF8p7PUx34w^QgkHjN|slt;2gP-W=}p9Ko7 zHWL)?oRNa2?Td{cZcBa65QQAI1KLY?#}BRz4zSz`}nHfhf~!>F;g?s-MhD} zz1B}gjmeU3Ht{yKSG}gxf#A!-uIb<+rs|iT59E2;#u}Ms2GkxJ@(ofX9$7X!8%sg( z(GydGe%tLoZklxA`RqHD^XAs_lGuL$Q2w#K2-5n~H389Ds&zZYftrk+@uq8y*t!qw zF96{D+GtX#MeVzgqQO)GDon1w!=QV*`}^=mhoVV0pLge%m|f4@^9X+c+u>^&2BSH+ z-(R9KxvGH@K{9|cBwab7Aj=KXh9PHM$Ot8|yr$h!{ zk;Lg7F!%wYz$^*nB;@>)W=2=N&9#d|gh#+q62pVK(~x0uAuZ7cz}3!8EhAl9X??7* zU!+fF+^5t>0X_*O89P4xyP>uX&FlpVECGYp-whpOak%|S)p)=#M|pn6&58c^B?3e# zRoJJ6H{Lr7ey0WCx9PSgkdzdH{2scRYas32$fGA~)q;Qac1u74k;erg6Y)>M3~bPN z`15~p>i)F^_t-$Dn)~@wuyE410Y)=ZuQ|veN=be)2dw>-H_4nd3Y>h$i>D?S?@5wJyOEtiyX-v_}(u@M>7Y>g(Rnaq}y0RAy&L%E0ZP3|1)di4Enma2eHNK&!F zFWNsmc#-a!VxwpX2ML}4Acl-sS?3iXg@YWJU6p2f;@l6TYPKxraGObiYw?A2>FM_j~w^c?h+t#&i%a@+`7<)5XfzcyU%%e9p_-a zNBdaj19mX_xvob*$`e2127vx4?o1wTR~K3TW=OQlvZ`^JO?Zh(r`U;wIRJn$Wq@c! z*`w7i?PZuyIdO#0bGM=Wp5&7EWZ7}t>2$_?N%C0RWi($5h3v1g3P_8{FhKt3O&EBA zzv2fgR}0hxWQN!ui2K8<;i>eRJ?J_@=RaI)4IdC@RrWES|A1v%EldJ3t8{CfYFUm>x(~Ywe z>^XR+N0O7Z+ZltYSlNnICxh;!ApULm$^WhK>c7mUGX(D;i8+~!V4g)PGX(iLjurOnaHnAd6G#ZkZ(Q# z66p}6XU5)L1*WLw(o-P>W6J|9;#JGF zP?K2nug>~D)I8rQIAofG#*d$P2s=@<^^8eHD0Oavjm5nn|KqS=fT&NKvvXaqoyE`^ zBhua)j+SvAo13+X_|H%N{fic!5U{)k;Nu9a&g|Eh$CkO8Z4PqQ@1ELDb-$0>G)nkj zqu%_*(SVXUw-rL}1XnQjpz2SgrkSEmW7$K zFq~RhPw%od=O?W-NvbK|q7ePx!+RY0+i?DPrQz@Ye3<`uqz;fMfy)Hef&bA2|9UAX z3Y>Cay!zW;{rX!J7(T#1v+Fd`Ch}k3_+S4J{rKb72hRgX@qe!kc*pyUl)sYtDT4Yo z=&zghzo-9i|L>O^iEhh;1~Eq*Z-Dq5%4X_`4EWg~ z)mtp1e69L{e7m9{4!SYIk0z7&$&CPgoA4LcC@4uZ;W;U&?u+%|bvxl&cQPTSR0)Wj z&1Qm|1O^?9#g`?Ep@Vdv_rg2FN#;W+gTwhF8a;rwlBqJK#!Lx`=Sy?x zeImuu=yZLPLxgcPWHWEsVju%IU}Fkvy?6FqV{$T2QuI^ZB8qLmf3~n7KT&6>jD<85 z!i&;W(O0clTIcpf6Z+j^ z1RVBCeNX~9ocYAl} zGz?k{&SYYBf*D-_74RnQDB{3Gz=$3t$Q>!qkLTFr`4t8(%NT(0%;vcA{B zCUul8yY}$CKIs+f<^_HVxm`UZ1`J5ZnGJ@&Y4l_NW8xPdfb@7Uj=R(CB2(@iPGSju z`}_#l2gWq3IKO(o+weu?t($S5Jk^WlcC;|%C6-Bij3bvaINy)*hW+JYt&p(l`6>i@ z6_m1m7{=Ck!JR3;p<#b+bPv1}4v_UChU{plByA<|)W~-n7~W&r>{Z9kKvk(V(VsYo zi-&a>E7xwNS!h{UDbg7uSQd#UxmCYR{S#_>bNhFge_}sgY}bLu;uH>sa~$m4@DN4j z^n7uflHQsMW#DCyjBOax(A5_?&uTe~B#fq&INNSyNWIQYzPP^# z`k+T zt#tSNuLI2&k*{t(4Gwf9+6LDq%I+Q(i}bym?jCD7x`G%fNI&^!(a(@g*~)tJ`5V>4 zeNq2J!${DO78;eXw0#kenp0(3Qc5LfQ94F0xBHZU^^d-}A|L}CSthgl@}dEc|G7)j zH&|4T9Vzce3)M-Nt;OFEE7{RzoD zt~Vis7<3pz@uf(BPEZ`+%Xzo0T;n`c4(PaHgaU?UAu$&Ma5}b^J0rzy>RI#>F$*bs z0SLIgKBt~Yf%w*D6<`(vH#&B+x!fDVu9)8-QK|_tP6on_T@P-#qxcmiK+5rFi{-$? z$%o>0=8Hsh29>EgHiDA=h-b}IUiXDMJQkhbfo<1^97Ws53vD!2hGRR?2Hkx!y5}3@ zRVCdU0G_gHNQvG0cKQAXuCr1swbmeMYvUV4PMC;D=8nC+3B3cSt)T?D+Ufv=boq20 z7u>KF4y*A32%YuRo6E6cC zN@len8%wbxaDK8}ut2jL4%TJ5}<9r%%nG z4xUR0=yJ>u4Smy5yCr;MP{hTwaCIYOhxfsTkWk4J z0bg8%@HtS%@D^C`?bpQw-R*R5rOGNd`%5LfAU6WMo@IC~27|zwz=|MK={8RiO`o)=7D27FBt1 zUu|9=gkZC@W280Pu~efEXq6_B37NPPy#z!Qk%EzzB?) z`_%)+c%k*H&Z?QASsbpfzS`M3tJ;!#0yu75_`JHYrZ!tSmxD2sj{Le{3F>%YiL6FR z9u_2;+IV|6B$447)GL)hj~a^cYD5lDy$g%ySPhPx78`H6F2`JgWL6fiu@1{youX4X zzo!n+yvu&n%kW^AUeh11afQQF*savY*yE~qxyC6s7oWw|Pt*m+{sQ;(w?57%?zyZv zPVH4fTnjxmzP_OB(8$KTfhCiN!2fKA#z+5kul7(GqVxt|i_Q(6^*F}ET{GkNFl*{R z_Rii`WLArzGG`>-Ud%>ol*Bx|XAZ`zZ8@`;tsLBrs7SS`f-ztBJB0!yPkk>WJuKx_+VUd&k|QPL*wLkB*SxkCZ{s^EZB&ImG|5(QpUuldhPbvW|Z;PMF;%)tUbP3-Fa@kaI_amUZpQ_fjLAn3ES+1kI zUv=U+EUfwJ@}PE5@ov@AG8n|nJ|l!aSR84)Q`YHw?MThKuIJ5u1}4N`|8jWcH?!~d z+*`eq%+?;%p3A}6vU}cH6g7;Q-XC^ZZ$nH#S6u-@3Tuc z^sIl91@rJexG0V4zik<*A8_RqHTUnodZ$0AM))$k1=Ct-uFxUb-soHdi@c>N0Wzm+F#G?ZZR}Q8e7x*oqg*{=vsexr6c9vyU;#u ze?T;HI@sJijwTgT#_byDM*MvfMJ9n#{o!x@-dhdzPvq1xl_Y@Kp3JKVdN4BQ8Wih! zr&#g}GV1Mt-puc$_ZDTPi#4|NH;IJk`>h+5BsSjln2s#Zj&N2%b zR7;v?nVCFlb`N#tI+%Q=cT5X`d?2NJQzvr;_HN`i4kP!2a0U|3~4g}i#u|#rhm-F}6 zd+`1=0YACW}zW+;<`TF#Ju>L z9a_2Lv)L3n<*LXm+SmfB$#fgr35!lkwg-G)|a-S0gPo%5TBh8;(aKLgyxufKPKl3b)$IeHcUz}IaI3Mn>#|J*@cw~Lb4uBtoZp&bdGGi-RdM^f1|)({=`%6U9^ zprxNzRS()qhrsPH=U$BO^WC4X@eZ-EZbb{`YE%Q_#`|FxOSNm4DA?DRj2K$sWy(!i zUL{cbAD=$@v}l{m5B5QfAuc@eV7yp~+#!H0KWBK$voDNC5x)~|tU^2}I{Vv3QCEiO zfJMBdI;0&Q>f@eaGi>IMFxkLzxhz)Dk5&NRWfR+SDpFq$(h>c_0rN%(3Q^uTl|!iH zbu*Ih9JO@4kMDGGeq+shS$BCsxh!2CaF-n$e5dnsytR+juD=WEz^PHY&8*TTig8xR z$;${mW_^g#qRNR1*u2G}=}w9@|3(arcUmeTUNc*$P$^MwR63fkl>wOq{TP53oZF4o z!Xc)yh(qiKc!UJg7rT-gx7nLqCR9 zR$%MnC-p`vs5Xn6xy5?}Ow_^cPZ1j#iM|JQjFw#SaW>oGnA^pjp!>lv-F%%UarpJb zb>eavF)DmyO?kRVMkDetRo1VpH!THcyO*CLe8)NaVp^xbD<&``Sji3qiu z)@-QHP3SINjU2}zxV}Bnv`5KaYIBVF%y{txA<|HlB{D(14kM;5RhQl-O%&JtIh?~> z5Nw$7QQ;h|QXsRxtCq1}FjWUeXWdPE{P%dkncL>mUh(YZ-#RA*v6V)M%zW&8j`B<62v5gMV@-3O$c7!eVuw!c3u9U67$*?9u|336?WxwNi}S z?*j>h&;nwrDZl6<`q6`lM2YS=j~@N`5`l$k09i&`Tn#$cXUvcN)9L6P76}mfffQ>*(EU-TRQZ(UFq-_U+J>P=?lxbGL5n#(!!7 zWClF@!kDOQrr8=`yFBE>U!1Tyed?0Y(I}5x!31xs9^GHIA@kAckf=x!qb$1^1N&9j zgbqfb)c$ILvGh}oxNyWuEP~2AEAKh0hWHeA-7O=wM>{Y@wrK)~`53bkfkxGjUr9^s z;n#PCtdhKeSI6L?IyzkiLA8m16JD7N50$Z^$2Fz>=}<)8(-!>cI6`7bAYC#nxtlt! zT?aE0jupD6jg>a?Vi2*%_pKrh@^-t^_aQX;;=boep6UC>kNss-WN5UNt%gw&k4$zD z58x+ZS+9VqAeJ2EAx;$nKJ|r*xDT@9#d|o1- z>W{LKpNV>WUWnd(*)rX9{-`Z&n+N@Hkr!%4s6ng(sHqAnKX)UpDC-v|4S1Gnw3ftY z$r^C*vxG0QjzVMc8W5ZLUB0t)-EZ}+Lxq1wxQvc+|GgrUVe1ldP&if5{0t{_|Ngl< z(q)m?&zo%_w~jx;zg%-)s1{?Mg|++`~|Sp}!%2zbwL9MRn6y)J|ki`%{( z)TwAf3gFQt8a`@h0bQ9T9&J~}_o!l9nVin`>EOF;xn0EBO@G~9YsrG-DV(Q{+PVKE zD`+6IIMR47qac}8K?mX521KQ+gwB##r;Ul(j&ypKptDq362tFbbxNz6!Yy*xUNjnM=jmj)}<={gPo$?G<_gec3UsnsKkC(4tjlJQBhUx zF_lc9Z$)`;yFG|a$;5iidjZVSYonxAiy<9u;gLmO$E_OnG!?X|)K6$;Y0Tvg~kdw>SJMG7XKZ?0Pw87xwr z2F@{FVU~YBF>U1$GF~|ZNo*NdAi@;LW<+pQ!rJ>dBKdgAB{7c(so50U5)CaSW-ewj z^zQ;b=_=6(xHI*^GA{2$A6dkneD{19yzLKpi(-2A%YmdEwIW5NlQ5-R`UiEMrNm^! zSpxw!QzZ_==X4JR8^`K~&$~;5U7?sD>Sf(30WXf`?hW#C)7eo1Z;j=PZDwE<9!K@! zfQR78kwQ75<3cgN4;<{3>P;R(e5Z~i=q)E1`H^KM8=De!BTM9Ft54_ z$#mFlT^~Wtc=_6Svu|XEA|<>`2Z7;{WU>=>3=>G|pWS)+*PIESD;o)7 z@3>csMOws<`vC-}>!bU+Fv@!fgE%B#`iP)U*q8yX22#{#oCqt$&9_Hi-d0_Iw!6OR zm;MgFxXzjAR#_t8`!G{ji&QO-31%TM9?vUl4MS8ZH9@cam~+)vOFYnQ45MxN;VY2; z4s86KxzDFRECv2-@3x$Z4#+%D}jb8$>zr>?1bM4ZV%i?p3GYAp$;(qSZ zK&i#+lsOL!-0VM`4rixR{rLvgFa1r~gfH|h=CtY57edpsFmSD@<3y{*EN8jqxo)qS z{#ue0?#c4##AYglpX}jNDN(C=nuL_NXiWL%@mZ(dy|{n{_9HT5|Nhs{()^%hIVccB>J`7V(8 zf{WLQLMB-$SZiTuXry~Cl0<2;R1_p($1{`!KA{c}Zd;O)$%0+HI$Rvm+p6b}e1CD- zs~t^X+WL5=JE>iYL};9;YqJcIY(G5q;)Z^-K*Arl2)?WzRseT#Hs|B0h~2-(TnF#VXbafg z4=w8$Is#g?lRC3kRc1CY-z^;url`?}7mlie>HSv-YL*jq6E_+dYmh-DT${Q*w_MPt z-SxTsleSgd$5r~Drq0D?Kj6=HHJ+O1QZ;&>ek>QPV&T~ZwylCkQKRm(-CvAk%T75T1JK^vxpt=|~w^ zN;PU@-wu>7>P8b&r{~$tKEk55#lE`;tL)9b_=%4RqywY8J`!GZH`Qs<&f6j8ys_yM={C! z^R0A7G-;zV3uKKsWgs8>h`HK@e44Ejwa63uQ>W|zW}QfAg7{d_7|5?6JcWHIQ5$_& zYn>G~iXVd9i{1>Y5sk_A8_c)k&_-v-%_byCAx!&A?T{uj>!pl~-LZ10-(KT6BDSEz zxKEG#HOh{cf*}s?65>SgtjQ4Zd5w%67RY%@rL$nQdBSn|r~xbc zVyFkTO1C_x+h@e*K}AaTO*E2Kxj^{wVoguQK>^L0&T52rO)ZO;_|#ryXlKJ;DS0N= zwsR3G$&l&adUyC5TM!0Ew-caafmZ}g*09Hn3-sNnIXGl1GxoLXC*wzm^{2IKgK47=!PRxn$ZYdiH$@UbHxbELt4 za8*QDkSNfkG4i`_6ULA)$j4PbnLu*+XtllBW?KOA%~qwCNI!yhwR6o$sPv*U#Q5% zsyB~JddE74MDm=s(3Qlghy`_FKAi95a0=Ld637_b12y1tq}Y#}G!koRZP@j3vLG?5|niDh!C(S&~++kQ;qDGb+pz$_z zY^0e5aoNPTQ_Bn3x;RO@j087CZa$?ipG-h%$0*bioB@9S4U}LQKC4jI)Ki0_O zdbfe=O@#Z;+@}_ z8#}eg#v_F*D^oJDi4YRPE2+PTQq5g%C^*YOjUWc z;ujfsF%q?&W-yXt%4KoP);s+=G<)j8P(y^X7 z)jTT}p0NyJEC5Tq>qn?DEl^D=QIBYd%wJzg0y#5JXht}NcFYeY&|zTp&Tc_lYFO@+ zMAqAlZX=0bv3P4kBmaIX%ztO7*E-JzfBXCl{c&$(>NJ1ki*?s9ItAg;p-X?D_*6SM zMXT4AnUU)SgW;=*g@p`h-w{W(;X;Gm$)r|dcHUPNqW&%#X<4Vukco@&5yJFiENrKf z)USWiND4jSpUa$}n{coLx`$)Hz?wsb1mj(Slv1&YWn5$!dBV$NbvX7a@hXsQg*?ww zB#eEoFf?l0&_H1A=B39FQ#SjyAr8iiYE;4>6LJUcRk0t#^bB z`1UN}B1w1Wk;Q+MM6Fox>$xu+i{l7T>C$k9G|dji(;qeVh4M6sTP?8koVbW%A0^8c z3Ec(7_fA8E^{v-1w5Mjbn4XDXY>Zwq)Yws+CyqrgSDRvqCixZ?JdWt)^o8d{#Es^* z;psXp-7!qTIDzvR?*u~=;@$)@wA`5lK`(=xZHl-4U^nm?X!~72NUh*a_wBBaS%&i$ z=w%Rc>s|hK*2wpqJIIgl$8e;??{PYC?VYQ>D4J#*@~m`xATq_!E>({uIw--MBp$f- z-KGht+9tq=oVDB{7EN~kzEGMLj>99{a;NZVdi%y7gRT@g{i=>rY&ejoRrA$EjvrFY zz0i%TRqffQnTyrFVwB6x*g-vkez^yl(4!=RVJ7Plw&&}g{#&20qI%}#WfY8D()Srs z%S^RUUf^jQm>eunAsty0)ORgKonEamGhw>&L4H`KOyhj&TaK zwe6Xw4nO3X8iBoW_HS!QS^jpd@Pw?Av2OBPBaV?y4GL_L^b4QU_}cH}{jk^<9^9Gb zkLFRyFS=QVTUHY2l_znCA1!#w}x;TVsC1NOQwevg_ z^Q4VO%5~Z&l5ZR#`rmB8!V~@sh{Vz#+GyM8Exm8nM=}Ekz++QH5bzY$bwiJK3N7)e zrR<{Ay%wqxPB-)KvKgM#>(0!s7M7LnIj`YSi5Gm*EQ|`dWz}@xnF`p=`B-I2%WKmO z!GrO+eXl|H2d`TYt(tg*5nV>!*-<&2qJ0vAw zAEjtEsom+T9c~|5*BG&6Y zM`?xWz-wZvy+y^D_r&Bt*)(oPb_6CQ^ZesbB9!Z&|cDiQ9P8 z>)WDfc7D@daIUGXeb>QVz!c`e61C{uW5XKR0hgvhVmmgpKM_F;R!A`Lc?_MI1j#-^xidm_gJe?@iT(xa+*Gf9C&nL^KS4MoeOLz zGwyab8H_bnxpg&zwv>T15q)FCa%KZSAAB)Pp-9~hytWIaYH@1cgG!hh{6F%S9^Tz7 zTJ8gZ8(XvLI>Be+m}y zP5}=7^x?I+vuJ|sw3-bAC}&m-{olwKd!(iVw&mE|MI%(ybRw)Mc*?h+>GAZ;4OE$E zh3cDt(rVFk;gI%zX{b9MsIYydr$7fU5QzLkp$XmO%@PP_kodK5)1!-2%kvF+1fkYE z!uddOSuZQ_w~w97btny;L?I+!HCSY+^hBqmiJRNj|ymBeSQJn*vx#!); zY;{{zFBa~2dn99X_P*P)$NBVmp#P;GR~6FacX6E>LXOAd`qd?j{swqK1v{wkPy(N1 zgfz)}aT5b#TUYEq0-1(6r#@2qba(hOW|ap+IV9gPg>LmY4f#Fv{W9HMiC_noLZ zId{iJHn!tWjtLQG_8vk{-mVU(udlc6nL`svN8m9H1IR7_`!2o)Iqv`A_N}E#`^)Br zH*=nr>LyO)oLCm~689I5mbFd207WAlHk-tUv#i5FtN@mLxyoR1q{+4IA3)x{zP$wz zYUrfB-7k@iWurT46m^@+p8{lQ{8qF8?#A++GpN#YfoQygyLGpjkcyb9$xzTvtj9LT z_)I+@DEAmZi!CfbLdJFq@uKc!n|rKv@+gE#2lX2DefMc#-2#7?oMMfnhU{IaHQmpi zvaB|co8S3D0BWdB_-US(fDJm$q}$wt6DCzO>bjtgpgU(I2q;Eopz`KX9mx)%c-hI6u zcNN5S&{(hPTa4kSJj&2VOccvN^fjQy&pvpd|BC%!(f!bEm5$3$v|h~a{3+k<K2;59{22;BFcyVK|)jgTlSTDg7UW^Xb#LL!4Jf9ZC^L)8-X=1Th&4 z@Ig*0XLOao%IK9tZ-cpNZCY*oODS; z_g~9T^af)_OVifx_|$iinbt2WA&ys!Cu$LMMUX5gKY=>jmzvAtlo>}G9bIqa&$&{s z1&9Mbir`^_#LLwxg&bzagXTTFyX>id`e`EMnme~9l);Sw(n>o3 zb#RtXG`8Y-TAh-}t~l?c!Matb4ol3R$onAphL<%l;BiH~Q4VXZ%eY|`*p`jx;ii4e zhCT^J+VwTY;(t3vZ2-be<2xy6aJ*waYF(ii*KuMPdvR&%uYTL~RfzLwrL;J%cJbVA z_a`+xaV2%^x3$LYf=+ybSoe@{JPM`Nvm-Gku2)B)*%u&1xFFc~E3atc$4ChPVSELBznK~;F z7Zf!M*oL~8A!kk&3ljRP;V-(XZ@W(0(N{zVz|IFcsqHyp^L%$T=8sJc^gPqwyz$`7 zFi}-&lv`FCc=WRA)_r$3MBp^%i9K=tAS@vTiK35opLB?D1HkONV>(ex#9e>Xo~*r z^G-}wMgbR))q_a_y_NzKlWh8D@~Ynq-AF%|2n1WV1Lu^Gy32;J2)Xp5j6DZ$;wd== z@dVz?;@SKV&{@QR02EN%)IN~yPuI1%aoH97&W~O!+b4wBZ^hG>#rNK%3U$c!jCMKy z6y+_+mPv&8RmMTTzxQHw1fX1Bo}9hiMcm5y8~5G*FoLWek@=s}07ymg#MDw5X4Vp^ zChF}mUnXS4-btVV{`yGpvjAolyv>#Jk`zQMWy*aYYq;qFpo-Ef^P8o)C9rdo1}(q^ zFYYmN7oun3y#H(%#-Twh6E|DKb^JC7K*tpt4lOm>JYH#r&B;0$bmEG1LfpHDeD`)sd4o8K#!u#UkG~s4NI7+pdeBkYrYX70H1waMpt;4Ch z6MXO4cv8r3cFhw4gD_|I8hdgS49lH%!g=t^AFLS)_WNQRP5wa@m(&_|D@q9MMIWpHK565|5`11}tl=eVn{nND<*3lmBr#6r{IqiA~OzI@W+^k@a(*jP>&a zoieu*NPjR&()qK0RTEl@phSlLpWNNw4=;hWE|L8ayZs{dpPUl9(M;|Dq8nR+Q{vB- z;_n9mq{Y&m>XZ9=$la}qu;b&^z5)#cWu)JE4Zybpc^feO>yu7&Nsb4?+b9M2;>B;84QvLJ7 z-oOeYH&AG;j-|C{WAGbEX3)R)&!+;VlKgYHoO!>2@Y}?O7(-#S8)|ns&PUXM;)CtY zUt`ga`1cj9VS0Q1AhIHE7tKDzBS866o&LvxMD^B5Y%>Ysdl!RF#F@f~!IQ>?Lne z7-5XEsFU#QKa!44{Cji|)j={Qn@|2g{u${p6J(zLVkTGln^0Z)e~>5zD6F`cgAA*3 z(UN__;^+U-4?6H=h@7i(O3{6UPgy3mQ1Uk|GXoj8Q|moz%|(ic>Lh6{Eet4E&0z= z09#86uV~$G!KjD7f}CIe&EE8$LmD5$$a3n+oI^_??Kl7HiNFie3?Np{TUq_9*Z;?S zLBtM8A{f(80c{}#@m+Rp>KBXd<0#L@TqP=r`-5Hf(K~Vx!1}Dqt-R6o&rMjlqHx+= zHHfFMhYt{y&;Dy<{+X1rHe=JLdaT@(z2QpGmt{5I&j$Y z;UD7?7@Zwy;(y2?k2i!@j}58ZxAnCnCp?e6`z*UXSwdpe>|d22GUlppy*NEKV($*u z)++!if@Z4;YVyTun(9*1bvYVyXfKS_W5jaNGCq$BNncGHnOcQaQiIEJGS%%jiRjZZ zP@5Twp)tGJlQJI2O&ToFp6*Vh{jvyjlbAlDf7 zrCVxtW1OdduXWyc3)~qwE9GAA=vf9iup+YZI2IOe9&!4QX39*-B3YdTyT0V|Zv7nE>XJp^#YT#u zTY@7up0A%$zx(40N@#ou5}+_8eWt5u*4QheVRrF@hbvn{H=15c{Y#nIuQ`?I#eYwT zKqU9KW@jtrJE}BIpnuNk$prLuFDp$-E?QgmdkNZ6SCm4tNw>c>Q9-28%DhN>C98Bcx@5xwn3_hGK z4{g5KQL)bdNl{CPV#{r}Dw)AsDyHQ)ur_V@L$wpA77n_RY$zr!X0-tP8#ue~f(vF} ztAkL;L*{%6(s^xF%vRepQe!(SYBDS+b2y$a*db;w z{o4FR#t_hViEUsYAxJbQPsB0V`k<9p%wsZ)ieEgFd&z zv9iu_bm5V29_S!qGomh}j0{PCq7P0ok`@oVcNAu}_^6*S-TtFKCYD-Z;A&pv_s?Dt zl*ZGAajm@|(25ah1!~nv@vNCV_!fg6OCFO%XED+OT96DSj)1;+ko7g`czMxTw~jkE z&`XWzRf{I7B1u&m9Hdz7@r+?F{Zq1Y5j$pxma^&Qm_be_;)|XAZ9Rx_fqD_Qu9;}@ zTf8*3$a>d%-R-ZtW*LsVL!=)DHj%(=SdaGigXop6w;uK@KZc{UYfbUm_oQ_g#tnd+ zVpbWONGRF`=UA#U@aVX4TX0U}XH|Fa_m@VwBHe(xRiJvR$ckYIMq__cPqLWfY1+j@ zNjW!1u2?y5yUoC5!#K}{d=MFf^0#l_=Kw+3z*5RZ@`~AD3;yJ&}A*V0DZjEH5%$Vzk$aGM|A~R?K)VKu(WxC~x zxYDnuC@fx+?7Pjev^slg0}4#bwiU^?sAfm}N4flT8sBf_nyo8!%c?Bx=zx|7x*VIw zQk~}5Q>A$U7Mn$t*sM6rX9R-s(x&il>Wc)}wue%Uo=EFI$s}m|JkI%AC3B-7lFN0$ zj;pU!J9KKg=Eaoq7!jnRiQqb$X$IZ>kZBi{X>@xp{diiw{pX0<(bZtvNez=Kc+Ln5 zBUvFW?Kqt+;*Tb{42!|eOMqG}%^CC%gHFvz0v*gRn3+)KR2A6|iev1q3b_F7sqTaM zlP7pLD1%gl)Rug*?N(N-c1*49+j^uz$)W{$=TOSdWX(BhwnHh+Oj9DolQ~}#K9LRp zbP2n1jlCg!ticCK+?lkPLZD*3jxCoywBxiL>sXWkOGIn(C8y+6iOrQFA~ zY3e*j+f)Y`U3Q6U{)C~OJ%p;otnvHJ;+cv)OHgY}{=&O!QY{HCq#n~!z?}>m!2ltTWy#!Ll}os;jh+bP~H>SuNgj z|Mk5xpszFWOX!oSmfbByYW>~~T-LnKW!(gcA-sNvbHZQ-%kB@6DW%pnV z)!(ke!}FKsvjSUut|zyWjD7ACW*jSJ=tJ9V&jlH?uoWP;t%k(&YfyEvNt4TT$ie6R zN}vDL8J{SSu2oKN^Xq4!q~c2z1W^fkq=pj0ToJ}{0lCj#40oeET|>nf$!pIF*-psY z-hOsx1ABCYCFcMV829r9+%*J5*4kTYTK*0%t=M!bB#$$D&O~_Z4^9Vl@4Y`9j~XNK)QZ`uq-Oj%di8$Xx;SHT-;rQ*NB^H2-CbCzqb>*-6|!Q) zeTu7RzpQtE90X_R{ZQ`H@0z_>6ZjDl&$(h$@C~{<6QNs7u42Jo>I*^WIPVCE*l#UQ zXSWe3=$=~x{)Tnk5KgE3Mm}@C=2j!)1exs;{_NhHy{?G&bbiGIIqalb@h>8Q^#6$_~%oT=YM+%6`6l*%-n;XbZaW+01R+ zKU@G*b4z60%tOzPdIr+ft@^LwA#!+Mj6!p^`{6v{(=(8gqo5l$unrv>bhp(jZxUo) ze)QM;JJjZf(1vO76utR*=b#;i`QEM8Y^KJ0nr$!hh(gO#l-F6*{PcXBt?B&is3pe$i!E#lC0b^==t&_A4(^l)F zHqqG{5SksoZ>s4y6TDACQ;gcJNwU?iZ(E8tL~CM)$72=^=AJuQu3xQaV#r!V)-F8n zx?k?flN=Ygg)8Wc`4xJlF2EtGgRtyHRMdF};ipCbaHbS4&)#&wF@or=K#v|%S`IP8 zg+7BM6-Pj|JQVvP^s{!lw)yrdlgfL}m-Y8pEBqCJt#1%TI0?kO?n~^^4*0VBZ6aDq z-IGPI@C2V#ZACvMS4|P%iM>1tP`y~}qFBb{M`W+NQWzpm(c@3iDMa=XU z0#Yr)dL5m?Ziftn*CiCXRkzk!DvD;DZmVIHk|z=AjO)^aCN}pO(D^VJVY(^tWxdJk z65$wiMSv6Y``LIBw`H{2)uvogDBAzS-d{&W*|lxJxKe_oA}Otav~+hUE#2J=Ass_V zgNO)7cS{Z3Eg&&83Q>5H6L$j}E#SoS)4Go8c#k+M8QpoBSpii$6@I#c0 zzDLi{%*}Vn=~^X|`s4F$?uz|pB!g`qo5((8Ad|!e@IGCM=OJKumBe&D)F}QU(!pKU z!o%DM>vGxCH8!L9%J753>i#)tDX4OBz$)M)0mTU|Qvm{h`%fk*JhX#+WhR&ycL+Nh zLxDw@}%9IUPY zyJKebxBaX?YJSmH1=GEh(Wm!BvngR}Ufd$4yzp&tP;8a^vB7VzC`fPU2pxZlIO)Ll zWOug3dbjt-P`h5Lu{p2jvn7k;bO&u9hW9%TjYYqBmQ-vjM;5y&@MwQ{Hs7Hqan#Z(~^U&S}>2S}s6<|7NQ z*qDqyN_eEtb0|G7?EXjE5ud;Q>Qn*#(Lf2 z?p@_;jG94v4!V@HkDD(;@L9O1MO&(qJo4dT|NICP+zxr zS4+f_jd7V5Z}2$=q24d{kBKI^?rvM?atsd;oow+CV(M+0FJUuP^@`n_CsJV>EZ1$t z5#1IDKB_@X2%o5t!95o7DL8#w9X>eqGj8|nB3m9 z5{MAe^%Vg9J9ZGgiY>_|)Mok-%4CQaaLmmCKh?|Ada0z%W|IfQk#ipHN%#UK%%5sG zgrpZs+-1I0;_}fXB4;)^uU~;wSvM+-{{175r%uxHCqs%)M*cA6EDIVxOstLe)Os?Z z8N48Xal40oU@0rE(2_xRD_N>AM64m|3(2RmGBB6_$~EyWjFPqi;#(7{LjY(}>1whi zHPmWrNrDSe7rc;3o1h}=xPH`|6>u`?L~1%C{?b`jghXFI6>$zd;u-c&uh5qC(7M8M z@i)=r^_p-%h+4-O2A({&8zR1}UHy3S6)yNb_$Wo9abBQ(o%&|G1?e`#xO`vnijVI& z5U*HNzhk1GXyTe86>fgYh&X+k$VvpY_r1aU1f%TY-Zi@nRa$4Mm3(czD?*;6=RCS@Zs5Bg zj^j?oNM)(@y9ob#;V8nUk&xp3C*(@X#QV~wDPz5~6$E3m^AH_kvRGCS8@7q1^My1} zPyikhGgi}SCX0#ToqVjd5y5A(z8N~~y0Fe^>Dq2+Iw@s;fA^d^Hb;=4sio7h57fVz zvtfCq8|3-!wNW{NgV&bDO5XYNqX5X4!07r;0@o`sKU}i#1A(qOuOrXT-37uWNd6_{PxDK+`g5H58AtHQ$Z(jR*7;?oR-#+gCq-=2>YItze~V zv_8@7v(w?eek)y1Z**iv`de{RJ@i2G_%yxRcD*OhPS(nUaF;jc{n)u~(=8c35eaX! z0m>h?bT8o|{V1AHSTPAZzw+v<^&igKpzm}jv3I>>b zXO@o?*bH4MPv;|npmNdG^<9tm_?NOqZjL9u0P=8;lQpb(nwU%6D8?-)m06I{hbwjR z{MsvPF9GS|LVsijG9lil15=!zs+fW)F|hJs zPW#zOhmxSCowPC3$AMA`8(pX*i8z*qOec+-jy;2Qfn&^I@Y0>-4wwJ2i@ZN$l^&;1 zA(fPO_25woT4jIYirF5}4iq=$+t7zc0^L{0yVx&V7+z}!N8=MenX;MoM;w!4b_Hz+ z{mx;35!*!6ft>l#ygsqjP`1HlwquuK)maeP?6P_daYKs3(}m=sdXU4gYiAmi&Bnp& zyWXSFM4tI{z^YC&hg2Q#TvMAi(``~;3U~FkOi+ox04A|Ueg$!7UM=Dq3%6>MU>Wd?N>yG0jONm=%YWDXkiHVvHSvbB(X>^ zWGcwkBfeWw2^H(q^S@V%az%A8Tl=QvIH0mWI4!(h7ocn_ujpYnYZ>+{stmQRy zr3ms)_3@Bi-6|i=sz2Z0jU20c(mh`Wu1d~4`^-^Juo=7IXvepT;aHKt0m4Un-q9E& z25%MJrW?h6t)0L-4tRE%E0|7rmVOCe2?IzfSpGEyu46f)20vQ43%omXWib``aL0OZ z4Kfg^EQV2o=Til~=SfvRz5iIa0eDXk6oyC?Y;m&(2>O~pD0ul8N@mh~gH#6p@Fd#w z%Da35?U*PBV|k{*t#+*Q112&1>&J^~ViAfP7DV9PSAX!D`) zMc)|qXuJzy)?JS+78FTiBVLuwiwN|X;YEQtwb$*lC>gSzGdOuAPdOei-DCsE12Nmr z$He0IucGtE=+0moC*3vDmXKnS$pHWt!mEQaAop&k2#{ zQVnb>s+7u#or5cYQday=XMvR1su@O^Q5O;?F)xTxoQ#%Be0xvXIbcsgOb!d2{^Xyw z2SPd)*hvC4yGHW~>?aj{0xI6GTq$yEf6x=kOBxu3Q_G1N2RAsa9cg5ib2n&OD<|&? zy2H@kk9tWiHNI*^GO?M+Kja%R{2EZ|gl)t0rcyFd{UB&*vicehs?2q5ySqtQ9(%~J z&>Z#g5pIcE4Ev|Y{QyM;!czDuju%c-4_~*hM5H3>i*gNi2nO6^f5DN~<@p%yxrpu# zx_+axhC=dHJl``fC0>U)v9AnjjPA+KfZx*<{_;!GOCgL7Fp5!;3Gq5?=vRswCtm37 zV8(H4N2IDgNuV2?s14F?cks$Gg6vH2S?HeObKcu^6|PX+dtbfog9FRKr?=zCG2*@N z*?HTXRth%hgW#XP7|`8?&n@UvZq^WfNdT)%&zXwbFB~;_Xm)54(g-v#8oW z<2>-++yAx~KFyepi>dGn3mgJOzT8Ga=bOs*Imgz$8DAlX&1-xch}h z9a=9+=ydn=qI>`PTpKhr~>5`8p3q&vEV0`7~OHTFlM2xn2P6;4ZR^;N!FUb+((a(^)L) z+Kn*e?nGzNK|r7bljv>B@Z<0HQ>cfTqe=%Gd$L4N&~cGQb8AaV6vyIsnY{KcEmSW8 z#Y?~Q+mnijY-Sy*wO`3BnT1c_?HD;~y^amxjB*hnhRULhc{I}v8Xe@WVQ5hu>E?r@ zK`f?OIQAq4NZ#eQ7P*E)ix1H4W~wx5tjFXOQn)l0h%~>o2Zeqi6HA!;qeA-c`jFI+ z5MJbmC`S!&u5Q#B;Pf@M3oHpC*Jlez$*CUMT~SHQ8T`PZULS)2*e~A5cQxLxO(sumPuJ{kdZKxMD*f1~ zAf-jYw>kEd7kqpPcO*m?o~8SIuHFBx_u)A7Z6b{wB0B?+bL1zMTGXj-ub##H4C`Z$ zo8*r;MVxv2pdh2LBct{Xj>bepd&44Sz9OPMx==|y8Rx@GgvAFNAT` zb57@)i!AtwFAUyCGIQ&=;33~#41sIht$`B=NnV)IO6z6mla#{E%iA)Gs1{z~(kxdI z0J=R?PlB@H75vN36QrSpkpwKCUJg)A8-#x<3R?-7wsF~Le&i2@9YE6`1dk&?sSL9o zo6O~hDnXtNh3l%n6%TsagXS?WlSe-vBKo~ucxN0uh5XhxM8(=f zdwp59YmZHLn}%$+ma@&tK-^8NN`&j{1eD(IT3@Z8?wMU8VZ%@Tv;fb#tx|bobA{8j z>`$+y!6*=>YKp1 zDqtpIR4$Ad32Rs*oRM$jK&whhWh~oXzE)>T^<3w?V0WAo*Iy~&VB8y#!yY3r( zHy@)pM{r}E55Om^C{D$1+LSoXl0|Tu4mM|LmN~YB>h0#~4y@`VU352k7dJ6>7FxAh zPBn2Es50xGY9c7vZVsx3Q#Me3yMHK=9rjx@c|C~st;=;+#Op1;II($o3SW(vx#+1J z-I;a@5E>qLeu0PUnBcH~PK z({;q{|5XWAQM z57$Q_*~|tM!=g30bWz)0&%!|M#Q=I&MuN@^;;lm5F z+Q}9jN*^&qE1L9O?`+%AbrTZ70C92MaI