Skip to content

Commit 8dba961

Browse files
nielsbaumangmarouli
authored andcommitted
Initial effort to atomically rename indices
1 parent 829fb56 commit 8dba961

File tree

21 files changed

+525
-36
lines changed

21 files changed

+525
-36
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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.alias.IndicesAliasesRequest;
13+
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesResponse;
14+
import org.elasticsearch.action.admin.indices.alias.TransportIndicesAliasesAction;
15+
import org.elasticsearch.action.support.IndicesOptions;
16+
import org.elasticsearch.action.support.WriteRequest;
17+
import org.elasticsearch.test.ESSingleNodeTestCase;
18+
import org.elasticsearch.xcontent.XContentType;
19+
20+
import static org.hamcrest.Matchers.equalTo;
21+
22+
public class IndexAliasRenameIT extends ESSingleNodeTestCase {
23+
private static final String ORIGINAL = "original";
24+
private static final String NEW_INDEX = "new_index";
25+
26+
public void testSimpleRename() {
27+
// Index a document, creating the original index
28+
createIndex(ORIGINAL);
29+
client().prepareIndex(ORIGINAL).setSource("""
30+
{
31+
"foo": "bar", "baz": 123
32+
}""", XContentType.JSON).setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE).get();
33+
34+
// Ensure the document is searchable in the original index
35+
assertThat(client().prepareSearch(ORIGINAL).get().getHits().getTotalHits().value(), equalTo(1L));
36+
assertThat(
37+
client().prepareSearch(NEW_INDEX).setIndicesOptions(IndicesOptions.lenientExpandOpen()).get().getHits().getTotalHits().value(),
38+
equalTo(0L)
39+
);
40+
41+
// Rename the index
42+
IndicesAliasesRequest request = new IndicesAliasesRequest(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT);
43+
request.addAliasAction(IndicesAliasesRequest.AliasActions.renameIndex().index(ORIGINAL).destination(NEW_INDEX));
44+
IndicesAliasesResponse response = client().execute(TransportIndicesAliasesAction.TYPE, request).actionGet();
45+
assertFalse(response.hasErrors());
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+
assertThat(
52+
client().prepareSearch(ORIGINAL).setIndicesOptions(IndicesOptions.lenientExpandOpen()).get().getHits().getTotalHits().value(),
53+
equalTo(0L)
54+
);
55+
assertThat(client().prepareSearch(NEW_INDEX).get().getHits().getTotalHits().value(), equalTo(1L));
56+
}
57+
}

server/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
requires org.apache.lucene.queryparser;
5555
requires org.apache.lucene.sandbox;
5656
requires org.apache.lucene.suggest;
57+
requires java.desktop;
5758

5859
exports org.elasticsearch;
5960
exports org.elasticsearch.action;

server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequest.java

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,14 @@ public static class AliasActions implements AliasesRequest, Writeable, ToXConten
109109
private static final ParseField ADD = new ParseField("add");
110110
private static final ParseField REMOVE = new ParseField("remove");
111111
private static final ParseField REMOVE_INDEX = new ParseField("remove_index");
112+
private static final ParseField RENAME_INDEX = new ParseField("rename_index");
113+
private static final ParseField DESTINATION = new ParseField("destination");
112114

113115
public enum Type {
114116
ADD((byte) 0, AliasActions.ADD),
115117
REMOVE((byte) 1, AliasActions.REMOVE),
116-
REMOVE_INDEX((byte) 2, AliasActions.REMOVE_INDEX);
118+
REMOVE_INDEX((byte) 2, AliasActions.REMOVE_INDEX),
119+
RENAME_INDEX((byte) 3, AliasActions.RENAME_INDEX);
117120

118121
private final byte value;
119122
private final String fieldName;
@@ -136,6 +139,7 @@ public static Type fromValue(byte value) {
136139
case 0 -> ADD;
137140
case 1 -> REMOVE;
138141
case 2 -> REMOVE_INDEX;
142+
case 3 -> RENAME_INDEX;
139143
default -> throw new IllegalArgumentException("No type for action [" + value + "]");
140144
};
141145
}
@@ -162,6 +166,13 @@ public static AliasActions removeIndex() {
162166
return new AliasActions(AliasActions.Type.REMOVE_INDEX);
163167
}
164168

169+
/**
170+
* Build a new {@link AliasAction} to rename an index.
171+
*/
172+
public static AliasActions renameIndex() {
173+
return new AliasActions(AliasActions.Type.RENAME_INDEX);
174+
}
175+
165176
private static ObjectParser<AliasActions, Factory> parser(String name, Supplier<AliasActions> supplier) {
166177
ObjectParser<AliasActions, Factory> parser = new ObjectParser<>(name, supplier);
167178
parser.declareString((action, index) -> {
@@ -197,6 +208,10 @@ private static ObjectParser<AliasActions, Factory> parser(String name, Supplier<
197208
REMOVE_INDEX.getPreferredName(),
198209
AliasActions::removeIndex
199210
);
211+
private static final ObjectParser<AliasActions, Factory> RENAME_INDEX_PARSER = parser(
212+
RENAME_INDEX.getPreferredName(),
213+
AliasActions::renameIndex
214+
);
200215
static {
201216
ADD_PARSER.declareObject(AliasActions::filter, (parser, m) -> {
202217
try {
@@ -212,6 +227,7 @@ private static ObjectParser<AliasActions, Factory> parser(String name, Supplier<
212227
ADD_PARSER.declareField(AliasActions::writeIndex, XContentParser::booleanValue, IS_WRITE_INDEX, ValueType.BOOLEAN);
213228
ADD_PARSER.declareField(AliasActions::isHidden, XContentParser::booleanValue, IS_HIDDEN, ValueType.BOOLEAN);
214229
REMOVE_PARSER.declareField(AliasActions::mustExist, XContentParser::booleanValue, MUST_EXIST, ValueType.BOOLEAN);
230+
RENAME_INDEX_PARSER.declareField(AliasActions::destination, XContentParser::text, DESTINATION, ValueType.STRING);
215231
}
216232

217233
/**
@@ -235,6 +251,7 @@ private static ObjectParser<AliasActions, Factory> parser(String name, Supplier<
235251
PARSER.declareObject(optionalConstructorArg(), ADD_PARSER, ADD);
236252
PARSER.declareObject(optionalConstructorArg(), REMOVE_PARSER, REMOVE);
237253
PARSER.declareObject(optionalConstructorArg(), REMOVE_INDEX_PARSER, REMOVE_INDEX);
254+
PARSER.declareObject(optionalConstructorArg(), RENAME_INDEX_PARSER, RENAME_INDEX);
238255
}
239256

240257
private final AliasActions.Type type;
@@ -245,6 +262,7 @@ private static ObjectParser<AliasActions, Factory> parser(String name, Supplier<
245262
private String routing;
246263
private String indexRouting;
247264
private String searchRouting;
265+
private String destination;
248266
private Boolean writeIndex;
249267
private Boolean isHidden;
250268
private Boolean mustExist;
@@ -268,6 +286,8 @@ public AliasActions(StreamInput in) throws IOException {
268286
isHidden = in.readOptionalBoolean();
269287
originalAliases = in.readStringArray();
270288
mustExist = in.readOptionalBoolean();
289+
// TODO: NOCOMMIT: protect with transport version
290+
destination = in.readOptionalString();
271291
}
272292

273293
@Override
@@ -283,6 +303,8 @@ public void writeTo(StreamOutput out) throws IOException {
283303
out.writeOptionalBoolean(isHidden);
284304
out.writeStringArray(originalAliases);
285305
out.writeOptionalBoolean(mustExist);
306+
// TODO: NOCOMMIT: protect with transport version
307+
out.writeOptionalString(destination);
286308
}
287309

288310
/**
@@ -293,7 +315,8 @@ void validate() {
293315
if (indices == null) {
294316
throw new IllegalArgumentException("One of [index] or [indices] is required");
295317
}
296-
if (type != AliasActions.Type.REMOVE_INDEX && (aliases == null || aliases.length == 0)) {
318+
if ((type != AliasActions.Type.REMOVE_INDEX && type != AliasActions.Type.RENAME_INDEX)
319+
&& (aliases == null || aliases.length == 0)) {
297320
throw new IllegalArgumentException("One of [alias] or [aliases] is required");
298321
}
299322
}
@@ -483,6 +506,18 @@ public Boolean mustExist() {
483506
return mustExist;
484507
}
485508

509+
public AliasActions destination(String destination) {
510+
if (type != Type.RENAME_INDEX) {
511+
throw new IllegalArgumentException("[" + DESTINATION.getPreferredName() + "] is unsupported for [" + type + "]");
512+
}
513+
this.destination = destination;
514+
return this;
515+
}
516+
517+
public String destination() {
518+
return destination;
519+
}
520+
486521
@Override
487522
public String[] aliases() {
488523
return aliases;
@@ -552,6 +587,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
552587
if (null != mustExist) {
553588
builder.field(MUST_EXIST.getPreferredName(), mustExist);
554589
}
590+
if (destination != null) {
591+
builder.field(DESTINATION.getPreferredName(), destination);
592+
}
555593
builder.endObject();
556594
builder.endObject();
557595
return builder;
@@ -584,6 +622,8 @@ public String toString() {
584622
+ isHidden
585623
+ ",mustExist="
586624
+ mustExist
625+
+ ",destination="
626+
+ destination
587627
+ "]";
588628
}
589629

@@ -603,7 +643,8 @@ public boolean equals(Object obj) {
603643
&& Objects.equals(searchRouting, other.searchRouting)
604644
&& Objects.equals(writeIndex, other.writeIndex)
605645
&& Objects.equals(isHidden, other.isHidden)
606-
&& Objects.equals(mustExist, other.mustExist);
646+
&& Objects.equals(mustExist, other.mustExist)
647+
&& Objects.equals(destination, other.destination);
607648
}
608649

609650
@Override
@@ -618,7 +659,8 @@ public int hashCode() {
618659
searchRouting,
619660
writeIndex,
620661
isHidden,
621-
mustExist
662+
mustExist,
663+
destination
622664
);
623665
}
624666
}

server/src/main/java/org/elasticsearch/action/admin/indices/alias/TransportIndicesAliasesAction.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ protected void masterOperation(
254254
case REMOVE_INDEX:
255255
finalActions.add(new AliasAction.RemoveIndex(index.getName()));
256256
break;
257+
case RENAME_INDEX:
258+
finalActions.add(new AliasAction.RenameIndex(index.getName(), action.destination()));
259+
break;
257260
default:
258261
throw new IllegalArgumentException("Unsupported action [" + action.actionType() + "]");
259262
}

server/src/main/java/org/elasticsearch/cluster/metadata/AliasAction.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
1313
import org.elasticsearch.common.Strings;
1414
import org.elasticsearch.core.Nullable;
15+
import org.elasticsearch.core.Tuple;
1516
import org.elasticsearch.rest.action.admin.indices.AliasesNotFoundException;
1617

18+
import java.util.Map;
19+
1720
/**
1821
* Individual operation to perform on the cluster state as part of an {@link IndicesAliasesRequest}.
1922
*/
@@ -40,6 +43,13 @@ public String getIndex() {
4043
*/
4144
abstract boolean removeIndex();
4245

46+
/**
47+
* TODO: document
48+
*/
49+
Tuple<String, String> rename() {
50+
return null;
51+
}
52+
4353
/**
4454
* Apply the action.
4555
*
@@ -219,6 +229,40 @@ boolean apply(NewAliasValidator aliasValidator, ProjectMetadata.Builder metadata
219229
}
220230
}
221231

232+
/**
233+
* TODO: document
234+
*/
235+
public static class RenameIndex extends AliasAction {
236+
private final String destination;
237+
238+
public RenameIndex(String index, String destination) {
239+
super(index);
240+
this.destination = destination;
241+
}
242+
243+
@Override
244+
boolean removeIndex() {
245+
return false;
246+
}
247+
248+
@Override
249+
Tuple<String, String> rename() {
250+
return new Tuple<>(getIndex(), destination);
251+
}
252+
253+
@Override
254+
boolean apply(NewAliasValidator aliasValidator, ProjectMetadata.Builder metadata, IndexMetadata index) {
255+
metadata.put(
256+
IndexMetadata.builder(index)
257+
.putCustom(
258+
MetadataIndexAliasesService.CUSTOM_RENAME_METADATA_KEY,
259+
Map.of("original_name", getIndex(), "new_name", destination)
260+
)
261+
);
262+
return true;
263+
}
264+
}
265+
222266
public static class AddDataStreamAlias extends AliasAction {
223267

224268
private final String aliasName;

server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexAliasesService.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
*/
5656
public class MetadataIndexAliasesService {
5757

58+
public static final String CUSTOM_RENAME_METADATA_KEY = "index_rename";
59+
5860
private final IndicesService indicesService;
5961

6062
private final NamedXContentRegistry xContentRegistry;
@@ -129,11 +131,16 @@ public ClusterState applyAliasActions(ProjectState projectState, Iterable<AliasA
129131
ProjectMetadata.Builder metadata = ProjectMetadata.builder(currentProjectMetadata);
130132
// Run the remaining alias actions
131133
final Set<String> maybeModifiedIndices = new HashSet<>();
134+
final Map<String, String> renamedIndices = new HashMap<>();
132135
for (AliasAction action : actions) {
133136
if (action.removeIndex()) {
134137
// Handled above
135138
continue;
136139
}
140+
final Tuple<String, String> destination = action.rename();
141+
if (destination != null) {
142+
renamedIndices.put(destination.v1(), destination.v2());
143+
}
137144

138145
/* It is important that we look up the index using the metadata builder we are modifying so we can remove an
139146
* index and replace it with an alias. */
@@ -193,17 +200,36 @@ public ClusterState applyAliasActions(ProjectState projectState, Iterable<AliasA
193200
final IndexMetadata currentIndexMetadata = currentProjectMetadata.index(maybeModifiedIndex);
194201
final IndexMetadata newIndexMetadata = metadata.get(maybeModifiedIndex);
195202
// only increment the aliases version if the aliases actually changed for this index
196-
if (currentIndexMetadata.getAliases().equals(newIndexMetadata.getAliases()) == false) {
203+
if (newIndexMetadata != null && currentIndexMetadata.getAliases().equals(newIndexMetadata.getAliases()) == false) {
197204
assert currentIndexMetadata.getAliasesVersion() == newIndexMetadata.getAliasesVersion();
198205
metadata.put(new IndexMetadata.Builder(newIndexMetadata).aliasesVersion(1 + currentIndexMetadata.getAliasesVersion()));
199206
}
200207
}
201208

202209
if (changed) {
203210
ProjectMetadata updatedMetadata = metadata.build();
211+
212+
// TODO: move this rename logic to a service that looks at the custom map in the IndexMetadata and does the right thing
213+
// RoutingTable existingRoutingTable = currentState.routingTable();
214+
// RoutingTable.Builder rtBuilder = RoutingTable.builder(existingRoutingTable);
215+
// for (Map.Entry<String, String> renamedIndex : renamedIndices.entrySet()) {
216+
// final String oldName = renamedIndex.getKey();
217+
// final String newName = renamedIndex.getValue();
218+
// System.out.println("--> updating routing table for " + oldName + " ==> " + newName);
219+
// IndexRoutingTable indexTable = existingRoutingTable.index(oldName);
220+
// IndexMetadata im = updatedState.getMetadata().index(newName);
221+
// System.out.println("--> building a new routing table for " + im.getIndex());
222+
// IndexRoutingTable.Builder tableBuilder = IndexRoutingTable.builder(im.getIndex());
223+
// for (ShardRouting sr : indexTable.randomAllActiveShardsIt()) {
224+
// tableBuilder.addShard(sr.updateIndex(im.getIndex()));
225+
// }
226+
// rtBuilder.add(tableBuilder);
227+
// rtBuilder.remove(oldName);
228+
// updatedState = ClusterState.builder(updatedState).routingTable(rtBuilder).build();
229+
// }
204230
// even though changes happened, they resulted in 0 actual changes to metadata
205231
// i.e. remove and add the same alias to the same index
206-
if (updatedMetadata.equalsAliases(currentProjectMetadata) == false) {
232+
if (renamedIndices.isEmpty() == false || updatedMetadata.equalsAliases(currentProjectMetadata) == false) {
207233
return ClusterState.builder(currentState).putProjectMetadata(updatedMetadata).build();
208234
}
209235
}

server/src/main/java/org/elasticsearch/cluster/routing/IndexShardRoutingTable.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,12 @@ private static List<ShardRouting> rankShardsAndUpdateStats(
363363
return sortedShards;
364364
}
365365

366+
public Builder rename(Index newIndex) {
367+
var renamedShardId = new ShardId(newIndex, shardId.id());
368+
var renameShardRoutings = Arrays.stream(shards).map(shardRouting -> shardRouting.updateIndex(newIndex)).toList();
369+
return new Builder(renamedShardId, renameShardRoutings);
370+
}
371+
366372
private static class NodeRankComparator implements Comparator<ShardRouting> {
367373
private final Map<String, Double> nodeRanks;
368374

@@ -595,6 +601,11 @@ public Builder(IndexShardRoutingTable indexShard) {
595601
Collections.addAll(this.shards, indexShard.shards);
596602
}
597603

604+
public Builder(ShardId shardId, List<ShardRouting> shards) {
605+
this.shardId = shardId;
606+
this.shards = new ArrayList<>(shards);
607+
}
608+
598609
public ShardId shardId() {
599610
return shardId;
600611
}

0 commit comments

Comments
 (0)