Skip to content

Commit 92c8a12

Browse files
committed
Atomic rename WIP
1 parent f7b7faf commit 92c8a12

File tree

24 files changed

+815
-36
lines changed

24 files changed

+815
-36
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
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+
10+
package org.elasticsearch.aliases;
11+
12+
import org.elasticsearch.action.admin.indices.rename.RenameIndexAction;
13+
import org.elasticsearch.action.support.IndicesOptions;
14+
import org.elasticsearch.action.support.WriteRequest;
15+
import org.elasticsearch.test.ESIntegTestCase;
16+
import org.elasticsearch.test.hamcrest.ElasticsearchAssertions;
17+
import org.elasticsearch.test.junit.annotations.TestLogging;
18+
import org.elasticsearch.xcontent.XContentType;
19+
20+
@TestLogging(value = "org.elasticsearch.indices.cluster.IndicesClusterStateService:DEBUG", reason = "log index renames")
21+
public class IndexAliasRenameIT extends ESIntegTestCase {
22+
private static final String ORIGINAL = "original";
23+
private static final String NEW_INDEX = "new_index";
24+
25+
/**
26+
* Tests a simple index rename operation and verifies that documents are only accessible
27+
* in the new index after the rename.
28+
*/
29+
public void testSimpleRename() {
30+
// Index a document, creating the original index
31+
createIndex(ORIGINAL);
32+
client().prepareIndex(ORIGINAL).setSource("""
33+
{
34+
"foo": "bar", "baz": 123
35+
}""", XContentType.JSON).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).get();
36+
37+
// Ensure the document is searchable in the original index
38+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(ORIGINAL), 1L);
39+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(NEW_INDEX).setIndicesOptions(IndicesOptions.lenientExpandOpen()), 0L);
40+
41+
// Rename the index
42+
client().execute(
43+
RenameIndexAction.INSTANCE,
44+
new RenameIndexAction.Request(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, ORIGINAL, NEW_INDEX)
45+
).actionGet();
46+
47+
// Ensure the new index exists and is healthy
48+
ensureGreen(NEW_INDEX);
49+
50+
// Ensure the document is searchable in the new index
51+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(ORIGINAL).setIndicesOptions(IndicesOptions.lenientExpandOpen()), 0L);
52+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(NEW_INDEX), 1L);
53+
}
54+
55+
/**
56+
* Tests renaming multiple indices and verifies that documents are only accessible
57+
* in the new indices after the renames.
58+
*/
59+
public void testRenameMultipleIndices() {
60+
String index1 = "index1";
61+
String index2 = "index2";
62+
String newIndex1 = "new_index1";
63+
String newIndex2 = "new_index2";
64+
65+
// Create two indices and index a document in each
66+
createIndex(index1);
67+
createIndex(index2);
68+
69+
client().prepareIndex(index1).setSource("""
70+
{ "field": "value1" }
71+
""", XContentType.JSON).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).get();
72+
73+
client().prepareIndex(index2).setSource("""
74+
{ "field": "value2" }
75+
""", XContentType.JSON).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).get();
76+
77+
// Ensure documents are searchable in original indices
78+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(index1), 1L);
79+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(index2), 1L);
80+
81+
// Rename both indices
82+
client().execute(
83+
RenameIndexAction.INSTANCE,
84+
new RenameIndexAction.Request(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, index1, newIndex1)
85+
).actionGet();
86+
87+
client().execute(
88+
RenameIndexAction.INSTANCE,
89+
new RenameIndexAction.Request(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, index2, newIndex2)
90+
).actionGet();
91+
92+
// Ensure new indices exist and are healthy
93+
ensureGreen(newIndex1, newIndex2);
94+
95+
// Ensure documents are searchable in new indices
96+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(newIndex1), 1L);
97+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(newIndex2), 1L);
98+
99+
// Ensure old indices are not searchable
100+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(index1).setIndicesOptions(IndicesOptions.lenientExpandOpen()), 0L);
101+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(index2).setIndicesOptions(IndicesOptions.lenientExpandOpen()), 0L);
102+
}
103+
104+
/**
105+
* Tests renaming an index twice with distinct names and verifies that documents
106+
* are only accessible in the latest renamed index.
107+
*/
108+
public void testRenameIndexTwiceWithDistinctNames() {
109+
String initialIndex = "index_1";
110+
String renamedOnceIndex = "index_2";
111+
String renamedTwiceIndex = "index_3";
112+
113+
// Create the initial index and index a document
114+
createIndex(initialIndex);
115+
client().prepareIndex(initialIndex).setSource("""
116+
{ "field": "test_value" }
117+
""", XContentType.JSON).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).get();
118+
119+
// Assert document is searchable in the initial index
120+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(initialIndex), 1L);
121+
122+
// Rename alpha_index to beta_index
123+
client().execute(
124+
RenameIndexAction.INSTANCE,
125+
new RenameIndexAction.Request(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, initialIndex, renamedOnceIndex)
126+
).actionGet();
127+
128+
// Ensure beta_index exists and is healthy
129+
ensureGreen(renamedOnceIndex);
130+
131+
// Assert document is searchable in beta_index, not in alpha_index
132+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(renamedOnceIndex), 1L);
133+
ElasticsearchAssertions.assertHitCount(
134+
client().prepareSearch(initialIndex).setIndicesOptions(IndicesOptions.lenientExpandOpen()),
135+
0L
136+
);
137+
138+
// Rename beta_index to gamma_index
139+
client().execute(
140+
RenameIndexAction.INSTANCE,
141+
new RenameIndexAction.Request(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, renamedOnceIndex, renamedTwiceIndex)
142+
).actionGet();
143+
144+
// Ensure gamma_index exists and is healthy
145+
ensureGreen(renamedTwiceIndex);
146+
147+
// Assert document is searchable in gamma_index, not in previous indices
148+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(renamedTwiceIndex), 1L);
149+
ElasticsearchAssertions.assertHitCount(
150+
client().prepareSearch(renamedOnceIndex).setIndicesOptions(IndicesOptions.lenientExpandOpen()),
151+
0L
152+
);
153+
ElasticsearchAssertions.assertHitCount(
154+
client().prepareSearch(initialIndex).setIndicesOptions(IndicesOptions.lenientExpandOpen()),
155+
0L
156+
);
157+
}
158+
159+
/**
160+
* Tests swapping the names of two indices via a series of renames using a temporary index name,
161+
* and verifies that documents are accessible under the swapped names.
162+
*/
163+
public void testSwapIndexNamesViaRenames() {
164+
String indexA = "index_a";
165+
String indexB = "index_b";
166+
String tempIndex = "temp_index";
167+
168+
// Create two indices and index a document in each
169+
createIndex(indexA);
170+
createIndex(indexB);
171+
172+
client().prepareIndex(indexA)
173+
.setSource("{ \"field\": \"A\" }", XContentType.JSON)
174+
.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)
175+
.get();
176+
client().prepareIndex(indexB)
177+
.setSource("{ \"field\": \"B\" }", XContentType.JSON)
178+
.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)
179+
.get();
180+
181+
// Initial assertions
182+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(indexA), 1L);
183+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(indexB), 1L);
184+
185+
// Rename indexA to tempIndex
186+
client().execute(
187+
RenameIndexAction.INSTANCE,
188+
new RenameIndexAction.Request(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, indexA, tempIndex)
189+
).actionGet();
190+
191+
// After first rename
192+
ensureGreen(tempIndex, indexB);
193+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(tempIndex), 1L);
194+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(indexB), 1L);
195+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(indexA).setIndicesOptions(IndicesOptions.lenientExpandOpen()), 0L);
196+
197+
// Rename indexB to indexA
198+
client().execute(
199+
RenameIndexAction.INSTANCE,
200+
new RenameIndexAction.Request(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, indexB, indexA)
201+
).actionGet();
202+
203+
// After second rename
204+
ensureGreen(tempIndex, indexA);
205+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(tempIndex), 1L);
206+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(indexA), 1L);
207+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(indexB).setIndicesOptions(IndicesOptions.lenientExpandOpen()), 0L);
208+
209+
// Rename tempIndex to indexB
210+
client().execute(
211+
RenameIndexAction.INSTANCE,
212+
new RenameIndexAction.Request(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, tempIndex, indexB)
213+
).actionGet();
214+
215+
// Ensure indices exist and are healthy
216+
ensureGreen(indexA, indexB);
217+
218+
// Final assertions
219+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(indexA), 1L);
220+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(indexB), 1L);
221+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(tempIndex).setIndicesOptions(IndicesOptions.lenientExpandOpen()), 0L);
222+
}
223+
224+
/**
225+
* Tests renaming an index and then recreating a new index with the original name,
226+
* ensuring that documents are correctly separated between the renamed and newly created indices.
227+
*/
228+
public void testRenameAndRecreateOriginalIndex() {
229+
String original = "original_index";
230+
String renamed = "renamed_index";
231+
232+
// Create the original index and index a document
233+
createIndex(original);
234+
client().prepareIndex(original)
235+
.setSource("{ \"field\": \"value\" }", XContentType.JSON)
236+
.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)
237+
.get();
238+
239+
// Assert document is searchable in the original index
240+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(original), 1L);
241+
242+
// Rename the original index
243+
client().execute(
244+
RenameIndexAction.INSTANCE,
245+
new RenameIndexAction.Request(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT, original, renamed)
246+
).actionGet();
247+
248+
// Assert document is searchable in the renamed index, not in the original
249+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(renamed), 1L);
250+
ElasticsearchAssertions.assertHitCount(client().prepareSearch(original).setIndicesOptions(IndicesOptions.lenientExpandOpen()), 0L);
251+
252+
// Create a new index with the original name and index a new document
253+
createIndex(original);
254+
client().prepareIndex(original)
255+
.setSource("{ \"field\": \"new_value\" }", XContentType.JSON)
256+
.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE)
257+
.get();
258+
259+
// Assert the correct field values are returned for each index
260+
ElasticsearchAssertions.assertResponse(client().prepareSearch(renamed), response -> {
261+
assertEquals(1, response.getHits().getTotalHits().value());
262+
assertEquals(renamed, response.getHits().getAt(0).getIndex());
263+
assertEquals("value", response.getHits().getAt(0).getSourceAsMap().get("field"));
264+
});
265+
ElasticsearchAssertions.assertResponse(client().prepareSearch(original), response -> {
266+
assertEquals(1, response.getHits().getTotalHits().value());
267+
assertEquals(original, response.getHits().getAt(0).getIndex());
268+
assertEquals("new_value", response.getHits().getAt(0).getSourceAsMap().get("field"));
269+
});
270+
}
271+
}

server/src/main/java/org/elasticsearch/action/ActionModule.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@
122122
import org.elasticsearch.action.admin.indices.refresh.RefreshAction;
123123
import org.elasticsearch.action.admin.indices.refresh.TransportRefreshAction;
124124
import org.elasticsearch.action.admin.indices.refresh.TransportShardRefreshAction;
125+
import org.elasticsearch.action.admin.indices.rename.RenameIndexAction;
126+
import org.elasticsearch.action.admin.indices.rename.TransportRenameIndexAction;
125127
import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction;
126128
import org.elasticsearch.action.admin.indices.resolve.TransportResolveClusterAction;
127129
import org.elasticsearch.action.admin.indices.rollover.LazyRolloverAction;
@@ -717,6 +719,7 @@ public <Request extends ActionRequest, Response extends ActionResponse> void reg
717719
actions.register(TransportClearIndicesCacheAction.TYPE, TransportClearIndicesCacheAction.class);
718720
actions.register(GetAliasesAction.INSTANCE, TransportGetAliasesAction.class);
719721
actions.register(GetSettingsAction.INSTANCE, TransportGetSettingsAction.class);
722+
actions.register(RenameIndexAction.INSTANCE, TransportRenameIndexAction.class);
720723

721724
actions.register(TransportIndexAction.TYPE, TransportIndexAction.class);
722725
actions.register(TransportGetAction.TYPE, TransportGetAction.class);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
10+
package org.elasticsearch.action.admin.indices.rename;
11+
12+
import org.elasticsearch.action.ActionType;
13+
import org.elasticsearch.action.support.master.AcknowledgedRequest;
14+
import org.elasticsearch.action.support.master.AcknowledgedResponse;
15+
import org.elasticsearch.common.io.stream.StreamInput;
16+
import org.elasticsearch.common.io.stream.StreamOutput;
17+
import org.elasticsearch.core.TimeValue;
18+
19+
import java.io.IOException;
20+
21+
public class RenameIndexAction extends ActionType<AcknowledgedResponse> {
22+
23+
public static final String NAME = "indices:admin/rename";
24+
public static final RenameIndexAction INSTANCE = new RenameIndexAction();
25+
26+
public RenameIndexAction() {
27+
super(NAME);
28+
}
29+
30+
public static class Request extends AcknowledgedRequest<Request> {
31+
private final String sourceIndex;
32+
private final String destinationIndex;
33+
34+
public Request(TimeValue masterNodeTimeout, TimeValue ackTimeout, String sourceIndex, String destinationIndex) {
35+
super(masterNodeTimeout, ackTimeout);
36+
this.sourceIndex = sourceIndex;
37+
this.destinationIndex = destinationIndex;
38+
}
39+
40+
public Request(StreamInput in) throws IOException {
41+
super(in);
42+
this.sourceIndex = in.readString();
43+
this.destinationIndex = in.readString();
44+
}
45+
46+
@Override
47+
public void writeTo(StreamOutput out) throws IOException {
48+
super.writeTo(out);
49+
out.writeString(sourceIndex);
50+
out.writeString(destinationIndex);
51+
}
52+
53+
public String getSourceIndex() {
54+
return sourceIndex;
55+
}
56+
57+
public String getDestinationIndex() {
58+
return destinationIndex;
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)