FullClusterRestartIT.java 89 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970
  1. /*
  2. * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
  3. * or more contributor license agreements. Licensed under the "Elastic License
  4. * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
  5. * Public License v 1"; you may not use this file except in compliance with, at
  6. * your election, the "Elastic License 2.0", the "GNU Affero General Public
  7. * License v3.0 only", or the "Server Side Public License, v 1".
  8. */
  9. package org.elasticsearch.upgrades;
  10. import io.netty.handler.codec.http.HttpMethod;
  11. import com.carrotsearch.randomizedtesting.annotations.Name;
  12. import org.apache.http.util.EntityUtils;
  13. import org.elasticsearch.Build;
  14. import org.elasticsearch.client.Request;
  15. import org.elasticsearch.client.Response;
  16. import org.elasticsearch.client.ResponseException;
  17. import org.elasticsearch.client.RestClient;
  18. import org.elasticsearch.cluster.metadata.IndexMetadata;
  19. import org.elasticsearch.cluster.metadata.MetadataIndexStateService;
  20. import org.elasticsearch.common.Strings;
  21. import org.elasticsearch.common.settings.Settings;
  22. import org.elasticsearch.common.time.DateUtils;
  23. import org.elasticsearch.common.xcontent.support.XContentMapValues;
  24. import org.elasticsearch.core.Booleans;
  25. import org.elasticsearch.core.CheckedFunction;
  26. import org.elasticsearch.index.IndexSettings;
  27. import org.elasticsearch.index.IndexVersion;
  28. import org.elasticsearch.index.IndexVersions;
  29. import org.elasticsearch.index.mapper.DateFieldMapper;
  30. import org.elasticsearch.rest.RestStatus;
  31. import org.elasticsearch.rest.action.admin.indices.RestPutIndexTemplateAction;
  32. import org.elasticsearch.search.SearchFeatures;
  33. import org.elasticsearch.test.NotEqualMessageBuilder;
  34. import org.elasticsearch.test.XContentTestUtils;
  35. import org.elasticsearch.test.cluster.ElasticsearchCluster;
  36. import org.elasticsearch.test.cluster.FeatureFlag;
  37. import org.elasticsearch.test.cluster.local.LocalClusterConfigProvider;
  38. import org.elasticsearch.test.cluster.local.distribution.DistributionType;
  39. import org.elasticsearch.test.cluster.util.Version;
  40. import org.elasticsearch.test.rest.ESRestTestCase;
  41. import org.elasticsearch.test.rest.ObjectPath;
  42. import org.elasticsearch.xcontent.ToXContent;
  43. import org.elasticsearch.xcontent.XContentBuilder;
  44. import org.elasticsearch.xcontent.XContentType;
  45. import org.elasticsearch.xcontent.json.JsonXContent;
  46. import org.junit.Before;
  47. import org.junit.ClassRule;
  48. import org.junit.rules.RuleChain;
  49. import org.junit.rules.TemporaryFolder;
  50. import org.junit.rules.TestRule;
  51. import java.io.IOException;
  52. import java.util.ArrayList;
  53. import java.util.Base64;
  54. import java.util.Collection;
  55. import java.util.HashMap;
  56. import java.util.HashSet;
  57. import java.util.List;
  58. import java.util.Map;
  59. import java.util.Set;
  60. import java.util.concurrent.TimeUnit;
  61. import java.util.regex.Matcher;
  62. import java.util.regex.Pattern;
  63. import java.util.stream.IntStream;
  64. import static java.util.Collections.emptyMap;
  65. import static java.util.Collections.singletonList;
  66. import static java.util.Collections.singletonMap;
  67. import static java.util.stream.Collectors.toList;
  68. import static org.elasticsearch.cluster.metadata.IndexNameExpressionResolver.SYSTEM_INDEX_ENFORCEMENT_INDEX_VERSION;
  69. import static org.elasticsearch.cluster.routing.UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING;
  70. import static org.elasticsearch.cluster.routing.allocation.decider.MaxRetryAllocationDecider.SETTING_ALLOCATION_MAX_RETRY;
  71. import static org.elasticsearch.test.MapMatcher.assertMap;
  72. import static org.elasticsearch.test.MapMatcher.matchesMap;
  73. import static org.elasticsearch.xcontent.XContentFactory.jsonBuilder;
  74. import static org.hamcrest.Matchers.anyOf;
  75. import static org.hamcrest.Matchers.contains;
  76. import static org.hamcrest.Matchers.containsString;
  77. import static org.hamcrest.Matchers.equalTo;
  78. import static org.hamcrest.Matchers.greaterThan;
  79. import static org.hamcrest.Matchers.greaterThanOrEqualTo;
  80. import static org.hamcrest.Matchers.hasKey;
  81. import static org.hamcrest.Matchers.hasSize;
  82. import static org.hamcrest.Matchers.is;
  83. import static org.hamcrest.Matchers.notNullValue;
  84. import static org.hamcrest.Matchers.nullValue;
  85. import static org.hamcrest.Matchers.startsWith;
  86. /**
  87. * Tests to run before and after a full cluster restart. This is run twice,
  88. * one with {@code tests.is_old_cluster} set to {@code true} against a cluster
  89. * of an older version. The cluster is shutdown and a cluster of the new
  90. * version is started with the same data directories and then this is rerun
  91. * with {@code tests.is_old_cluster} set to {@code false}.
  92. */
  93. public class FullClusterRestartIT extends ParameterizedFullClusterRestartTestCase {
  94. private static TemporaryFolder repoDirectory = new TemporaryFolder();
  95. protected static LocalClusterConfigProvider clusterConfig = c -> {};
  96. private static ElasticsearchCluster cluster = buildCluster();
  97. private static ElasticsearchCluster buildCluster() {
  98. Version oldVersion = Version.fromString(OLD_CLUSTER_VERSION);
  99. var cluster = ElasticsearchCluster.local()
  100. .distribution(DistributionType.DEFAULT)
  101. .version(Version.fromString(OLD_CLUSTER_VERSION))
  102. .nodes(2)
  103. .setting("path.repo", () -> repoDirectory.getRoot().getPath())
  104. .setting("xpack.security.enabled", "false")
  105. // some tests rely on the translog not being flushed
  106. .setting("indices.memory.shard_inactive_time", "60m")
  107. .apply(() -> clusterConfig)
  108. .feature(FeatureFlag.TIME_SERIES_MODE)
  109. .feature(FeatureFlag.FAILURE_STORE_ENABLED);
  110. if (oldVersion.before(Version.fromString("9.1.0"))) {
  111. cluster.jvmArg("-da:org.elasticsearch.index.mapper.DocumentMapper");
  112. cluster.jvmArg("-da:org.elasticsearch.index.mapper.MapperService");
  113. }
  114. return cluster.build();
  115. }
  116. @ClassRule
  117. public static TestRule ruleChain = RuleChain.outerRule(repoDirectory).around(cluster);
  118. private String index;
  119. public FullClusterRestartIT(@Name("cluster") FullClusterRestartUpgradeStatus upgradeStatus) {
  120. super(upgradeStatus);
  121. }
  122. @Override
  123. protected ElasticsearchCluster getUpgradeCluster() {
  124. return cluster;
  125. }
  126. @Before
  127. public void setIndex() {
  128. index = getRootTestName();
  129. }
  130. public void testSearch() throws Exception {
  131. int count;
  132. if (isRunningAgainstOldCluster()) {
  133. final var createIndex = newXContentRequest(HttpMethod.PUT, "/" + index, (mappingsAndSettings, params) -> {
  134. mappingsAndSettings.startObject("settings");
  135. mappingsAndSettings.field("number_of_shards", 1);
  136. mappingsAndSettings.field("number_of_replicas", 0);
  137. mappingsAndSettings.endObject();
  138. mappingsAndSettings.startObject("mappings");
  139. mappingsAndSettings.startObject("properties");
  140. {
  141. mappingsAndSettings.startObject("string");
  142. mappingsAndSettings.field("type", "text");
  143. mappingsAndSettings.endObject();
  144. }
  145. {
  146. mappingsAndSettings.startObject("dots_in_field_names");
  147. mappingsAndSettings.field("type", "text");
  148. mappingsAndSettings.endObject();
  149. }
  150. {
  151. mappingsAndSettings.startObject("binary");
  152. mappingsAndSettings.field("type", "binary");
  153. mappingsAndSettings.field("store", "true");
  154. mappingsAndSettings.endObject();
  155. }
  156. mappingsAndSettings.endObject();
  157. mappingsAndSettings.endObject();
  158. return mappingsAndSettings;
  159. });
  160. client().performRequest(createIndex);
  161. count = randomIntBetween(2000, 3000);
  162. byte[] randomByteArray = new byte[16];
  163. random().nextBytes(randomByteArray);
  164. indexRandomDocuments(
  165. count,
  166. true,
  167. true,
  168. randomBoolean(),
  169. i -> JsonXContent.contentBuilder()
  170. .startObject()
  171. .field("string", randomAlphaOfLength(10))
  172. .field("int", randomInt(100))
  173. .field("float", randomFloat())
  174. // be sure to create a "proper" boolean (True, False) for the first document so that automapping is correct
  175. .field("bool", i > 0 && randomBoolean())
  176. .field("field.with.dots", randomAlphaOfLength(10))
  177. .field("binary", Base64.getEncoder().encodeToString(randomByteArray))
  178. .endObject()
  179. );
  180. refreshAllIndices();
  181. } else {
  182. count = countOfIndexedRandomDocuments();
  183. }
  184. ensureGreenLongWait(index);
  185. assertBasicSearchWorks(count);
  186. assertAllSearchWorks(count);
  187. assertBasicAggregationWorks();
  188. assertRealtimeGetWorks();
  189. assertStoredBinaryFields(count);
  190. }
  191. public void testNewReplicas() throws Exception {
  192. if (isRunningAgainstOldCluster()) {
  193. final var createIndex = newXContentRequest(HttpMethod.PUT, "/" + index, (mappingsAndSettings, params) -> {
  194. mappingsAndSettings.startObject("settings");
  195. mappingsAndSettings.field("number_of_shards", 1);
  196. mappingsAndSettings.field("number_of_replicas", 0);
  197. mappingsAndSettings.endObject();
  198. mappingsAndSettings.startObject("mappings");
  199. mappingsAndSettings.startObject("properties");
  200. {
  201. mappingsAndSettings.startObject("field");
  202. mappingsAndSettings.field("type", "text");
  203. mappingsAndSettings.endObject();
  204. }
  205. mappingsAndSettings.endObject();
  206. mappingsAndSettings.endObject();
  207. return mappingsAndSettings;
  208. });
  209. client().performRequest(createIndex);
  210. int numDocs = randomIntBetween(2000, 3000);
  211. indexRandomDocuments(
  212. numDocs,
  213. true,
  214. false,
  215. randomBoolean(),
  216. i -> JsonXContent.contentBuilder().startObject().field("field", "value").endObject()
  217. );
  218. logger.info("Refreshing [{}]", index);
  219. client().performRequest(new Request("POST", "/" + index + "/_refresh"));
  220. } else {
  221. // The test runs with two nodes so this should still go green.
  222. final int numReplicas = 1;
  223. final long startTime = System.currentTimeMillis();
  224. logger.debug("--> creating [{}] replicas for index [{}]", numReplicas, index);
  225. Request setNumberOfReplicas = newXContentRequest(
  226. HttpMethod.PUT,
  227. "/" + index + "/_settings",
  228. (builder, params) -> builder.startObject("index").field("number_of_replicas", numReplicas).endObject()
  229. );
  230. client().performRequest(setNumberOfReplicas);
  231. ensureGreenLongWait(index);
  232. logger.debug("--> index [{}] is green, took [{}] ms", index, (System.currentTimeMillis() - startTime));
  233. Map<String, Object> recoverRsp = entityAsMap(client().performRequest(new Request("GET", "/" + index + "/_recovery")));
  234. logger.debug("--> recovery status:\n{}", recoverRsp);
  235. Set<Integer> counts = new HashSet<>();
  236. for (String node : dataNodes(index, client())) {
  237. Request search = new Request("GET", "/" + index + "/_search");
  238. search.addParameter("preference", "_only_nodes:" + node);
  239. Map<String, Object> responseBody = entityAsMap(client().performRequest(search));
  240. assertNoFailures(responseBody);
  241. int hits = extractTotalHits(responseBody);
  242. counts.add(hits);
  243. }
  244. assertEquals("All nodes should have a consistent number of documents", 1, counts.size());
  245. }
  246. }
  247. public void testSearchTimeSeriesMode() throws Exception {
  248. assumeTrue("indexing time series indices changed in 8.2.0", oldClusterHasFeature("gte_v8.2.0"));
  249. int numDocs;
  250. if (isRunningAgainstOldCluster()) {
  251. numDocs = createTimeSeriesModeIndex(1);
  252. } else {
  253. numDocs = countOfIndexedRandomDocuments();
  254. }
  255. assertCountAll(numDocs);
  256. Request request = newXContentRequest(HttpMethod.GET, "/" + index + "/_search", (body, params) -> {
  257. body.field("size", 0);
  258. body.startObject("aggs").startObject("check").startObject("scripted_metric");
  259. body.field("init_script", "state.timeSeries = new HashSet()");
  260. body.field("map_script", "state.timeSeries.add(doc['dim'].value)");
  261. body.field("combine_script", "return state.timeSeries");
  262. body.field("reduce_script", """
  263. Set timeSeries = new TreeSet();
  264. for (s in states) {
  265. for (ts in s) {
  266. boolean newTs = timeSeries.add(ts);
  267. if (false == newTs) {
  268. throw new IllegalArgumentException(ts + ' appeared in two shards');
  269. }
  270. }
  271. }
  272. return timeSeries;""");
  273. body.endObject().endObject().endObject();
  274. return body;
  275. });
  276. Map<String, Object> response = entityAsMap(client().performRequest(request));
  277. assertMap(
  278. response,
  279. matchesMap().extraOk()
  280. .entry("hits", matchesMap().extraOk().entry("total", Map.of("value", numDocs, "relation", "eq")))
  281. .entry("aggregations", Map.of("check", Map.of("value", IntStream.range(0, 10).mapToObj(i -> "dim" + i).collect(toList()))))
  282. );
  283. }
  284. public void testNewReplicasTimeSeriesMode() throws Exception {
  285. assumeTrue("indexing time series indices changed in 8.2.0", oldClusterHasFeature("gte_v8.2.0"));
  286. if (isRunningAgainstOldCluster()) {
  287. createTimeSeriesModeIndex(0);
  288. } else {
  289. // The test runs with two nodes so this should still go green.
  290. final int numReplicas = 1;
  291. final long startTime = System.currentTimeMillis();
  292. logger.debug("--> creating [{}] replicas for index [{}]", numReplicas, index);
  293. Request setNumberOfReplicas = newXContentRequest(
  294. HttpMethod.PUT,
  295. "/" + index + "/_settings",
  296. (builder, params) -> builder.startObject("index").field("number_of_replicas", numReplicas).endObject()
  297. );
  298. client().performRequest(setNumberOfReplicas);
  299. ensureGreenLongWait(index);
  300. logger.debug("--> index [{}] is green, took [{}] ms", index, (System.currentTimeMillis() - startTime));
  301. Map<String, Object> recoverRsp = entityAsMap(client().performRequest(new Request("GET", "/" + index + "/_recovery")));
  302. logger.debug("--> recovery status:\n{}", recoverRsp);
  303. Set<Integer> counts = new HashSet<>();
  304. for (String node : dataNodes(index, client())) {
  305. Request search = new Request("GET", "/" + index + "/_search");
  306. search.addParameter("preference", "_only_nodes:" + node);
  307. Map<String, Object> responseBody = entityAsMap(client().performRequest(search));
  308. assertNoFailures(responseBody);
  309. int hits = extractTotalHits(responseBody);
  310. counts.add(hits);
  311. }
  312. assertEquals("All nodes should have a consistent number of documents", 1, counts.size());
  313. }
  314. }
  315. private int createTimeSeriesModeIndex(int replicas) throws IOException {
  316. final var createIndex = newXContentRequest(HttpMethod.PUT, "/" + index, (mappingsAndSettings, params) -> {
  317. mappingsAndSettings.startObject("settings");
  318. mappingsAndSettings.field("number_of_shards", 1);
  319. mappingsAndSettings.field("number_of_replicas", replicas);
  320. mappingsAndSettings.field("mode", "time_series");
  321. mappingsAndSettings.field("routing_path", "dim");
  322. mappingsAndSettings.field("time_series.start_time", 1L);
  323. mappingsAndSettings.field("time_series.end_time", DateUtils.MAX_MILLIS_BEFORE_9999 - 1);
  324. mappingsAndSettings.endObject();
  325. mappingsAndSettings.startObject("mappings");
  326. mappingsAndSettings.startObject("properties");
  327. {
  328. mappingsAndSettings.startObject("@timestamp").field("type", "date").endObject();
  329. mappingsAndSettings.startObject("dim").field("type", "keyword").field("time_series_dimension", true).endObject();
  330. }
  331. mappingsAndSettings.endObject();
  332. mappingsAndSettings.endObject();
  333. return mappingsAndSettings;
  334. });
  335. client().performRequest(createIndex);
  336. int numDocs = randomIntBetween(2000, 3000);
  337. long basetime = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.parseMillis("2021-01-01T00:00:00Z");
  338. indexRandomDocuments(
  339. numDocs,
  340. true,
  341. true,
  342. false,
  343. i -> JsonXContent.contentBuilder()
  344. .startObject()
  345. .field("@timestamp", basetime + TimeUnit.MINUTES.toMillis(i))
  346. .field("dim", "dim" + (i % 10))
  347. .endObject()
  348. );
  349. logger.info("Refreshing [{}]", index);
  350. client().performRequest(new Request("POST", "/" + index + "/_refresh"));
  351. return numDocs;
  352. }
  353. public void testClusterState() throws Exception {
  354. if (isRunningAgainstOldCluster()) {
  355. final Request createTemplate = newXContentRequest(HttpMethod.PUT, "/_template/template_1", (mappingsAndSettings, params) -> {
  356. mappingsAndSettings.field("index_patterns", index);
  357. mappingsAndSettings.field("order", "1000");
  358. mappingsAndSettings.startObject("settings");
  359. mappingsAndSettings.field("number_of_shards", 1);
  360. mappingsAndSettings.field("number_of_replicas", 0);
  361. mappingsAndSettings.endObject();
  362. return mappingsAndSettings;
  363. });
  364. createTemplate.setOptions(expectWarnings(RestPutIndexTemplateAction.DEPRECATION_WARNING));
  365. client().performRequest(createTemplate);
  366. client().performRequest(new Request("PUT", "/" + index));
  367. }
  368. // verifying if we can still read some properties from cluster state api:
  369. Map<String, Object> clusterState = entityAsMap(client().performRequest(new Request("GET", "/_cluster/state")));
  370. // Check some global properties:
  371. String numberOfShards = (String) XContentMapValues.extractValue(
  372. "metadata.templates.template_1.settings.index.number_of_shards",
  373. clusterState
  374. );
  375. assertEquals("1", numberOfShards);
  376. String numberOfReplicas = (String) XContentMapValues.extractValue(
  377. "metadata.templates.template_1.settings.index.number_of_replicas",
  378. clusterState
  379. );
  380. assertEquals("0", numberOfReplicas);
  381. // Check some index properties:
  382. numberOfShards = (String) XContentMapValues.extractValue(
  383. "metadata.indices." + index + ".settings.index.number_of_shards",
  384. clusterState
  385. );
  386. assertEquals("1", numberOfShards);
  387. numberOfReplicas = (String) XContentMapValues.extractValue(
  388. "metadata.indices." + index + ".settings.index.number_of_replicas",
  389. clusterState
  390. );
  391. assertEquals("0", numberOfReplicas);
  392. IndexVersion version = IndexVersion.fromId(
  393. Integer.valueOf(
  394. (String) XContentMapValues.extractValue("metadata.indices." + index + ".settings.index.version.created", clusterState)
  395. )
  396. );
  397. assertEquals(getOldClusterIndexVersion(), version);
  398. }
  399. public void testShrink() throws IOException {
  400. String shrunkenIndex = index + "_shrunk";
  401. int numDocs;
  402. if (isRunningAgainstOldCluster()) {
  403. final var createIndex = newXContentRequest(HttpMethod.PUT, "/" + index, (mappingsAndSettings, params) -> {
  404. mappingsAndSettings.startObject("mappings");
  405. {
  406. mappingsAndSettings.startObject("properties");
  407. {
  408. mappingsAndSettings.startObject("field");
  409. {
  410. mappingsAndSettings.field("type", "text");
  411. }
  412. mappingsAndSettings.endObject();
  413. }
  414. mappingsAndSettings.endObject();
  415. }
  416. mappingsAndSettings.endObject();
  417. mappingsAndSettings.startObject("settings");
  418. {
  419. mappingsAndSettings.field("index.number_of_shards", 5);
  420. }
  421. mappingsAndSettings.endObject();
  422. return mappingsAndSettings;
  423. });
  424. client().performRequest(createIndex);
  425. numDocs = randomIntBetween(512, 1024);
  426. indexRandomDocuments(
  427. numDocs,
  428. true,
  429. true,
  430. randomBoolean(),
  431. i -> JsonXContent.contentBuilder().startObject().field("field", "value").endObject()
  432. );
  433. ensureGreen(index); // wait for source index to be available on both nodes before starting shrink
  434. client().performRequest(
  435. newXContentRequest(
  436. HttpMethod.PUT,
  437. "/" + index + "/_settings",
  438. (builder, params) -> builder.startObject("settings").field("index.blocks.write", true).endObject()
  439. )
  440. );
  441. client().performRequest(
  442. newXContentRequest(
  443. HttpMethod.PUT,
  444. "/" + index + "/_shrink/" + shrunkenIndex,
  445. (builder, params) -> builder.startObject("settings").field("index.number_of_shards", 1).endObject()
  446. )
  447. );
  448. refreshAllIndices();
  449. } else {
  450. numDocs = countOfIndexedRandomDocuments();
  451. }
  452. Map<?, ?> response = entityAsMap(client().performRequest(new Request("GET", "/" + index + "/_search")));
  453. assertNoFailures(response);
  454. int totalShards = (int) XContentMapValues.extractValue("_shards.total", response);
  455. assertThat(totalShards, greaterThan(1));
  456. int successfulShards = (int) XContentMapValues.extractValue("_shards.successful", response);
  457. assertEquals(totalShards, successfulShards);
  458. int totalHits = extractTotalHits(response);
  459. assertEquals(numDocs, totalHits);
  460. response = entityAsMap(client().performRequest(new Request("GET", "/" + shrunkenIndex + "/_search")));
  461. assertNoFailures(response);
  462. totalShards = (int) XContentMapValues.extractValue("_shards.total", response);
  463. assertEquals(1, totalShards);
  464. successfulShards = (int) XContentMapValues.extractValue("_shards.successful", response);
  465. assertEquals(1, successfulShards);
  466. totalHits = extractTotalHits(response);
  467. assertEquals(numDocs, totalHits);
  468. }
  469. public void testShrinkAfterUpgrade() throws IOException {
  470. String shrunkenIndex = index + "_shrunk";
  471. int numDocs;
  472. if (isRunningAgainstOldCluster()) {
  473. final var createIndex = newXContentRequest(HttpMethod.PUT, "/" + index, (mappingsAndSettings, params) -> {
  474. mappingsAndSettings.startObject("mappings");
  475. {
  476. mappingsAndSettings.startObject("properties");
  477. {
  478. mappingsAndSettings.startObject("field");
  479. {
  480. mappingsAndSettings.field("type", "text");
  481. }
  482. mappingsAndSettings.endObject();
  483. }
  484. mappingsAndSettings.endObject();
  485. }
  486. mappingsAndSettings.endObject();
  487. // the default number of shards is now one so we have to set the number of shards to be more than one explicitly
  488. mappingsAndSettings.startObject("settings");
  489. mappingsAndSettings.field("index.number_of_shards", 5);
  490. mappingsAndSettings.endObject();
  491. return mappingsAndSettings;
  492. });
  493. client().performRequest(createIndex);
  494. numDocs = randomIntBetween(512, 1024);
  495. indexRandomDocuments(
  496. numDocs,
  497. true,
  498. true,
  499. randomBoolean(),
  500. i -> JsonXContent.contentBuilder().startObject().field("field", "value").endObject()
  501. );
  502. } else {
  503. ensureGreen(index); // wait for source index to be available on both nodes before starting shrink
  504. client().performRequest(
  505. newXContentRequest(
  506. HttpMethod.PUT,
  507. "/" + index + "/_settings",
  508. (builder, params) -> builder.startObject("settings").field("index.blocks.write", true).endObject()
  509. )
  510. );
  511. client().performRequest(
  512. newXContentRequest(
  513. HttpMethod.PUT,
  514. "/" + index + "/_shrink/" + shrunkenIndex,
  515. (builder, params) -> builder.startObject("settings").field("index.number_of_shards", 1).endObject()
  516. )
  517. );
  518. numDocs = countOfIndexedRandomDocuments();
  519. }
  520. refreshAllIndices();
  521. Map<?, ?> response = entityAsMap(client().performRequest(new Request("GET", "/" + index + "/_search")));
  522. assertNoFailures(response);
  523. int totalShards = (int) XContentMapValues.extractValue("_shards.total", response);
  524. assertThat(totalShards, greaterThan(1));
  525. int successfulShards = (int) XContentMapValues.extractValue("_shards.successful", response);
  526. assertEquals(totalShards, successfulShards);
  527. int totalHits = extractTotalHits(response);
  528. assertEquals(numDocs, totalHits);
  529. if (isRunningAgainstOldCluster() == false) {
  530. response = entityAsMap(client().performRequest(new Request("GET", "/" + shrunkenIndex + "/_search")));
  531. assertNoFailures(response);
  532. totalShards = (int) XContentMapValues.extractValue("_shards.total", response);
  533. assertEquals(1, totalShards);
  534. successfulShards = (int) XContentMapValues.extractValue("_shards.successful", response);
  535. assertEquals(1, successfulShards);
  536. totalHits = extractTotalHits(response);
  537. assertEquals(numDocs, totalHits);
  538. }
  539. }
  540. /**
  541. * Test upgrading after a rollover. Specifically:
  542. * <ol>
  543. * <li>Create an index with a write alias
  544. * <li>Write some documents to the write alias
  545. * <li>Roll over the index
  546. * <li>Make sure the document count is correct
  547. * <li>Upgrade
  548. * <li>Write some more documents to the write alias
  549. * <li>Make sure the document count is correct
  550. * </ol>
  551. */
  552. public void testRollover() throws Exception {
  553. if (isRunningAgainstOldCluster()) {
  554. client().performRequest(
  555. newXContentRequest(
  556. HttpMethod.PUT,
  557. "/" + index + "-000001",
  558. (builder, params) -> builder.startObject("aliases").startObject(index + "_write").endObject().endObject()
  559. )
  560. );
  561. }
  562. int bulkCount = 10;
  563. String bulk = """
  564. {"index":{}}
  565. {"test":"test"}
  566. """.repeat(bulkCount);
  567. Request bulkRequest = new Request("POST", "/" + index + "_write/_bulk");
  568. bulkRequest.setJsonEntity(bulk);
  569. bulkRequest.addParameter("refresh", "");
  570. assertThat(EntityUtils.toString(client().performRequest(bulkRequest).getEntity()), containsString("\"errors\":false"));
  571. if (isRunningAgainstOldCluster()) {
  572. client().performRequest(
  573. newXContentRequest(
  574. HttpMethod.POST,
  575. "/" + index + "_write/_rollover",
  576. (builder, params) -> builder.startObject("conditions").field("max_docs", 5).endObject()
  577. )
  578. );
  579. assertBusy(() -> {
  580. Request catIndices = new Request("GET", "/_cat/indices?v&error_trace");
  581. // the cat APIs can sometimes 404, erroneously
  582. // see https://github.com/elastic/elasticsearch/issues/104371
  583. setIgnoredErrorResponseCodes(catIndices, RestStatus.NOT_FOUND);
  584. Response response = assertOK(client().performRequest(catIndices));
  585. assertThat(EntityUtils.toString(response.getEntity()), containsString("testrollover-000002"));
  586. });
  587. }
  588. Request countRequest = new Request("POST", "/" + index + "-*/_search");
  589. countRequest.addParameter("size", "0");
  590. Map<String, Object> count = entityAsMap(client().performRequest(countRequest));
  591. assertNoFailures(count);
  592. int expectedCount = bulkCount + (isRunningAgainstOldCluster() ? 0 : bulkCount);
  593. assertEquals(expectedCount, extractTotalHits(count));
  594. }
  595. void assertCountAll(int count) throws IOException {
  596. Map<String, Object> response = entityAsMap(client().performRequest(new Request("GET", "/" + index + "/_search")));
  597. assertNoFailures(response);
  598. int numDocs = extractTotalHits(response);
  599. logger.info("Found {} in old index", numDocs);
  600. assertEquals(count, numDocs);
  601. }
  602. void assertBasicSearchWorks(int count) throws IOException {
  603. logger.info("--> testing basic search");
  604. {
  605. assertCountAll(count);
  606. }
  607. logger.info("--> testing basic search with sort");
  608. {
  609. Map<String, Object> response = entityAsMap(
  610. client().performRequest(
  611. newXContentRequest(
  612. HttpMethod.GET,
  613. "/" + index + "/_search",
  614. (builder, params) -> builder.startArray("sort").startObject().field("int", "asc").endObject().endArray()
  615. )
  616. )
  617. );
  618. assertNoFailures(response);
  619. assertTotalHits(count, response);
  620. }
  621. logger.info("--> testing exists filter");
  622. {
  623. Map<String, Object> response = entityAsMap(
  624. client().performRequest(
  625. newXContentRequest(
  626. HttpMethod.GET,
  627. "/" + index + "/_search",
  628. (builder, params) -> builder.startObject("query")
  629. .startObject("exists")
  630. .field("field", "string")
  631. .endObject()
  632. .endObject()
  633. )
  634. )
  635. );
  636. assertNoFailures(response);
  637. assertTotalHits(count, response);
  638. }
  639. logger.info("--> testing field with dots in the name");
  640. {
  641. Map<String, Object> response = entityAsMap(
  642. client().performRequest(
  643. newXContentRequest(
  644. HttpMethod.GET,
  645. "/" + index + "/_search",
  646. (builder, params) -> builder.startObject("query")
  647. .startObject("exists")
  648. .field("field", "field.with.dots")
  649. .endObject()
  650. .endObject()
  651. )
  652. )
  653. );
  654. assertNoFailures(response);
  655. assertTotalHits(count, response);
  656. }
  657. }
  658. void assertAllSearchWorks(int count) throws IOException {
  659. logger.info("--> testing _all search");
  660. Map<String, Object> response = entityAsMap(client().performRequest(new Request("GET", "/" + index + "/_search")));
  661. assertNoFailures(response);
  662. assertTotalHits(count, response);
  663. Map<?, ?> bestHit = (Map<?, ?>) ((List<?>) (XContentMapValues.extractValue("hits.hits", response))).get(0);
  664. // Make sure there are payloads and they are taken into account for the score
  665. // the 'string' field has a boost of 4 in the mappings so it should get a payload boost
  666. String stringValue = (String) XContentMapValues.extractValue("_source.string", bestHit);
  667. assertNotNull(stringValue);
  668. String id = (String) bestHit.get("_id");
  669. String explanation = toStr(
  670. client().performRequest(
  671. newXContentRequest(
  672. HttpMethod.GET,
  673. "/" + index + "/_explain/" + id,
  674. (builder, params) -> builder.startObject("query").startObject("match_all").endObject().endObject()
  675. )
  676. )
  677. );
  678. assertFalse("Could not find payload boost in explanation\n" + explanation, explanation.contains("payloadBoost"));
  679. // Make sure the query can run on the whole index
  680. Request searchRequest = newXContentRequest(
  681. HttpMethod.GET,
  682. "/" + index + "/_search",
  683. (builder, params) -> builder.startObject("query").startObject("match_all").endObject().endObject()
  684. );
  685. searchRequest.addParameter("explain", "true");
  686. Map<?, ?> matchAllResponse = entityAsMap(client().performRequest(searchRequest));
  687. assertNoFailures(matchAllResponse);
  688. assertTotalHits(count, matchAllResponse);
  689. }
  690. void assertBasicAggregationWorks() throws IOException {
  691. // histogram on a long
  692. Map<?, ?> longHistogram = entityAsMap(
  693. client().performRequest(
  694. newXContentRequest(
  695. HttpMethod.GET,
  696. "/" + index + "/_search",
  697. (builder, params) -> builder.startObject("aggs")
  698. .startObject("histo")
  699. .startObject("histogram")
  700. .field("field", "int")
  701. .field("interval", 10)
  702. .endObject()
  703. .endObject()
  704. .endObject()
  705. )
  706. )
  707. );
  708. assertNoFailures(longHistogram);
  709. List<?> histoBuckets = (List<?>) XContentMapValues.extractValue("aggregations.histo.buckets", longHistogram);
  710. int histoCount = 0;
  711. for (Object entry : histoBuckets) {
  712. Map<?, ?> bucket = (Map<?, ?>) entry;
  713. histoCount += (Integer) bucket.get("doc_count");
  714. }
  715. assertTotalHits(histoCount, longHistogram);
  716. // terms on a boolean
  717. Map<?, ?> boolTerms = entityAsMap(
  718. client().performRequest(
  719. newXContentRequest(
  720. HttpMethod.GET,
  721. "/" + index + "/_search",
  722. (builder, params) -> builder.startObject("aggs")
  723. .startObject("bool_terms")
  724. .startObject("terms")
  725. .field("field", "bool")
  726. .endObject()
  727. .endObject()
  728. .endObject()
  729. )
  730. )
  731. );
  732. List<?> termsBuckets = (List<?>) XContentMapValues.extractValue("aggregations.bool_terms.buckets", boolTerms);
  733. int termsCount = 0;
  734. for (Object entry : termsBuckets) {
  735. Map<?, ?> bucket = (Map<?, ?>) entry;
  736. termsCount += (Integer) bucket.get("doc_count");
  737. }
  738. assertTotalHits(termsCount, boolTerms);
  739. }
  740. void assertRealtimeGetWorks() throws IOException {
  741. client().performRequest(
  742. newXContentRequest(
  743. HttpMethod.PUT,
  744. "/" + index + "/_settings",
  745. (builder, params) -> builder.startObject("index").field("refresh_interval", -1).endObject()
  746. )
  747. );
  748. Map<?, ?> searchResponse = entityAsMap(
  749. client().performRequest(
  750. newXContentRequest(
  751. HttpMethod.GET,
  752. "/" + index + "/_search",
  753. (builder, params) -> builder.startObject("query").startObject("match_all").endObject().endObject()
  754. )
  755. )
  756. );
  757. Map<?, ?> hit = (Map<?, ?>) ((List<?>) (XContentMapValues.extractValue("hits.hits", searchResponse))).get(0);
  758. String docId = (String) hit.get("_id");
  759. client().performRequest(
  760. newXContentRequest(
  761. HttpMethod.POST,
  762. "/" + index + "/_update/" + docId,
  763. (builder, params) -> builder.startObject("doc").field("foo", "bar").endObject()
  764. )
  765. );
  766. Request getRequest = new Request("GET", "/" + index + "/_doc/" + docId);
  767. Map<String, Object> getRsp = entityAsMap(client().performRequest(getRequest));
  768. Map<?, ?> source = (Map<?, ?>) getRsp.get("_source");
  769. assertTrue("doc does not contain 'foo' key: " + source, source.containsKey("foo"));
  770. client().performRequest(
  771. newXContentRequest(
  772. HttpMethod.PUT,
  773. "/" + index + "/_settings",
  774. (builder, params) -> builder.startObject("index").field("refresh_interval", "1s").endObject()
  775. )
  776. );
  777. }
  778. void assertStoredBinaryFields(int count) throws Exception {
  779. final var restResponse = client().performRequest(
  780. newXContentRequest(
  781. HttpMethod.GET,
  782. "/" + index + "/_search",
  783. (builder, params) -> builder.startObject("query")
  784. .startObject("match_all")
  785. .endObject()
  786. .endObject()
  787. .field("size", 100)
  788. .field("stored_fields", "binary")
  789. )
  790. );
  791. Map<String, Object> rsp = entityAsMap(restResponse);
  792. assertTotalHits(count, rsp);
  793. List<?> hits = (List<?>) XContentMapValues.extractValue("hits.hits", rsp);
  794. assertEquals(100, hits.size());
  795. for (Object hit : hits) {
  796. Map<?, ?> hitRsp = (Map<?, ?>) hit;
  797. List<?> values = (List<?>) XContentMapValues.extractValue("fields.binary", hitRsp);
  798. assertEquals(1, values.size());
  799. byte[] binaryValue = switch (XContentType.fromMediaType(restResponse.getEntity().getContentType().getValue())) {
  800. case JSON, VND_JSON -> Base64.getDecoder().decode((String) values.get(0));
  801. case SMILE, CBOR, YAML, VND_SMILE, VND_CBOR, VND_YAML -> (byte[]) values.get(0);
  802. };
  803. assertEquals("Unexpected binary length [" + Base64.getEncoder().encodeToString(binaryValue) + "]", 16, binaryValue.length);
  804. }
  805. }
  806. static String toStr(Response response) throws IOException {
  807. return EntityUtils.toString(response.getEntity());
  808. }
  809. static void assertNoFailures(Map<?, ?> response) {
  810. int failed = (int) XContentMapValues.extractValue("_shards.failed", response);
  811. assertEquals(0, failed);
  812. }
  813. void assertTotalHits(int expectedTotalHits, Map<?, ?> response) {
  814. int actualTotalHits = extractTotalHits(response);
  815. assertEquals(response.toString(), expectedTotalHits, actualTotalHits);
  816. }
  817. static int extractTotalHits(Map<?, ?> response) {
  818. return (Integer) XContentMapValues.extractValue("hits.total.value", response);
  819. }
  820. /**
  821. * Tests that a single document survives. Super basic smoke test.
  822. */
  823. public void testSingleDoc() throws IOException {
  824. String docLocation = "/" + index + "/_doc/1";
  825. String doc = "{\"test\": \"test\"}";
  826. if (isRunningAgainstOldCluster()) {
  827. Request createDoc = new Request("PUT", docLocation);
  828. createDoc.setJsonEntity(doc);
  829. client().performRequest(createDoc);
  830. }
  831. Request request = new Request("GET", docLocation);
  832. assertThat(toStr(client().performRequest(request)), containsString(doc));
  833. }
  834. /**
  835. * Tests that a single empty shard index is correctly recovered. Empty shards are often an edge case.
  836. */
  837. public void testEmptyShard() throws IOException {
  838. final String indexName = "test_empty_shard";
  839. if (isRunningAgainstOldCluster()) {
  840. Settings.Builder settings = indexSettings(1, 1)
  841. // if the node with the replica is the first to be restarted, while a replica is still recovering
  842. // then delayed allocation will kick in. When the node comes back, the master will search for a copy
  843. // but the recovering copy will be seen as invalid and the cluster health won't return to GREEN
  844. // before timing out
  845. .put(INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING.getKey(), "100ms")
  846. .put(SETTING_ALLOCATION_MAX_RETRY.getKey(), "0"); // fail faster
  847. createIndex(indexName, settings.build());
  848. }
  849. ensureGreen(indexName);
  850. }
  851. /**
  852. * Tests recovery of an index.
  853. */
  854. public void testRecovery() throws Exception {
  855. int count;
  856. if (isRunningAgainstOldCluster()) {
  857. count = between(200, 300);
  858. Settings.Builder settings = Settings.builder();
  859. if (minimumIndexVersion().before(IndexVersions.V_8_0_0) && randomBoolean()) {
  860. settings.put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean());
  861. }
  862. final String mappings = randomBoolean() ? "\"_source\": { \"enabled\": false}" : null;
  863. createIndex(index, settings.build(), mappings);
  864. indexRandomDocuments(count, true, true, true, i -> jsonBuilder().startObject().field("field", "value").endObject());
  865. // make sure all recoveries are done
  866. ensureGreen(index);
  867. // Force flush so we're sure that all translog are committed
  868. Request flushRequest = new Request("POST", "/" + index + "/_flush");
  869. flushRequest.addParameter("force", "true");
  870. flushRequest.addParameter("wait_if_ongoing", "true");
  871. assertOK(client().performRequest(flushRequest));
  872. } else {
  873. count = countOfIndexedRandomDocuments();
  874. }
  875. // Count the documents in the index to make sure we have as many as we put there
  876. Request countRequest = new Request("GET", "/" + index + "/_search");
  877. countRequest.addParameter("size", "0");
  878. refreshAllIndices();
  879. Map<String, Object> countResponse = entityAsMap(client().performRequest(countRequest));
  880. assertTotalHits(count, countResponse);
  881. if (false == isRunningAgainstOldCluster()) {
  882. boolean foundPrimary = false;
  883. Request recoveryRequest = new Request("GET", "/_cat/recovery/" + index);
  884. recoveryRequest.addParameter("h", "index,shard,type,stage,translog_ops_recovered");
  885. recoveryRequest.addParameter("s", "index,shard,type");
  886. String recoveryResponse = toStr(client().performRequest(recoveryRequest));
  887. foundPrimary = recoveryResponse.split("\n").length > 0;
  888. assertTrue("expected to find a primary but didn't\n" + recoveryResponse, foundPrimary);
  889. }
  890. }
  891. /**
  892. * Tests snapshot/restore by creating a snapshot and restoring it. It takes
  893. * a snapshot on the old cluster and restores it on the old cluster as a
  894. * sanity check and on the new cluster as an upgrade test. It also takes a
  895. * snapshot on the new cluster and restores that on the new cluster as a
  896. * test that the repository is ok with containing snapshot from both the
  897. * old and new versions. All of the snapshots include an index, a template,
  898. * and some routing configuration.
  899. */
  900. public void testSnapshotRestore() throws IOException {
  901. int count;
  902. if (isRunningAgainstOldCluster()) {
  903. // Create the index
  904. count = between(200, 300);
  905. Settings.Builder settings = Settings.builder();
  906. if (minimumIndexVersion().before(IndexVersions.V_8_0_0) && randomBoolean()) {
  907. settings.put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean());
  908. }
  909. createIndex(index, settings.build());
  910. indexRandomDocuments(count, true, true, randomBoolean(), i -> jsonBuilder().startObject().field("field", "value").endObject());
  911. } else {
  912. count = countOfIndexedRandomDocuments();
  913. }
  914. // Refresh the index so the count doesn't fail
  915. refreshAllIndices();
  916. // Count the documents in the index to make sure we have as many as we put there
  917. Request countRequest = new Request("GET", "/" + index + "/_search");
  918. countRequest.addParameter("size", "0");
  919. Map<String, Object> countResponse = entityAsMap(client().performRequest(countRequest));
  920. assertTotalHits(count, countResponse);
  921. // Stick a routing attribute into to cluster settings so we can see it after the restore
  922. client().performRequest(
  923. newXContentRequest(
  924. HttpMethod.PUT,
  925. "/_cluster/settings",
  926. (builder, params) -> builder.startObject("persistent")
  927. .field("cluster.routing.allocation.exclude.test_attr", getOldClusterVersion())
  928. .endObject()
  929. )
  930. );
  931. // Stick a template into the cluster so we can see it after the restore
  932. Request createTemplateRequest = newXContentRequest(HttpMethod.PUT, "/_template/test_template", (templateBuilder, params) -> {
  933. templateBuilder.field("index_patterns", "evil_*"); // Don't confuse other tests by applying the template
  934. templateBuilder.startObject("settings");
  935. {
  936. templateBuilder.field("number_of_shards", 1);
  937. }
  938. templateBuilder.endObject();
  939. templateBuilder.startObject("mappings");
  940. {
  941. {
  942. templateBuilder.startObject("_source");
  943. {
  944. templateBuilder.field("enabled", true);
  945. }
  946. templateBuilder.endObject();
  947. }
  948. }
  949. templateBuilder.endObject();
  950. templateBuilder.startObject("aliases");
  951. {
  952. templateBuilder.startObject("alias1").endObject();
  953. templateBuilder.startObject("alias2");
  954. {
  955. templateBuilder.startObject("filter");
  956. {
  957. templateBuilder.startObject("term");
  958. {
  959. templateBuilder.field(
  960. "version",
  961. isRunningAgainstOldCluster() ? getOldClusterVersion() : Build.current().version()
  962. );
  963. }
  964. templateBuilder.endObject();
  965. }
  966. templateBuilder.endObject();
  967. }
  968. templateBuilder.endObject();
  969. }
  970. templateBuilder.endObject();
  971. return templateBuilder;
  972. });
  973. createTemplateRequest.setOptions(expectWarnings(RestPutIndexTemplateAction.DEPRECATION_WARNING));
  974. client().performRequest(createTemplateRequest);
  975. if (isRunningAgainstOldCluster()) {
  976. // Create the repo
  977. client().performRequest(newXContentRequest(HttpMethod.PUT, "/_snapshot/repo", (repoConfig, params) -> {
  978. repoConfig.field("type", "fs");
  979. repoConfig.startObject("settings");
  980. {
  981. repoConfig.field("compress", randomBoolean());
  982. repoConfig.field("location", repoDirectory.getRoot().getPath());
  983. }
  984. return repoConfig.endObject();
  985. }));
  986. }
  987. Request createSnapshot = newXContentRequest(
  988. HttpMethod.PUT,
  989. "/_snapshot/repo/" + (isRunningAgainstOldCluster() ? "old_snap" : "new_snap"),
  990. (builder, params) -> builder.field("indices", index)
  991. );
  992. createSnapshot.addParameter("wait_for_completion", "true");
  993. client().performRequest(createSnapshot);
  994. checkSnapshot("old_snap", count, getOldClusterVersion(), getOldClusterIndexVersion());
  995. if (false == isRunningAgainstOldCluster()) {
  996. checkSnapshot("new_snap", count, Build.current().version(), IndexVersion.current());
  997. }
  998. }
  999. public void testHistoryUUIDIsAdded() throws Exception {
  1000. if (isRunningAgainstOldCluster()) {
  1001. client().performRequest(newXContentRequest(HttpMethod.PUT, '/' + index, (mappingsAndSettings, params) -> {
  1002. mappingsAndSettings.startObject("settings");
  1003. mappingsAndSettings.field("number_of_shards", 1);
  1004. mappingsAndSettings.field("number_of_replicas", 1);
  1005. mappingsAndSettings.endObject();
  1006. return mappingsAndSettings;
  1007. }));
  1008. } else {
  1009. ensureGreenLongWait(index);
  1010. Request statsRequest = new Request("GET", index + "/_stats");
  1011. statsRequest.addParameter("level", "shards");
  1012. Response response = client().performRequest(statsRequest);
  1013. List<Object> shardStats = ObjectPath.createFromResponse(response).evaluate("indices." + index + ".shards.0");
  1014. assertThat(shardStats, notNullValue());
  1015. assertThat("Expected stats for 2 shards", shardStats, hasSize(2));
  1016. String globalHistoryUUID = null;
  1017. for (Object shard : shardStats) {
  1018. final String nodeId = ObjectPath.evaluate(shard, "routing.node");
  1019. final Boolean primary = ObjectPath.evaluate(shard, "routing.primary");
  1020. logger.info("evaluating: {} , {}", ObjectPath.evaluate(shard, "routing"), ObjectPath.evaluate(shard, "commit"));
  1021. String historyUUID = ObjectPath.evaluate(shard, "commit.user_data.history_uuid");
  1022. assertThat("no history uuid found on " + nodeId + " (primary: " + primary + ")", historyUUID, notNullValue());
  1023. if (globalHistoryUUID == null) {
  1024. globalHistoryUUID = historyUUID;
  1025. } else {
  1026. assertThat(
  1027. "history uuid mismatch on " + nodeId + " (primary: " + primary + ")",
  1028. historyUUID,
  1029. equalTo(globalHistoryUUID)
  1030. );
  1031. }
  1032. }
  1033. }
  1034. }
  1035. public void testSoftDeletes() throws Exception {
  1036. if (isRunningAgainstOldCluster()) {
  1037. client().performRequest(newXContentRequest(HttpMethod.PUT, "/" + index, (mappingsAndSettings, params) -> {
  1038. mappingsAndSettings.startObject("settings");
  1039. mappingsAndSettings.field("number_of_shards", 1);
  1040. mappingsAndSettings.field("number_of_replicas", 1);
  1041. if (randomBoolean()) {
  1042. mappingsAndSettings.field("soft_deletes.enabled", true);
  1043. }
  1044. mappingsAndSettings.endObject();
  1045. return mappingsAndSettings;
  1046. }));
  1047. int numDocs = between(10, 100);
  1048. for (int i = 0; i < numDocs; i++) {
  1049. client().performRequest(
  1050. newXContentRequest(HttpMethod.POST, "/" + index + "/_doc/" + i, (builder, params) -> builder.field("field", "v1"))
  1051. );
  1052. refreshAllIndices();
  1053. }
  1054. client().performRequest(new Request("POST", "/" + index + "/_flush"));
  1055. int liveDocs = numDocs;
  1056. assertTotalHits(liveDocs, entityAsMap(client().performRequest(new Request("GET", "/" + index + "/_search"))));
  1057. for (int i = 0; i < numDocs; i++) {
  1058. if (randomBoolean()) {
  1059. client().performRequest(
  1060. newXContentRequest(HttpMethod.POST, "/" + index + "/_doc/" + i, (builder, params) -> builder.field("field", "v2"))
  1061. );
  1062. } else if (randomBoolean()) {
  1063. client().performRequest(new Request("DELETE", "/" + index + "/_doc/" + i));
  1064. liveDocs--;
  1065. }
  1066. }
  1067. refreshAllIndices();
  1068. assertTotalHits(liveDocs, entityAsMap(client().performRequest(new Request("GET", "/" + index + "/_search"))));
  1069. saveInfoDocument(index + "_doc_count", Integer.toString(liveDocs));
  1070. } else {
  1071. int liveDocs = Integer.parseInt(loadInfoDocument(index + "_doc_count"));
  1072. assertTotalHits(liveDocs, entityAsMap(client().performRequest(new Request("GET", "/" + index + "/_search"))));
  1073. }
  1074. }
  1075. /**
  1076. * This test creates an index in the old cluster and then closes it. When the cluster is fully restarted in a newer version,
  1077. * it verifies that the index exists and is replicated if the old version supports replication.
  1078. */
  1079. public void testClosedIndices() throws Exception {
  1080. if (isRunningAgainstOldCluster()) {
  1081. createIndex(index, Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 1).build());
  1082. ensureGreen(index);
  1083. int numDocs = 0;
  1084. if (randomBoolean()) {
  1085. numDocs = between(1, 100);
  1086. for (int i = 0; i < numDocs; i++) {
  1087. assertOK(
  1088. client().performRequest(
  1089. newXContentRequest(
  1090. HttpMethod.POST,
  1091. "/" + index + "/_doc/" + i,
  1092. (builder, params) -> builder.field("field", "v1")
  1093. )
  1094. )
  1095. );
  1096. if (rarely()) {
  1097. refreshAllIndices();
  1098. }
  1099. }
  1100. refreshAllIndices();
  1101. }
  1102. assertTotalHits(numDocs, entityAsMap(client().performRequest(new Request("GET", "/" + index + "/_search"))));
  1103. saveInfoDocument(index + "_doc_count", Integer.toString(numDocs));
  1104. closeIndex(index);
  1105. }
  1106. ensureGreenLongWait(index);
  1107. assertClosedIndex(index, true);
  1108. if (isRunningAgainstOldCluster() == false) {
  1109. openIndex(index);
  1110. ensureGreen(index);
  1111. final int expectedNumDocs = Integer.parseInt(loadInfoDocument(index + "_doc_count"));
  1112. assertTotalHits(expectedNumDocs, entityAsMap(client().performRequest(new Request("GET", "/" + index + "/_search"))));
  1113. }
  1114. }
  1115. /**
  1116. * Asserts that an index is closed in the cluster state. If `checkRoutingTable` is true, it also asserts
  1117. * that the index has started shards.
  1118. */
  1119. @SuppressWarnings("unchecked")
  1120. private void assertClosedIndex(final String indexName, final boolean checkRoutingTable) throws IOException {
  1121. final Map<String, ?> state = entityAsMap(client().performRequest(new Request("GET", "/_cluster/state")));
  1122. final Map<String, ?> metadata = (Map<String, Object>) XContentMapValues.extractValue("metadata.indices." + indexName, state);
  1123. assertThat(metadata, notNullValue());
  1124. assertThat(metadata.get("state"), equalTo("close"));
  1125. final Map<String, ?> blocks = (Map<String, Object>) XContentMapValues.extractValue("blocks.indices." + indexName, state);
  1126. assertThat(blocks, notNullValue());
  1127. assertThat(blocks.containsKey(String.valueOf(MetadataIndexStateService.INDEX_CLOSED_BLOCK_ID)), is(true));
  1128. final Map<String, ?> settings = (Map<String, Object>) XContentMapValues.extractValue("settings", metadata);
  1129. assertThat(settings, notNullValue());
  1130. final Map<String, ?> routingTable = (Map<String, Object>) XContentMapValues.extractValue(
  1131. "routing_table.indices." + indexName,
  1132. state
  1133. );
  1134. if (checkRoutingTable) {
  1135. assertThat(routingTable, notNullValue());
  1136. assertThat(Booleans.parseBoolean((String) XContentMapValues.extractValue("index.verified_before_close", settings)), is(true));
  1137. final String numberOfShards = (String) XContentMapValues.extractValue("index.number_of_shards", settings);
  1138. assertThat(numberOfShards, notNullValue());
  1139. final int nbShards = Integer.parseInt(numberOfShards);
  1140. assertThat(nbShards, greaterThanOrEqualTo(1));
  1141. for (int i = 0; i < nbShards; i++) {
  1142. final Collection<Map<String, ?>> shards = (Collection<Map<String, ?>>) XContentMapValues.extractValue(
  1143. "shards." + i,
  1144. routingTable
  1145. );
  1146. assertThat(shards, notNullValue());
  1147. assertThat(shards.size(), equalTo(2));
  1148. for (Map<String, ?> shard : shards) {
  1149. assertThat(XContentMapValues.extractValue("shard", shard), equalTo(i));
  1150. assertThat(XContentMapValues.extractValue("state", shard), equalTo("STARTED"));
  1151. assertThat(XContentMapValues.extractValue("index", shard), equalTo(indexName));
  1152. }
  1153. }
  1154. } else {
  1155. assertThat(routingTable, nullValue());
  1156. assertThat(XContentMapValues.extractValue("index.verified_before_close", settings), nullValue());
  1157. }
  1158. }
  1159. @SuppressWarnings("unchecked")
  1160. private void checkSnapshot(String snapshotName, int count, String tookOnVersion, IndexVersion tookOnIndexVersion) throws IOException {
  1161. // Check the snapshot metadata, especially the version
  1162. Request listSnapshotRequest = new Request("GET", "/_snapshot/repo/" + snapshotName);
  1163. Map<String, Object> snapResponse = entityAsMap(client().performRequest(listSnapshotRequest));
  1164. assertEquals(singletonList(snapshotName), XContentMapValues.extractValue("snapshots.snapshot", snapResponse));
  1165. assertEquals(singletonList("SUCCESS"), XContentMapValues.extractValue("snapshots.state", snapResponse));
  1166. // the format can change depending on the ES node version running & this test code running
  1167. // and if there's an in-progress release that hasn't been published yet,
  1168. // which could affect the top range of the index release version
  1169. String firstReleaseVersion = tookOnIndexVersion.toReleaseVersion().split("-")[0];
  1170. assertThat(
  1171. (Iterable<String>) XContentMapValues.extractValue("snapshots.version", snapResponse),
  1172. anyOf(
  1173. contains(tookOnVersion),
  1174. contains(tookOnIndexVersion.toString()),
  1175. contains(firstReleaseVersion),
  1176. contains(startsWith(firstReleaseVersion + "-"))
  1177. )
  1178. );
  1179. // Remove the routing setting and template so we can test restoring them.
  1180. client().performRequest(
  1181. newXContentRequest(
  1182. HttpMethod.PUT,
  1183. "/_cluster/settings",
  1184. (builder, params) -> builder.startObject("persistent").nullField("cluster.routing.allocation.exclude.test_attr").endObject()
  1185. )
  1186. );
  1187. client().performRequest(new Request("DELETE", "/_template/test_template"));
  1188. // Restore
  1189. Request restoreRequest = newXContentRequest(
  1190. HttpMethod.POST,
  1191. "/_snapshot/repo/" + snapshotName + "/_restore",
  1192. (restoreCommand, params) -> {
  1193. restoreCommand.field("include_global_state", true);
  1194. restoreCommand.field("indices", index);
  1195. restoreCommand.field("rename_pattern", index);
  1196. restoreCommand.field("rename_replacement", "restored_" + index);
  1197. return restoreCommand;
  1198. }
  1199. );
  1200. restoreRequest.addParameter("wait_for_completion", "true");
  1201. client().performRequest(restoreRequest);
  1202. // Make sure search finds all documents
  1203. Request countRequest = new Request("GET", "/restored_" + index + "/_search");
  1204. countRequest.addParameter("size", "0");
  1205. Map<String, Object> countResponse = entityAsMap(client().performRequest(countRequest));
  1206. assertTotalHits(count, countResponse);
  1207. // Add some extra documents to the index to be sure we can still write to it after restoring it
  1208. int extras = between(1, 100);
  1209. StringBuilder bulk = new StringBuilder();
  1210. for (int i = 0; i < extras; i++) {
  1211. bulk.append(Strings.format("""
  1212. {"index":{"_id":"%s"}}
  1213. {"test":"test"}
  1214. """, count + i));
  1215. }
  1216. Request writeToRestoredRequest = new Request("POST", "/restored_" + index + "/_bulk");
  1217. writeToRestoredRequest.addParameter("refresh", "true");
  1218. writeToRestoredRequest.setJsonEntity(bulk.toString());
  1219. assertThat(EntityUtils.toString(client().performRequest(writeToRestoredRequest).getEntity()), containsString("\"errors\":false"));
  1220. // And count to make sure the add worked
  1221. // Make sure search finds all documents
  1222. Request countAfterWriteRequest = new Request("GET", "/restored_" + index + "/_search");
  1223. countAfterWriteRequest.addParameter("size", "0");
  1224. Map<String, Object> countAfterResponse = entityAsMap(client().performRequest(countRequest));
  1225. assertTotalHits(count + extras, countAfterResponse);
  1226. // Clean up the index for the next iteration
  1227. client().performRequest(new Request("DELETE", "/restored_*"));
  1228. // Check settings added by the restore process
  1229. Request clusterSettingsRequest = new Request("GET", "/_cluster/settings");
  1230. clusterSettingsRequest.addParameter("flat_settings", "true");
  1231. Map<String, Object> clusterSettingsResponse = entityAsMap(client().performRequest(clusterSettingsRequest));
  1232. @SuppressWarnings("unchecked")
  1233. final Map<String, Object> persistentSettings = (Map<String, Object>) clusterSettingsResponse.get("persistent");
  1234. assertThat(persistentSettings.get("cluster.routing.allocation.exclude.test_attr"), equalTo(getOldClusterVersion()));
  1235. // Check that the template was restored successfully
  1236. Request getTemplateRequest = new Request("GET", "/_template/test_template");
  1237. Map<String, Object> getTemplateResponse = entityAsMap(client().performRequest(getTemplateRequest));
  1238. Map<String, Object> expectedTemplate = new HashMap<>();
  1239. expectedTemplate.put("index_patterns", singletonList("evil_*"));
  1240. expectedTemplate.put("settings", singletonMap("index", singletonMap("number_of_shards", "1")));
  1241. expectedTemplate.put("mappings", singletonMap("_source", singletonMap("enabled", true)));
  1242. expectedTemplate.put("order", 0);
  1243. Map<String, Object> aliases = new HashMap<>();
  1244. aliases.put("alias1", emptyMap());
  1245. aliases.put("alias2", singletonMap("filter", singletonMap("term", singletonMap("version", tookOnVersion))));
  1246. expectedTemplate.put("aliases", aliases);
  1247. expectedTemplate = singletonMap("test_template", expectedTemplate);
  1248. if (false == expectedTemplate.equals(getTemplateResponse)) {
  1249. NotEqualMessageBuilder builder = new NotEqualMessageBuilder();
  1250. builder.compareMaps(getTemplateResponse, expectedTemplate);
  1251. logger.info("expected: {}\nactual:{}", expectedTemplate, getTemplateResponse);
  1252. fail("template doesn't match:\n" + builder);
  1253. }
  1254. }
  1255. private void indexRandomDocuments(
  1256. final int count,
  1257. final boolean flushAllowed,
  1258. final boolean saveInfo,
  1259. final boolean specifyId,
  1260. final CheckedFunction<Integer, XContentBuilder, IOException> docSupplier
  1261. ) throws IOException {
  1262. logger.info("Indexing {} random documents", count);
  1263. for (int i = 0; i < count; i++) {
  1264. logger.debug("Indexing document [{}]", i);
  1265. Request createDocument = new Request("POST", "/" + index + "/_doc/" + (specifyId ? i : ""));
  1266. createDocument.setJsonEntity(Strings.toString(docSupplier.apply(i)));
  1267. client().performRequest(createDocument);
  1268. if (rarely()) {
  1269. refreshAllIndices();
  1270. }
  1271. if (flushAllowed && rarely()) {
  1272. logger.debug("Flushing [{}]", index);
  1273. client().performRequest(new Request("POST", "/" + index + "/_flush"));
  1274. }
  1275. }
  1276. if (saveInfo) {
  1277. saveInfoDocument(index + "_count", Integer.toString(count));
  1278. }
  1279. }
  1280. private void indexDocument(String id) throws IOException {
  1281. final var req = newXContentRequest(HttpMethod.POST, "/" + index + "/" + "_doc/" + id, (builder, params) -> builder.field("f", "v"));
  1282. assertOK(client().performRequest(req));
  1283. }
  1284. private int countOfIndexedRandomDocuments() throws IOException {
  1285. return Integer.parseInt(loadInfoDocument(index + "_count"));
  1286. }
  1287. private void saveInfoDocument(String id, String value) throws IOException {
  1288. // Only create the first version so we know how many documents are created when the index is first created
  1289. Request request = newXContentRequest(HttpMethod.PUT, "/info/_doc/" + id, (builder, params) -> builder.field("value", value));
  1290. request.addParameter("op_type", "create");
  1291. client().performRequest(request);
  1292. }
  1293. private String loadInfoDocument(String id) throws IOException {
  1294. Request request = new Request("GET", "/info/_doc/" + id);
  1295. request.addParameter("filter_path", "_source");
  1296. String doc = toStr(client().performRequest(request));
  1297. Matcher m = Pattern.compile("\"value\":\"(.+)\"").matcher(doc);
  1298. assertTrue(doc, m.find());
  1299. return m.group(1);
  1300. }
  1301. private List<String> dataNodes(String indexName, RestClient client) throws IOException {
  1302. Request request = new Request("GET", indexName + "/_stats");
  1303. request.addParameter("level", "shards");
  1304. Response response = client.performRequest(request);
  1305. List<String> nodes = new ArrayList<>();
  1306. List<Object> shardStats = ObjectPath.createFromResponse(response).evaluate("indices." + indexName + ".shards.0");
  1307. for (Object shard : shardStats) {
  1308. final String nodeId = ObjectPath.evaluate(shard, "routing.node");
  1309. nodes.add(nodeId);
  1310. }
  1311. return nodes;
  1312. }
  1313. /**
  1314. * Wait for an index to have green health, waiting longer than
  1315. * {@link ESRestTestCase#ensureGreen}.
  1316. */
  1317. protected void ensureGreenLongWait(String indexName) throws IOException {
  1318. Request request = new Request("GET", "/_cluster/health/" + indexName);
  1319. request.addParameter("timeout", "2m");
  1320. request.addParameter("wait_for_status", "green");
  1321. request.addParameter("wait_for_no_relocating_shards", "true");
  1322. request.addParameter("wait_for_events", "languid");
  1323. request.addParameter("level", "shards");
  1324. Map<String, Object> healthRsp = entityAsMap(client().performRequest(request));
  1325. logger.info("health api response: {}", healthRsp);
  1326. assertEquals("green", healthRsp.get("status"));
  1327. assertFalse((Boolean) healthRsp.get("timed_out"));
  1328. }
  1329. public void testPeerRecoveryRetentionLeases() throws Exception {
  1330. if (isRunningAgainstOldCluster()) {
  1331. client().performRequest(newXContentRequest(HttpMethod.PUT, "/" + index, (settings, params) -> {
  1332. settings.startObject("settings");
  1333. settings.field("number_of_shards", between(1, 5));
  1334. settings.field("number_of_replicas", between(0, 1));
  1335. settings.endObject();
  1336. return settings;
  1337. }));
  1338. }
  1339. ensureGreen(index);
  1340. ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index);
  1341. }
  1342. /**
  1343. * Tests that with or without soft-deletes, we should perform an operation-based recovery if there were some
  1344. * but not too many uncommitted documents (i.e., less than 10% of committed documents or the extra translog)
  1345. * before we restart the cluster. This is important when we move from translog based to retention leases based
  1346. * peer recoveries.
  1347. */
  1348. public void testOperationBasedRecovery() throws Exception {
  1349. if (isRunningAgainstOldCluster()) {
  1350. Settings.Builder settings = indexSettings(1, 1);
  1351. if (minimumIndexVersion().before(IndexVersions.V_8_0_0) && randomBoolean()) {
  1352. settings.put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), randomBoolean());
  1353. }
  1354. final String mappings = randomBoolean() ? "\"_source\": { \"enabled\": false}" : null;
  1355. createIndex(index, settings.build(), mappings);
  1356. ensureGreen(index);
  1357. int committedDocs = randomIntBetween(100, 200);
  1358. for (int i = 0; i < committedDocs; i++) {
  1359. indexDocument(Integer.toString(i));
  1360. if (rarely()) {
  1361. flush(index, randomBoolean());
  1362. }
  1363. }
  1364. flush(index, true);
  1365. ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index);
  1366. // less than 10% of the committed docs (see IndexSetting#FILE_BASED_RECOVERY_THRESHOLD_SETTING).
  1367. int uncommittedDocs = randomIntBetween(0, (int) (committedDocs * 0.1));
  1368. for (int i = 0; i < uncommittedDocs; i++) {
  1369. final String id = Integer.toString(randomIntBetween(1, 100));
  1370. indexDocument(id);
  1371. }
  1372. } else {
  1373. ensureGreen(index);
  1374. assertNoFileBasedRecovery(index, n -> true);
  1375. ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index);
  1376. }
  1377. }
  1378. /**
  1379. * Verifies that once all shard copies on the new version, we should turn off the translog retention for indices with soft-deletes.
  1380. */
  1381. public void testTurnOffTranslogRetentionAfterUpgraded() throws Exception {
  1382. if (isRunningAgainstOldCluster()) {
  1383. createIndex(index, indexSettings(1, 1).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true).build());
  1384. ensureGreen(index);
  1385. int numDocs = randomIntBetween(10, 100);
  1386. for (int i = 0; i < numDocs; i++) {
  1387. indexDocument(Integer.toString(randomIntBetween(1, 100)));
  1388. if (rarely()) {
  1389. flush(index, randomBoolean());
  1390. }
  1391. }
  1392. } else {
  1393. ensureGreen(index);
  1394. flush(index, true);
  1395. assertEmptyTranslog(index);
  1396. ensurePeerRecoveryRetentionLeasesRenewedAndSynced(index);
  1397. }
  1398. }
  1399. public void testResize() throws Exception {
  1400. int numDocs;
  1401. if (isRunningAgainstOldCluster()) {
  1402. final Settings.Builder settings = indexSettings(3, 1);
  1403. if (minimumIndexVersion().before(IndexVersions.V_8_0_0) && randomBoolean()) {
  1404. settings.put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), false);
  1405. }
  1406. final String mappings = randomBoolean() ? "\"_source\": { \"enabled\": false}" : null;
  1407. createIndex(index, settings.build(), mappings);
  1408. numDocs = randomIntBetween(10, 1000);
  1409. for (int i = 0; i < numDocs; i++) {
  1410. indexDocument(Integer.toString(i));
  1411. if (rarely()) {
  1412. flush(index, randomBoolean());
  1413. }
  1414. }
  1415. saveInfoDocument("num_doc_" + index, Integer.toString(numDocs));
  1416. ensureGreen(index);
  1417. } else {
  1418. ensureGreen(index);
  1419. numDocs = Integer.parseInt(loadInfoDocument("num_doc_" + index));
  1420. int moreDocs = randomIntBetween(0, 100);
  1421. for (int i = 0; i < moreDocs; i++) {
  1422. indexDocument(Integer.toString(numDocs + i));
  1423. if (rarely()) {
  1424. flush(index, randomBoolean());
  1425. }
  1426. }
  1427. final ToXContent settings0 = (builder, params) -> builder.startObject("settings").field("index.blocks.write", true).endObject();
  1428. client().performRequest(newXContentRequest(HttpMethod.PUT, "/" + index + "/_settings", settings0));
  1429. {
  1430. final String target = index + "_shrunken";
  1431. Settings.Builder settings = Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1);
  1432. if (randomBoolean()) {
  1433. settings.put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true);
  1434. }
  1435. client().performRequest(newXContentRequest(HttpMethod.PUT, "/" + index + "/_shrink/" + target, (builder, params) -> {
  1436. builder.startObject("settings");
  1437. settings.build().toXContent(builder, params);
  1438. return builder.endObject();
  1439. }));
  1440. ensureGreenLongWait(target);
  1441. assertNumHits(target, numDocs + moreDocs, 1);
  1442. }
  1443. {
  1444. final String target = index + "_split";
  1445. Settings.Builder settings = Settings.builder().put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 6);
  1446. if (randomBoolean()) {
  1447. settings.put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true);
  1448. }
  1449. client().performRequest(newXContentRequest(HttpMethod.PUT, "/" + index + "/_split/" + target, (builder, params) -> {
  1450. builder.startObject("settings");
  1451. settings.build().toXContent(builder, params);
  1452. return builder.endObject();
  1453. }));
  1454. ensureGreenLongWait(target);
  1455. assertNumHits(target, numDocs + moreDocs, 6);
  1456. }
  1457. {
  1458. final String target = index + "_cloned";
  1459. client().performRequest(new Request("PUT", "/" + index + "/_clone/" + target));
  1460. ensureGreenLongWait(target);
  1461. assertNumHits(target, numDocs + moreDocs, 3);
  1462. }
  1463. }
  1464. }
  1465. @SuppressWarnings("unchecked")
  1466. public void testSystemIndexMetadataIsUpgraded() throws Exception {
  1467. final String systemIndexWarning = "this request accesses system indices: [.tasks], but in a future major version, direct "
  1468. + "access to system indices will be prevented by default";
  1469. if (isRunningAgainstOldCluster()) {
  1470. // create index
  1471. client().performRequest(
  1472. newXContentRequest(
  1473. HttpMethod.PUT,
  1474. "/test_index_old",
  1475. (builder, params) -> builder.startObject("settings").field("index.number_of_replicas", 0).endObject()
  1476. )
  1477. );
  1478. Request bulk = new Request("POST", "/_bulk");
  1479. bulk.addParameter("refresh", "true");
  1480. bulk.setJsonEntity("""
  1481. {"index": {"_index": "test_index_old"}}
  1482. {"f1": "v1", "f2": "v2"}
  1483. """);
  1484. client().performRequest(bulk);
  1485. // start a async reindex job
  1486. Request reindex = newXContentRequest(
  1487. HttpMethod.POST,
  1488. "/_reindex",
  1489. (builder, params) -> builder.startObject("source")
  1490. .field("index", "test_index_old")
  1491. .endObject()
  1492. .startObject("dest")
  1493. .field("index", "test_index_reindex")
  1494. .endObject()
  1495. );
  1496. reindex.addParameter("wait_for_completion", "false");
  1497. Map<String, Object> response = entityAsMap(client().performRequest(reindex));
  1498. String taskId = (String) response.get("task");
  1499. // wait for task
  1500. Request getTask = new Request("GET", "/_tasks/" + taskId);
  1501. getTask.addParameter("wait_for_completion", "true");
  1502. client().performRequest(getTask);
  1503. // make sure .tasks index exists
  1504. Request getTasksIndex = new Request("GET", "/.tasks");
  1505. getTasksIndex.setOptions(expectVersionSpecificWarnings(v -> {
  1506. v.current(systemIndexWarning);
  1507. v.compatible(systemIndexWarning);
  1508. }));
  1509. getTasksIndex.addParameter("allow_no_indices", "false");
  1510. getTasksIndex.setOptions(expectVersionSpecificWarnings(v -> {
  1511. v.current(systemIndexWarning);
  1512. v.compatible(systemIndexWarning);
  1513. }));
  1514. assertBusy(() -> {
  1515. try {
  1516. assertThat(client().performRequest(getTasksIndex).getStatusLine().getStatusCode(), is(200));
  1517. } catch (ResponseException e) {
  1518. throw new AssertionError(".tasks index does not exist yet");
  1519. }
  1520. });
  1521. } else {
  1522. assertBusy(() -> {
  1523. Request clusterStateRequest = new Request("GET", "/_cluster/state/metadata");
  1524. Map<String, Object> indices = new XContentTestUtils.JsonMapView(entityAsMap(client().performRequest(clusterStateRequest)))
  1525. .get("metadata.indices");
  1526. // Make sure our non-system index is still non-system
  1527. assertThat(new XContentTestUtils.JsonMapView(indices).get("test_index_old.system"), is(false));
  1528. // Can't get the .tasks index via JsonMapView because it splits on `.`
  1529. assertThat(indices, hasKey(".tasks"));
  1530. XContentTestUtils.JsonMapView tasksIndex = new XContentTestUtils.JsonMapView((Map<String, Object>) indices.get(".tasks"));
  1531. assertThat(tasksIndex.get("system"), is(true));
  1532. // If .tasks was created in a 7.x version, it should have an alias on it that we need to make sure got upgraded properly.
  1533. final String tasksCreatedVersionString = tasksIndex.get("settings.index.version.created");
  1534. assertThat(tasksCreatedVersionString, notNullValue());
  1535. final IndexVersion tasksCreatedVersion = IndexVersion.fromId(Integer.parseInt(tasksCreatedVersionString));
  1536. if (tasksCreatedVersion.before(SYSTEM_INDEX_ENFORCEMENT_INDEX_VERSION)) {
  1537. // Verify that the alias survived the upgrade
  1538. Request getAliasRequest = new Request("GET", "/_alias/test-system-alias");
  1539. getAliasRequest.setOptions(expectVersionSpecificWarnings(v -> {
  1540. v.current(systemIndexWarning);
  1541. v.compatible(systemIndexWarning);
  1542. }));
  1543. Map<String, Object> aliasResponse = entityAsMap(client().performRequest(getAliasRequest));
  1544. assertThat(aliasResponse, hasKey(".tasks"));
  1545. assertThat(aliasResponse, hasKey("test_index_reindex"));
  1546. }
  1547. });
  1548. }
  1549. }
  1550. /**
  1551. * This test ensures that search results on old indices using "persian" analyzer don't change
  1552. * after we introduce Lucene 10
  1553. */
  1554. public void testPersianAnalyzerBWC() throws Exception {
  1555. var originalClusterLegacyPersianAnalyzer = oldClusterHasFeature(SearchFeatures.LUCENE_10_0_0_UPGRADE) == false;
  1556. assumeTrue("Don't run this test if both versions already support stemming", originalClusterLegacyPersianAnalyzer);
  1557. final String indexName = "test_persian_stemmer";
  1558. Settings idxSettings = indexSettings(1, 1).build();
  1559. String mapping = """
  1560. {
  1561. "properties": {
  1562. "textfield" : {
  1563. "type": "text",
  1564. "analyzer": "persian"
  1565. }
  1566. }
  1567. }
  1568. """;
  1569. String query = """
  1570. {
  1571. "query": {
  1572. "match": {
  1573. "textfield": "كتابها"
  1574. }
  1575. }
  1576. }
  1577. """;
  1578. if (isRunningAgainstOldCluster()) {
  1579. createIndex(client(), indexName, idxSettings, mapping);
  1580. ensureGreen(indexName);
  1581. assertOK(
  1582. client().performRequest(
  1583. newXContentRequest(
  1584. HttpMethod.POST,
  1585. "/" + indexName + "/" + "_doc/1",
  1586. (builder, params) -> builder.field("textfield", "كتابها")
  1587. )
  1588. )
  1589. );
  1590. assertOK(
  1591. client().performRequest(
  1592. newXContentRequest(
  1593. HttpMethod.POST,
  1594. "/" + indexName + "/" + "_doc/2",
  1595. (builder, params) -> builder.field("textfield", "كتاب")
  1596. )
  1597. )
  1598. );
  1599. refresh(indexName);
  1600. assertNumHits(indexName, 2, 1);
  1601. Request searchRequest = new Request("POST", "/" + indexName + "/_search");
  1602. searchRequest.setJsonEntity(query);
  1603. assertTotalHits(1, entityAsMap(client().performRequest(searchRequest)));
  1604. } else {
  1605. // old index should still only return one doc
  1606. Request searchRequest = new Request("POST", "/" + indexName + "/_search");
  1607. searchRequest.setJsonEntity(query);
  1608. assertTotalHits(1, entityAsMap(client().performRequest(searchRequest)));
  1609. String newIndexName = indexName + "_new";
  1610. createIndex(client(), newIndexName, idxSettings, mapping);
  1611. ensureGreen(newIndexName);
  1612. assertOK(
  1613. client().performRequest(
  1614. newXContentRequest(
  1615. HttpMethod.POST,
  1616. "/" + newIndexName + "/" + "_doc/1",
  1617. (builder, params) -> builder.field("textfield", "كتابها")
  1618. )
  1619. )
  1620. );
  1621. assertOK(
  1622. client().performRequest(
  1623. newXContentRequest(
  1624. HttpMethod.POST,
  1625. "/" + newIndexName + "/" + "_doc/2",
  1626. (builder, params) -> builder.field("textfield", "كتاب")
  1627. )
  1628. )
  1629. );
  1630. refresh(newIndexName);
  1631. searchRequest = new Request("POST", "/" + newIndexName + "/_search");
  1632. searchRequest.setJsonEntity(query);
  1633. assertTotalHits(2, entityAsMap(client().performRequest(searchRequest)));
  1634. // searching both indices (old and new analysis version) we should get 1 hit from the old and 2 from the new index
  1635. searchRequest = new Request("POST", "/" + indexName + "," + newIndexName + "/_search");
  1636. searchRequest.setJsonEntity(query);
  1637. assertTotalHits(3, entityAsMap(client().performRequest(searchRequest)));
  1638. }
  1639. }
  1640. /**
  1641. * This test ensures that search results on old indices using "romanain" analyzer don't change
  1642. * after we introduce Lucene 10
  1643. */
  1644. public void testRomanianAnalyzerBWC() throws Exception {
  1645. var originalClusterLegacyRomanianAnalyzer = oldClusterHasFeature(SearchFeatures.LUCENE_10_0_0_UPGRADE) == false;
  1646. assumeTrue("Don't run this test if both versions already support stemming", originalClusterLegacyRomanianAnalyzer);
  1647. final String indexName = "test_romanian_stemmer";
  1648. Settings idxSettings = indexSettings(1, 1).build();
  1649. String cedillaForm = "absenţa";
  1650. String commaForm = "absența";
  1651. String mapping = """
  1652. {
  1653. "properties": {
  1654. "textfield" : {
  1655. "type": "text",
  1656. "analyzer": "romanian"
  1657. }
  1658. }
  1659. }
  1660. """;
  1661. // query that uses the cedilla form of "t"
  1662. String query = """
  1663. {
  1664. "query": {
  1665. "match": {
  1666. "textfield": "absenţa"
  1667. }
  1668. }
  1669. }
  1670. """;
  1671. if (isRunningAgainstOldCluster()) {
  1672. createIndex(client(), indexName, idxSettings, mapping);
  1673. ensureGreen(indexName);
  1674. assertOK(
  1675. client().performRequest(
  1676. newXContentRequest(
  1677. HttpMethod.POST,
  1678. "/" + indexName + "/" + "_doc/1",
  1679. (builder, params) -> builder.field("textfield", cedillaForm)
  1680. )
  1681. )
  1682. );
  1683. assertOK(
  1684. client().performRequest(
  1685. newXContentRequest(
  1686. HttpMethod.POST,
  1687. "/" + indexName + "/" + "_doc/2",
  1688. // this doc uses the comma form
  1689. (builder, params) -> builder.field("textfield", commaForm)
  1690. )
  1691. )
  1692. );
  1693. refresh(indexName);
  1694. assertNumHits(indexName, 2, 1);
  1695. Request searchRequest = new Request("POST", "/" + indexName + "/_search");
  1696. searchRequest.setJsonEntity(query);
  1697. assertTotalHits(1, entityAsMap(client().performRequest(searchRequest)));
  1698. } else {
  1699. // old index should still only return one doc
  1700. Request searchRequest = new Request("POST", "/" + indexName + "/_search");
  1701. searchRequest.setJsonEntity(query);
  1702. assertTotalHits(1, entityAsMap(client().performRequest(searchRequest)));
  1703. String newIndexName = indexName + "_new";
  1704. createIndex(client(), newIndexName, idxSettings, mapping);
  1705. ensureGreen(newIndexName);
  1706. assertOK(
  1707. client().performRequest(
  1708. newXContentRequest(
  1709. HttpMethod.POST,
  1710. "/" + newIndexName + "/" + "_doc/1",
  1711. (builder, params) -> builder.field("textfield", cedillaForm)
  1712. )
  1713. )
  1714. );
  1715. assertOK(
  1716. client().performRequest(
  1717. newXContentRequest(
  1718. HttpMethod.POST,
  1719. "/" + newIndexName + "/" + "_doc/2",
  1720. (builder, params) -> builder.field("textfield", commaForm)
  1721. )
  1722. )
  1723. );
  1724. refresh(newIndexName);
  1725. searchRequest = new Request("POST", "/" + newIndexName + "/_search");
  1726. searchRequest.setJsonEntity(query);
  1727. assertTotalHits(2, entityAsMap(client().performRequest(searchRequest)));
  1728. // searching both indices (old and new analysis version) we should get 1 hit from the old and 2 from the new index
  1729. searchRequest = new Request("POST", "/" + indexName + "," + newIndexName + "/_search");
  1730. searchRequest.setJsonEntity(query);
  1731. assertTotalHits(3, entityAsMap(client().performRequest(searchRequest)));
  1732. }
  1733. }
  1734. public void testForbidDisableSoftDeletesOnRestore() throws Exception {
  1735. final String snapshot = "snapshot-" + index;
  1736. if (isRunningAgainstOldCluster()) {
  1737. final Settings.Builder settings = indexSettings(1, 1).put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true);
  1738. createIndex(index, settings.build());
  1739. ensureGreen(index);
  1740. int numDocs = randomIntBetween(0, 100);
  1741. indexRandomDocuments(
  1742. numDocs,
  1743. true,
  1744. true,
  1745. randomBoolean(),
  1746. i -> jsonBuilder().startObject().field("field", "value").endObject()
  1747. );
  1748. // create repo
  1749. client().performRequest(newXContentRequest(HttpMethod.PUT, "/_snapshot/repo", (repoConfig, params) -> {
  1750. repoConfig.field("type", "fs");
  1751. repoConfig.startObject("settings");
  1752. repoConfig.field("compress", randomBoolean());
  1753. repoConfig.field("location", repoDirectory.getRoot().getPath());
  1754. repoConfig.endObject();
  1755. return repoConfig;
  1756. }));
  1757. // create snapshot
  1758. Request createSnapshot = newXContentRequest(
  1759. HttpMethod.PUT,
  1760. "/_snapshot/repo/" + snapshot,
  1761. (builder, params) -> builder.field("indices", index)
  1762. );
  1763. createSnapshot.addParameter("wait_for_completion", "true");
  1764. client().performRequest(createSnapshot);
  1765. } else {
  1766. // Restore
  1767. Request restoreRequest = newXContentRequest(
  1768. HttpMethod.POST,
  1769. "/_snapshot/repo/" + snapshot + "/_restore",
  1770. (restoreCommand, params) -> {
  1771. restoreCommand.field("indices", index);
  1772. restoreCommand.field("rename_pattern", index);
  1773. restoreCommand.field("rename_replacement", "restored-" + index);
  1774. restoreCommand.startObject("index_settings").field("index.soft_deletes.enabled", false).endObject();
  1775. return restoreCommand;
  1776. }
  1777. );
  1778. restoreRequest.addParameter("wait_for_completion", "true");
  1779. final ResponseException error = expectThrows(ResponseException.class, () -> client().performRequest(restoreRequest));
  1780. assertThat(error.getMessage(), containsString("cannot disable setting [index.soft_deletes.enabled] on restore"));
  1781. }
  1782. }
  1783. public static void assertNumHits(String index, int numHits, int totalShards) throws IOException {
  1784. Map<String, Object> resp = entityAsMap(client().performRequest(new Request("GET", "/" + index + "/_search")));
  1785. assertNoFailures(resp);
  1786. assertThat(XContentMapValues.extractValue("_shards.total", resp), equalTo(totalShards));
  1787. assertThat(XContentMapValues.extractValue("_shards.successful", resp), equalTo(totalShards));
  1788. assertThat(extractTotalHits(resp), equalTo(numHits));
  1789. }
  1790. }