Browse Source

add is-write-index flag to aliases (#30942)

This commit adds the is-write-index flag for aliases.
It allows requests to set the flag, and responses to display the flag.
It does not validate and/or affect any indexing/getting/updating behavior
of Elasticsearch -- this will be done in a follow-up PR.
Tal Levy 7 years ago
parent
commit
3b70e943eb
24 changed files with 524 additions and 40 deletions
  1. 88 0
      docs/reference/indices/aliases.asciidoc
  2. 24 1
      rest-api-spec/src/main/resources/rest-api-spec/test/indices.create/10_basic.yml
  3. 34 0
      server/src/main/java/org/elasticsearch/action/admin/indices/alias/Alias.java
  4. 22 0
      server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequest.java
  5. 12 0
      server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequestBuilder.java
  6. 2 1
      server/src/main/java/org/elasticsearch/action/admin/indices/alias/TransportIndicesAliasesAction.java
  7. 1 1
      server/src/main/java/org/elasticsearch/action/admin/indices/rollover/TransportRolloverAction.java
  8. 18 6
      server/src/main/java/org/elasticsearch/cluster/metadata/AliasAction.java
  9. 41 3
      server/src/main/java/org/elasticsearch/cluster/metadata/AliasMetaData.java
  10. 30 0
      server/src/main/java/org/elasticsearch/cluster/metadata/AliasOrIndex.java
  11. 2 2
      server/src/main/java/org/elasticsearch/cluster/metadata/AliasValidator.java
  12. 19 12
      server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java
  13. 1 1
      server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java
  14. 1 1
      server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesService.java
  15. 3 0
      server/src/test/java/org/elasticsearch/action/admin/indices/alias/AliasActionsTests.java
  16. 2 1
      server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestTests.java
  17. 2 1
      server/src/test/java/org/elasticsearch/action/admin/indices/shrink/ResizeRequestTests.java
  18. 17 0
      server/src/test/java/org/elasticsearch/cluster/metadata/AliasMetaDataTests.java
  19. 27 0
      server/src/test/java/org/elasticsearch/cluster/metadata/IndexCreationTaskTests.java
  20. 130 4
      server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesServiceTests.java
  21. 35 0
      server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataTests.java
  22. 6 3
      server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetaDataTests.java
  23. 3 3
      server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java
  24. 4 0
      test/framework/src/main/java/org/elasticsearch/index/RandomCreateIndexGenerator.java

+ 88 - 0
docs/reference/indices/aliases.asciidoc

@@ -244,6 +244,94 @@ GET /alias2/_search?q=user:kimchy&routing=2,3
 // CONSOLE
 // TEST[continued]
 
+[float]
+[[aliases-write-index]]
+==== Write Index
+
+It is possible to associate the index pointed to by an alias as the write index.
+When specified, all index and update requests against an alias that point to multiple
+indices will attempt to resolve to the one index that is the write index.
+Only one index per alias can be assigned to be the write index at a time. If no write index is specified
+and there are multiple indices referenced by an alias, then writes will not be allowed.
+
+It is possible to specify an index associated with an alias as a write index using both the aliases API
+and index creation API.
+
+[source,js]
+--------------------------------------------------
+POST /_aliases
+{
+    "actions" : [
+        {
+            "add" : {
+                 "index" : "test",
+                 "alias" : "alias1",
+                 "is_write_index" : true
+            }
+        }
+    ]
+}
+--------------------------------------------------
+// CONSOLE
+// TEST[s/^/PUT test\n/]
+
+In this example, we associate the alias `alias1` to both `test` and `test2`, where
+`test` will be the index chosen for writing to.
+
+[source,js]
+--------------------------------------------------
+PUT /alias1/_doc/1
+{
+    "foo": "bar"
+}
+--------------------------------------------------
+// CONSOLE
+// TEST[continued]
+
+The new document that was indexed to `/alias1/_doc/1` will be indexed as if it were
+`/test/_doc/1`.
+
+[source,js]
+--------------------------------------------------
+GET /test/_doc/1
+--------------------------------------------------
+// CONSOLE
+// TEST[continued]
+
+To swap which index is the write index for an alias, the Aliases API can be leveraged to
+do an atomic swap. The swap is not dependent on the ordering of the actions.
+
+[source,js]
+--------------------------------------------------
+POST /_aliases
+{
+    "actions" : [
+        {
+            "add" : {
+                 "index" : "test",
+                 "alias" : "alias1",
+                 "is_write_index" : true
+            }
+        }, {
+            "add" : {
+                 "index" : "test2",
+                 "alias" : "alias1",
+                 "is_write_index" : false
+            }
+        }
+    ]
+}
+--------------------------------------------------
+// CONSOLE
+// TEST[s/^/PUT test\nPUT test2\n/]
+
+[IMPORTANT]
+=====================================
+Aliases that do not explicitly set `is_write_index: true` for an index, and
+only reference one index, will have that referenced index behave as if it is the write index
+until an additional index is referenced. At that point, there will be no write index and
+writes will be rejected.
+=====================================
 
 [float]
 [[alias-adding]]

+ 24 - 1
rest-api-spec/src/main/resources/rest-api-spec/test/indices.create/10_basic.yml

@@ -79,7 +79,6 @@
       indices.get_alias:
         index: test_index
 
-  - match: {test_index.aliases.test_alias: {}}
   - match: {test_index.aliases.test_blias.search_routing: b}
   - match: {test_index.aliases.test_blias.index_routing: b}
   - is_false: test_index.aliases.test_blias.filter
@@ -87,6 +86,30 @@
   - is_false: test_index.aliases.test_clias.index_routing
   - is_false: test_index.aliases.test_clias.search_routing
 
+---
+"Create index with write aliases":
+  - skip:
+      version: " - 6.99.99"
+      reason: is_write_index is not implemented in ES <= 6.x
+  - do:
+      indices.create:
+        index: test_index
+        body:
+          aliases:
+            test_alias: {}
+            test_blias:
+              is_write_index: false
+            test_clias:
+              is_write_index: true
+
+  - do:
+      indices.get_alias:
+        index: test_index
+
+  - is_false: test_index.aliases.test_alias.is_write_index
+  - is_false: test_index.aliases.test_blias.is_write_index
+  - is_true: test_index.aliases.test_clias.is_write_index
+
 ---
 "Create index with no type mappings":
   - do:

+ 34 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/alias/Alias.java

@@ -20,6 +20,7 @@
 package org.elasticsearch.action.admin.indices.alias;
 
 import org.elasticsearch.ElasticsearchGenerationException;
+import org.elasticsearch.Version;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.ParseField;
 import org.elasticsearch.common.Strings;
@@ -49,6 +50,7 @@ public class Alias implements Streamable, ToXContentFragment {
     private static final ParseField ROUTING = new ParseField("routing");
     private static final ParseField INDEX_ROUTING = new ParseField("index_routing", "indexRouting", "index-routing");
     private static final ParseField SEARCH_ROUTING = new ParseField("search_routing", "searchRouting", "search-routing");
+    private static final ParseField IS_WRITE_INDEX = new ParseField("is_write_index");
 
     private String name;
 
@@ -61,6 +63,9 @@ public class Alias implements Streamable, ToXContentFragment {
     @Nullable
     private String searchRouting;
 
+    @Nullable
+    private Boolean writeIndex;
+
     private Alias() {
 
     }
@@ -167,6 +172,21 @@ public class Alias implements Streamable, ToXContentFragment {
         return this;
     }
 
+    /**
+     * @return the write index flag for the alias
+     */
+    public Boolean writeIndex() {
+        return writeIndex;
+    }
+
+    /**
+     *  Sets whether an alias is pointing to a write-index
+     */
+    public Alias writeIndex(@Nullable Boolean writeIndex) {
+        this.writeIndex = writeIndex;
+        return this;
+    }
+
     /**
      * Allows to read an alias from the provided input stream
      */
@@ -182,6 +202,11 @@ public class Alias implements Streamable, ToXContentFragment {
         filter = in.readOptionalString();
         indexRouting = in.readOptionalString();
         searchRouting = in.readOptionalString();
+        if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
+            writeIndex = in.readOptionalBoolean();
+        } else {
+            writeIndex = null;
+        }
     }
 
     @Override
@@ -190,6 +215,9 @@ public class Alias implements Streamable, ToXContentFragment {
         out.writeOptionalString(filter);
         out.writeOptionalString(indexRouting);
         out.writeOptionalString(searchRouting);
+        if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
+            out.writeOptionalBoolean(writeIndex);
+        }
     }
 
     /**
@@ -219,6 +247,10 @@ public class Alias implements Streamable, ToXContentFragment {
                 } else if (SEARCH_ROUTING.match(currentFieldName, parser.getDeprecationHandler())) {
                     alias.searchRouting(parser.text());
                 }
+            } else if (token == XContentParser.Token.VALUE_BOOLEAN) {
+                if (IS_WRITE_INDEX.match(currentFieldName, parser.getDeprecationHandler())) {
+                    alias.writeIndex(parser.booleanValue());
+                }
             }
         }
         return alias;
@@ -245,6 +277,8 @@ public class Alias implements Streamable, ToXContentFragment {
             }
         }
 
+        builder.field(IS_WRITE_INDEX.getPreferredName(), writeIndex);
+
         builder.endObject();
         return builder;
     }

+ 22 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequest.java

@@ -20,6 +20,7 @@
 package org.elasticsearch.action.admin.indices.alias;
 
 import org.elasticsearch.ElasticsearchGenerationException;
+import org.elasticsearch.Version;
 import org.elasticsearch.action.ActionRequestValidationException;
 import org.elasticsearch.action.AliasesRequest;
 import org.elasticsearch.action.support.IndicesOptions;
@@ -84,6 +85,7 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
         private static final ParseField ROUTING = new ParseField("routing");
         private static final ParseField INDEX_ROUTING = new ParseField("index_routing", "indexRouting", "index-routing");
         private static final ParseField SEARCH_ROUTING = new ParseField("search_routing", "searchRouting", "search-routing");
+        private static final ParseField IS_WRITE_INDEX = new ParseField("is_write_index");
 
         private static final ParseField ADD = new ParseField("add");
         private static final ParseField REMOVE = new ParseField("remove");
@@ -179,6 +181,7 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
             ADD_PARSER.declareField(AliasActions::routing, XContentParser::text, ROUTING, ValueType.INT);
             ADD_PARSER.declareField(AliasActions::indexRouting, XContentParser::text, INDEX_ROUTING, ValueType.INT);
             ADD_PARSER.declareField(AliasActions::searchRouting, XContentParser::text, SEARCH_ROUTING, ValueType.INT);
+            ADD_PARSER.declareField(AliasActions::writeIndex, XContentParser::booleanValue, IS_WRITE_INDEX, ValueType.BOOLEAN);
         }
         private static final ObjectParser<AliasActions, Void> REMOVE_PARSER = parser(REMOVE.getPreferredName(), AliasActions::remove);
         private static final ObjectParser<AliasActions, Void> REMOVE_INDEX_PARSER = parser(REMOVE_INDEX.getPreferredName(),
@@ -215,6 +218,7 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
         private String routing;
         private String indexRouting;
         private String searchRouting;
+        private Boolean writeIndex;
 
         public AliasActions(AliasActions.Type type) {
             this.type = type;
@@ -231,6 +235,9 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
             routing = in.readOptionalString();
             searchRouting = in.readOptionalString();
             indexRouting = in.readOptionalString();
+            if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
+                writeIndex = in.readOptionalBoolean();
+            }
         }
 
         @Override
@@ -242,6 +249,9 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
             out.writeOptionalString(routing);
             out.writeOptionalString(searchRouting);
             out.writeOptionalString(indexRouting);
+            if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
+                out.writeOptionalBoolean(writeIndex);
+            }
         }
 
         /**
@@ -401,6 +411,18 @@ public class IndicesAliasesRequest extends AcknowledgedRequest<IndicesAliasesReq
             }
         }
 
+        public AliasActions writeIndex(Boolean writeIndex) {
+            if (type != AliasActions.Type.ADD) {
+                throw new IllegalArgumentException("[is_write_index] is unsupported for [" + type + "]");
+            }
+            this.writeIndex = writeIndex;
+            return this;
+        }
+
+        public Boolean writeIndex() {
+            return writeIndex;
+        }
+
         @Override
         public String[] aliases() {
             return aliases;

+ 12 - 0
server/src/main/java/org/elasticsearch/action/admin/indices/alias/IndicesAliasesRequestBuilder.java

@@ -130,6 +130,18 @@ public class IndicesAliasesRequestBuilder
         return this;
     }
 
+    /**
+     * Adds an alias to the index.
+     *
+     * @param index         The index
+     * @param alias         The alias
+     * @param writeIndex    write index flag
+     */
+    public IndicesAliasesRequestBuilder addAlias(String index, String alias, boolean writeIndex) {
+        request.addAliasAction(AliasActions.add().index(index).alias(alias).writeIndex(writeIndex));
+        return this;
+    }
+
     /**
      * Removes an alias from the index.
      *

+ 2 - 1
server/src/main/java/org/elasticsearch/action/admin/indices/alias/TransportIndicesAliasesAction.java

@@ -100,7 +100,8 @@ public class TransportIndicesAliasesAction extends TransportMasterNodeAction<Ind
                 switch (action.actionType()) {
                 case ADD:
                     for (String alias : concreteAliases(action, state.metaData(), index)) {
-                        finalActions.add(new AliasAction.Add(index, alias, action.filter(), action.indexRouting(), action.searchRouting()));
+                        finalActions.add(new AliasAction.Add(index, alias, action.filter(), action.indexRouting(),
+                            action.searchRouting(), action.writeIndex()));
                     }
                     break;
                 case REMOVE:

+ 1 - 1
server/src/main/java/org/elasticsearch/action/admin/indices/rollover/TransportRolloverAction.java

@@ -196,7 +196,7 @@ public class TransportRolloverAction extends TransportMasterNodeAction<RolloverR
     static IndicesAliasesClusterStateUpdateRequest prepareRolloverAliasesUpdateRequest(String oldIndex, String newIndex,
                                                                                        RolloverRequest request) {
         List<AliasAction> actions = unmodifiableList(Arrays.asList(
-                new AliasAction.Add(newIndex, request.getAlias(), null, null, null),
+                new AliasAction.Add(newIndex, request.getAlias(), null, null, null, null),
                 new AliasAction.Remove(oldIndex, request.getAlias())));
         final IndicesAliasesClusterStateUpdateRequest updateRequest = new IndicesAliasesClusterStateUpdateRequest(actions)
             .ackTimeout(request.ackTimeout())

+ 18 - 6
server/src/main/java/org/elasticsearch/cluster/metadata/AliasAction.java

@@ -51,7 +51,7 @@ public abstract class AliasAction {
 
     /**
      * Apply the action.
-     * 
+     *
      * @param aliasValidator call to validate a new alias before adding it to the builder
      * @param metadata metadata builder for the changes made by all actions as part of this request
      * @param index metadata for the index being changed
@@ -64,7 +64,7 @@ public abstract class AliasAction {
      */
     @FunctionalInterface
     public interface NewAliasValidator {
-        void validate(String alias, @Nullable String indexRouting, @Nullable String filter);
+        void validate(String alias, @Nullable String indexRouting, @Nullable String filter, @Nullable Boolean writeIndex);
     }
 
     /**
@@ -82,10 +82,14 @@ public abstract class AliasAction {
         @Nullable
         private final String searchRouting;
 
+        @Nullable
+        private final Boolean writeIndex;
+
         /**
          * Build the operation.
          */
-        public Add(String index, String alias, @Nullable String filter, @Nullable String indexRouting, @Nullable String searchRouting) {
+        public Add(String index, String alias, @Nullable String filter, @Nullable String indexRouting,
+                   @Nullable String searchRouting, @Nullable Boolean writeIndex) {
             super(index);
             if (false == Strings.hasText(alias)) {
                 throw new IllegalArgumentException("[alias] is required");
@@ -94,6 +98,7 @@ public abstract class AliasAction {
             this.filter = filter;
             this.indexRouting = indexRouting;
             this.searchRouting = searchRouting;
+            this.writeIndex = writeIndex;
         }
 
         /**
@@ -103,6 +108,10 @@ public abstract class AliasAction {
             return alias;
         }
 
+        public Boolean writeIndex() {
+            return writeIndex;
+        }
+
         @Override
         boolean removeIndex() {
             return false;
@@ -110,15 +119,18 @@ public abstract class AliasAction {
 
         @Override
         boolean apply(NewAliasValidator aliasValidator, MetaData.Builder metadata, IndexMetaData index) {
-            aliasValidator.validate(alias, indexRouting, filter);
+            aliasValidator.validate(alias, indexRouting, filter, writeIndex);
+
             AliasMetaData newAliasMd = AliasMetaData.newAliasMetaDataBuilder(alias).filter(filter).indexRouting(indexRouting)
-                    .searchRouting(searchRouting).build();
+                    .searchRouting(searchRouting).writeIndex(writeIndex).build();
+
             // Check if this alias already exists
             AliasMetaData currentAliasMd = index.getAliases().get(alias);
             if (currentAliasMd != null && currentAliasMd.equals(newAliasMd)) {
                 // It already exists, ignore it
                 return false;
             }
+
             metadata.put(IndexMetaData.builder(index).putAlias(newAliasMd));
             return true;
         }
@@ -182,4 +194,4 @@ public abstract class AliasAction {
             throw new UnsupportedOperationException();
         }
     }
-}
+}

+ 41 - 3
server/src/main/java/org/elasticsearch/cluster/metadata/AliasMetaData.java

@@ -20,8 +20,10 @@
 package org.elasticsearch.cluster.metadata;
 
 import org.elasticsearch.ElasticsearchGenerationException;
+import org.elasticsearch.Version;
 import org.elasticsearch.cluster.AbstractDiffable;
 import org.elasticsearch.cluster.Diff;
+import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.bytes.BytesArray;
 import org.elasticsearch.common.bytes.BytesReference;
@@ -55,7 +57,10 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
 
     private final Set<String> searchRoutingValues;
 
-    private AliasMetaData(String alias, CompressedXContent filter, String indexRouting, String searchRouting) {
+    @Nullable
+    private final Boolean writeIndex;
+
+    private AliasMetaData(String alias, CompressedXContent filter, String indexRouting, String searchRouting, Boolean writeIndex) {
         this.alias = alias;
         this.filter = filter;
         this.indexRouting = indexRouting;
@@ -65,10 +70,11 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
         } else {
             searchRoutingValues = emptySet();
         }
+        this.writeIndex = writeIndex;
     }
 
     private AliasMetaData(AliasMetaData aliasMetaData, String alias) {
-        this(alias, aliasMetaData.filter(), aliasMetaData.indexRouting(), aliasMetaData.searchRouting());
+        this(alias, aliasMetaData.filter(), aliasMetaData.indexRouting(), aliasMetaData.searchRouting(), aliasMetaData.writeIndex());
     }
 
     public String alias() {
@@ -111,6 +117,10 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
         return searchRoutingValues;
     }
 
+    public Boolean writeIndex() {
+        return writeIndex;
+    }
+
     public static Builder builder(String alias) {
         return new Builder(alias);
     }
@@ -138,6 +148,8 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
         if (indexRouting != null ? !indexRouting.equals(that.indexRouting) : that.indexRouting != null) return false;
         if (searchRouting != null ? !searchRouting.equals(that.searchRouting) : that.searchRouting != null)
             return false;
+        if (writeIndex != null ? writeIndex != that.writeIndex : that.writeIndex != null)
+            return false;
 
         return true;
     }
@@ -148,6 +160,7 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
         result = 31 * result + (filter != null ? filter.hashCode() : 0);
         result = 31 * result + (indexRouting != null ? indexRouting.hashCode() : 0);
         result = 31 * result + (searchRouting != null ? searchRouting.hashCode() : 0);
+        result = 31 * result + (writeIndex != null ? writeIndex.hashCode() : 0);
         return result;
     }
 
@@ -173,6 +186,9 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
             out.writeBoolean(false);
         }
 
+        if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
+            out.writeOptionalBoolean(writeIndex());
+        }
     }
 
     public AliasMetaData(StreamInput in) throws IOException {
@@ -194,6 +210,11 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
             searchRouting = null;
             searchRoutingValues = emptySet();
         }
+        if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
+            writeIndex = in.readOptionalBoolean();
+        } else {
+            writeIndex = null;
+        }
     }
 
     public static Diff<AliasMetaData> readDiffFrom(StreamInput in) throws IOException {
@@ -221,6 +242,9 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
 
         private String searchRouting;
 
+        @Nullable
+        private Boolean writeIndex;
+
 
         public Builder(String alias) {
             this.alias = alias;
@@ -231,6 +255,7 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
             filter = aliasMetaData.filter();
             indexRouting = aliasMetaData.indexRouting();
             searchRouting = aliasMetaData.searchRouting();
+            writeIndex = aliasMetaData.writeIndex();
         }
 
         public String alias() {
@@ -284,8 +309,13 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
             return this;
         }
 
+        public Builder writeIndex(@Nullable Boolean writeIndex) {
+            this.writeIndex = writeIndex;
+            return this;
+        }
+
         public AliasMetaData build() {
-            return new AliasMetaData(alias, filter, indexRouting, searchRouting);
+            return new AliasMetaData(alias, filter, indexRouting, searchRouting, writeIndex);
         }
 
         public static void toXContent(AliasMetaData aliasMetaData, XContentBuilder builder, ToXContent.Params params) throws IOException {
@@ -307,6 +337,10 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
                 builder.field("search_routing", aliasMetaData.searchRouting());
             }
 
+            if (aliasMetaData.writeIndex() != null) {
+                builder.field("is_write_index", aliasMetaData.writeIndex());
+            }
+
             builder.endObject();
         }
 
@@ -343,6 +377,10 @@ public class AliasMetaData extends AbstractDiffable<AliasMetaData> implements To
                     }
                 } else if (token == XContentParser.Token.START_ARRAY) {
                     parser.skipChildren();
+                } else if (token == XContentParser.Token.VALUE_BOOLEAN) {
+                    if ("is_write_index".equals(currentFieldName)) {
+                        builder.writeIndex(parser.booleanValue());
+                    }
                 }
             }
             return builder.build();

+ 30 - 0
server/src/main/java/org/elasticsearch/cluster/metadata/AliasOrIndex.java

@@ -19,12 +19,16 @@
 
 package org.elasticsearch.cluster.metadata;
 
+import org.apache.lucene.util.SetOnce;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.collect.Tuple;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * Encapsulates the  {@link IndexMetaData} instances of a concrete index or indices an alias is pointing to.
@@ -78,6 +82,7 @@ public interface AliasOrIndex {
 
         private final String aliasName;
         private final List<IndexMetaData> referenceIndexMetaDatas;
+        private SetOnce<IndexMetaData> writeIndex = new SetOnce<>();
 
         public Alias(AliasMetaData aliasMetaData, IndexMetaData indexMetaData) {
             this.aliasName = aliasMetaData.getAlias();
@@ -90,11 +95,21 @@ public interface AliasOrIndex {
             return true;
         }
 
+        public String getAliasName() {
+            return aliasName;
+        }
+
         @Override
         public List<IndexMetaData> getIndices() {
             return referenceIndexMetaDatas;
         }
 
+
+        @Nullable
+        public IndexMetaData getWriteIndex() {
+            return writeIndex.get();
+        }
+
         /**
          * Returns the unique alias metadata per concrete index.
          *
@@ -138,5 +153,20 @@ public interface AliasOrIndex {
             this.referenceIndexMetaDatas.add(indexMetaData);
         }
 
+        public void computeAndValidateWriteIndex() {
+            List<IndexMetaData> writeIndices = referenceIndexMetaDatas.stream()
+                .filter(idxMeta -> Boolean.TRUE.equals(idxMeta.getAliases().get(aliasName).writeIndex()))
+                .collect(Collectors.toList());
+            if (referenceIndexMetaDatas.size() == 1) {
+                writeIndex.set(referenceIndexMetaDatas.get(0));
+            } else if (writeIndices.size() == 1) {
+                writeIndex.set(writeIndices.get(0));
+            } else if (writeIndices.size() > 1) {
+                List<String> writeIndicesStrings = writeIndices.stream()
+                    .map(i -> i.getIndex().getName()).collect(Collectors.toList());
+                throw new IllegalStateException("alias [" + aliasName + "] has more than one write index [" +
+                    Strings.collectionToCommaDelimitedString(writeIndicesStrings) + "]");
+            }
+        }
     }
 }

+ 2 - 2
server/src/main/java/org/elasticsearch/cluster/metadata/AliasValidator.java

@@ -57,7 +57,7 @@ public class AliasValidator extends AbstractComponent {
      * @throws IllegalArgumentException if the alias is not valid
      */
     public void validateAlias(Alias alias, String index, MetaData metaData) {
-        validateAlias(alias.name(), index, alias.indexRouting(), name -> metaData.index(name));
+        validateAlias(alias.name(), index, alias.indexRouting(), metaData::index);
     }
 
     /**
@@ -66,7 +66,7 @@ public class AliasValidator extends AbstractComponent {
      * @throws IllegalArgumentException if the alias is not valid
      */
     public void validateAliasMetaData(AliasMetaData aliasMetaData, String index, MetaData metaData) {
-        validateAlias(aliasMetaData.alias(), index, aliasMetaData.indexRouting(), name -> metaData.index(name));
+        validateAlias(aliasMetaData.alias(), index, aliasMetaData.indexRouting(), metaData::index);
     }
 
     /**

+ 19 - 12
server/src/main/java/org/elasticsearch/cluster/metadata/MetaData.java

@@ -1039,7 +1039,22 @@ public class MetaData implements Iterable<IndexMetaData>, Diffable<MetaData>, To
 
             }
 
-            // build all indices map
+            SortedMap<String, AliasOrIndex> aliasAndIndexLookup = Collections.unmodifiableSortedMap(buildAliasAndIndexLookup());
+
+
+            // build all concrete indices arrays:
+            // TODO: I think we can remove these arrays. it isn't worth the effort, for operations on all indices.
+            // When doing an operation across all indices, most of the time is spent on actually going to all shards and
+            // do the required operations, the bottleneck isn't resolving expressions into concrete indices.
+            String[] allIndicesArray = allIndices.toArray(new String[allIndices.size()]);
+            String[] allOpenIndicesArray = allOpenIndices.toArray(new String[allOpenIndices.size()]);
+            String[] allClosedIndicesArray = allClosedIndices.toArray(new String[allClosedIndices.size()]);
+
+            return new MetaData(clusterUUID, version, transientSettings, persistentSettings, indices.build(), templates.build(),
+                                customs.build(), allIndicesArray, allOpenIndicesArray, allClosedIndicesArray, aliasAndIndexLookup);
+        }
+
+        private SortedMap<String, AliasOrIndex> buildAliasAndIndexLookup() {
             SortedMap<String, AliasOrIndex> aliasAndIndexLookup = new TreeMap<>();
             for (ObjectCursor<IndexMetaData> cursor : indices.values()) {
                 IndexMetaData indexMetaData = cursor.value;
@@ -1059,17 +1074,9 @@ public class MetaData implements Iterable<IndexMetaData>, Diffable<MetaData>, To
                     });
                 }
             }
-            aliasAndIndexLookup = Collections.unmodifiableSortedMap(aliasAndIndexLookup);
-            // build all concrete indices arrays:
-            // TODO: I think we can remove these arrays. it isn't worth the effort, for operations on all indices.
-            // When doing an operation across all indices, most of the time is spent on actually going to all shards and
-            // do the required operations, the bottleneck isn't resolving expressions into concrete indices.
-            String[] allIndicesArray = allIndices.toArray(new String[allIndices.size()]);
-            String[] allOpenIndicesArray = allOpenIndices.toArray(new String[allOpenIndices.size()]);
-            String[] allClosedIndicesArray = allClosedIndices.toArray(new String[allClosedIndices.size()]);
-
-            return new MetaData(clusterUUID, version, transientSettings, persistentSettings, indices.build(), templates.build(),
-                                customs.build(), allIndicesArray, allOpenIndicesArray, allClosedIndicesArray, aliasAndIndexLookup);
+            aliasAndIndexLookup.values().stream().filter(AliasOrIndex::isAlias)
+                .forEach(alias -> ((AliasOrIndex.Alias) alias).computeAndValidateWriteIndex());
+            return aliasAndIndexLookup;
         }
 
         public static String toXContent(MetaData metaData) throws IOException {

+ 1 - 1
server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataCreateIndexService.java

@@ -516,7 +516,7 @@ public class MetaDataCreateIndexService extends AbstractComponent {
                 }
                 for (Alias alias : request.aliases()) {
                     AliasMetaData aliasMetaData = AliasMetaData.builder(alias.name()).filter(alias.filter())
-                        .indexRouting(alias.indexRouting()).searchRouting(alias.searchRouting()).build();
+                        .indexRouting(alias.indexRouting()).searchRouting(alias.searchRouting()).writeIndex(alias.writeIndex()).build();
                     indexMetaDataBuilder.putAlias(aliasMetaData);
                 }
 

+ 1 - 1
server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesService.java

@@ -127,7 +127,7 @@ public class MetaDataIndexAliasesService extends AbstractComponent {
                 if (index == null) {
                     throw new IndexNotFoundException(action.getIndex());
                 }
-                NewAliasValidator newAliasValidator = (alias, indexRouting, filter) -> {
+                NewAliasValidator newAliasValidator = (alias, indexRouting, filter, writeIndex) -> {
                     /* It is important that we look up the index using the metadata builder we are modifying so we can remove an
                      * index and replace it with an alias. */
                     Function<String, IndexMetaData> indexLookup = name -> metadata.get(name);

+ 3 - 0
server/src/test/java/org/elasticsearch/action/admin/indices/alias/AliasActionsTests.java

@@ -114,6 +114,7 @@ public class AliasActionsTests extends ESTestCase {
         Map<String, Object> filter = randomBoolean() ? randomMap(5) : null;
         Object searchRouting = randomBoolean() ? randomRouting() : null;
         Object indexRouting = randomBoolean() ? randomBoolean() ? searchRouting : randomRouting() : null;
+        boolean writeIndex = randomBoolean();
         XContentBuilder b = XContentBuilder.builder(randomFrom(XContentType.values()).xContent());
         b.startObject();
         {
@@ -142,6 +143,7 @@ public class AliasActionsTests extends ESTestCase {
                 if (indexRouting != null && false == indexRouting.equals(searchRouting)) {
                     b.field("index_routing", indexRouting);
                 }
+                b.field("is_write_index", writeIndex);
             }
             b.endObject();
         }
@@ -159,6 +161,7 @@ public class AliasActionsTests extends ESTestCase {
             }
             assertEquals(Objects.toString(searchRouting, null), action.searchRouting());
             assertEquals(Objects.toString(indexRouting, null), action.indexRouting());
+            assertEquals(writeIndex, action.writeIndex());
         }
     }
 

+ 2 - 1
server/src/test/java/org/elasticsearch/action/admin/indices/create/CreateIndexRequestTests.java

@@ -93,6 +93,7 @@ public class CreateIndexRequestTests extends ESTestCase {
         Alias alias = new Alias("test_alias");
         alias.routing("1");
         alias.filter("{\"term\":{\"year\":2016}}");
+        alias.writeIndex(true);
         request.alias(alias);
 
         Settings.Builder settings = Settings.builder();
@@ -103,7 +104,7 @@ public class CreateIndexRequestTests extends ESTestCase {
 
         String expectedRequestBody = "{\"settings\":{\"index\":{\"number_of_shards\":\"10\"}}," +
             "\"mappings\":{\"my_type\":{\"type\":{}}}," +
-            "\"aliases\":{\"test_alias\":{\"filter\":{\"term\":{\"year\":2016}},\"routing\":\"1\"}}}";
+            "\"aliases\":{\"test_alias\":{\"filter\":{\"term\":{\"year\":2016}},\"routing\":\"1\",\"is_write_index\":true}}}";
 
         assertEquals(expectedRequestBody, actualRequestBody);
     }

+ 2 - 1
server/src/test/java/org/elasticsearch/action/admin/indices/shrink/ResizeRequestTests.java

@@ -71,6 +71,7 @@ public class ResizeRequestTests extends ESTestCase {
             Alias alias = new Alias("test_alias");
             alias.routing("1");
             alias.filter("{\"term\":{\"year\":2016}}");
+            alias.writeIndex(true);
             target.alias(alias);
             Settings.Builder settings = Settings.builder();
             settings.put(SETTING_NUMBER_OF_SHARDS, 10);
@@ -78,7 +79,7 @@ public class ResizeRequestTests extends ESTestCase {
             request.setTargetIndex(target);
             String actualRequestBody = Strings.toString(request);
             String expectedRequestBody = "{\"settings\":{\"index\":{\"number_of_shards\":\"10\"}}," +
-                    "\"aliases\":{\"test_alias\":{\"filter\":{\"term\":{\"year\":2016}},\"routing\":\"1\"}}}";
+                    "\"aliases\":{\"test_alias\":{\"filter\":{\"term\":{\"year\":2016}},\"routing\":\"1\",\"is_write_index\":true}}}";
             assertEquals(expectedRequestBody, actualRequestBody);
         }
     }

+ 17 - 0
server/src/test/java/org/elasticsearch/cluster/metadata/AliasMetaDataTests.java

@@ -41,6 +41,7 @@ public class AliasMetaDataTests extends AbstractXContentTestCase<AliasMetaData>
                         .indexRouting("indexRouting")
                         .routing("routing")
                         .searchRouting("trim,tw , ltw , lw")
+                        .writeIndex(randomBoolean() ? null : randomBoolean())
                         .build();
 
         assertThat(before.searchRoutingValues(), equalTo(Sets.newHashSet("trim", "tw ", " ltw ", " lw")));
@@ -54,6 +55,21 @@ public class AliasMetaDataTests extends AbstractXContentTestCase<AliasMetaData>
         assertThat(after, equalTo(before));
     }
 
+    @Override
+    protected void assertEqualInstances(AliasMetaData expectedInstance, AliasMetaData newInstance) {
+        assertNotSame(newInstance, expectedInstance);
+        if (expectedInstance.writeIndex() == null) {
+            expectedInstance = AliasMetaData.builder(expectedInstance.alias())
+                .filter(expectedInstance.filter())
+                .indexRouting(expectedInstance.indexRouting())
+                .searchRouting(expectedInstance.searchRouting())
+                .writeIndex(randomBoolean() ? null : randomBoolean())
+                .build();
+        }
+        assertEquals(expectedInstance, newInstance);
+        assertEquals(expectedInstance.hashCode(), newInstance.hashCode());
+    }
+
     @Override
     protected AliasMetaData createTestInstance() {
         return createTestItem();
@@ -95,6 +111,7 @@ public class AliasMetaDataTests extends AbstractXContentTestCase<AliasMetaData>
         if (randomBoolean()) {
             builder.filter("{\"term\":{\"year\":2016}}");
         }
+        builder.writeIndex(randomBoolean());
         return builder.build();
     }
 

+ 27 - 0
server/src/test/java/org/elasticsearch/cluster/metadata/IndexCreationTaskTests.java

@@ -69,6 +69,7 @@ import static org.elasticsearch.test.hamcrest.CollectionAssertions.hasKey;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
 import static org.mockito.Matchers.anyObject;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -291,6 +292,32 @@ public class IndexCreationTaskTests extends ESTestCase {
         assertThat(e.getMessage(), containsString("invalid wait_for_active_shards"));
     }
 
+    public void testWriteIndex() throws Exception {
+        Boolean writeIndex = randomBoolean() ? null : randomBoolean();
+        setupRequestAlias(new Alias("alias1").writeIndex(writeIndex));
+        setupRequestMapping("mapping1", createMapping());
+        setupRequestCustom("custom1", createCustom());
+        reqSettings.put("key1", "value1");
+
+        final ClusterState result = executeTask();
+        assertThat(result.metaData().index("test").getAliases(), hasKey("alias1"));
+        assertThat(result.metaData().index("test").getAliases().get("alias1").writeIndex(), equalTo(writeIndex));
+    }
+
+    public void testWriteIndexValidationException() throws Exception {
+        IndexMetaData existingWriteIndex = IndexMetaData.builder("test2")
+            .settings(settings(Version.CURRENT)).putAlias(AliasMetaData.builder("alias1").writeIndex(true).build())
+            .numberOfShards(1).numberOfReplicas(0).build();
+        idxBuilder.put("test2", existingWriteIndex);
+        setupRequestMapping("mapping1", createMapping());
+        setupRequestCustom("custom1", createCustom());
+        reqSettings.put("key1", "value1");
+        setupRequestAlias(new Alias("alias1").writeIndex(true));
+
+        Exception exception = expectThrows(IllegalStateException.class, () -> executeTask());
+        assertThat(exception.getMessage(), startsWith("alias [alias1] has more than one write index ["));
+    }
+
     private IndexRoutingTable createIndexRoutingTableWithStartedShards(Index index) {
         final IndexRoutingTable idxRoutingTable = mock(IndexRoutingTable.class);
 

+ 130 - 4
server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataIndexAliasesServiceTests.java

@@ -19,6 +19,7 @@
 
 package org.elasticsearch.cluster.metadata;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.cluster.ClusterName;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.common.settings.Settings;
@@ -29,9 +30,13 @@ import org.elasticsearch.test.VersionUtils;
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 
 import static java.util.Collections.singletonList;
 import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.startsWith;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anySetOf;
 import static org.mockito.Mockito.mock;
@@ -64,7 +69,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         ClusterState before = createIndex(ClusterState.builder(ClusterName.DEFAULT).build(), index);
 
         // Add an alias to it
-        ClusterState after = service.innerExecute(before, singletonList(new AliasAction.Add(index, "test", null, null, null)));
+        ClusterState after = service.innerExecute(before, singletonList(new AliasAction.Add(index, "test", null, null, null, null)));
         AliasOrIndex alias = after.metaData().getAliasAndIndexLookup().get("test");
         assertNotNull(alias);
         assertTrue(alias.isAlias());
@@ -74,7 +79,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         before = after;
         after = service.innerExecute(before, Arrays.asList(
                 new AliasAction.Remove(index, "test"),
-                new AliasAction.Add(index, "test_2", null, null, null)));
+                new AliasAction.Add(index, "test_2", null, null, null, null)));
         assertNull(after.metaData().getAliasAndIndexLookup().get("test"));
         alias = after.metaData().getAliasAndIndexLookup().get("test_2");
         assertNotNull(alias);
@@ -95,7 +100,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
 
         // Now remove "test" and add an alias to "test" to "test_2" in one go
         ClusterState after = service.innerExecute(before, Arrays.asList(
-                new AliasAction.Add("test_2", "test", null, null, null),
+                new AliasAction.Add("test_2", "test", null, null, null, null),
                 new AliasAction.RemoveIndex("test")));
         AliasOrIndex alias = after.metaData().getAliasAndIndexLookup().get("test");
         assertNotNull(alias);
@@ -109,7 +114,7 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
 
         // Attempt to add an alias to "test" at the same time as we remove it
         IndexNotFoundException e = expectThrows(IndexNotFoundException.class, () -> service.innerExecute(before, Arrays.asList(
-                new AliasAction.Add("test", "alias", null, null, null),
+                new AliasAction.Add("test", "alias", null, null, null, null),
                 new AliasAction.RemoveIndex("test"))));
         assertEquals("test", e.getIndex().getName());
     }
@@ -125,6 +130,127 @@ public class MetaDataIndexAliasesServiceTests extends ESTestCase {
         assertNull(after.metaData().getAliasAndIndexLookup().get("test"));
     }
 
+    public void testAddWriteOnlyWithNoExistingAliases() {
+        ClusterState before = createIndex(ClusterState.builder(ClusterName.DEFAULT).build(), "test");
+
+        ClusterState after = service.innerExecute(before, Arrays.asList(
+            new AliasAction.Add("test", "alias", null, null, null, false)));
+        assertFalse(after.metaData().index("test").getAliases().get("alias").writeIndex());
+        assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(),
+            equalTo(after.metaData().index("test")));
+
+        after = service.innerExecute(before, Arrays.asList(
+            new AliasAction.Add("test", "alias", null, null, null, null)));
+        assertNull(after.metaData().index("test").getAliases().get("alias").writeIndex());
+        assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(),
+            equalTo(after.metaData().index("test")));
+
+        after = service.innerExecute(before, Arrays.asList(
+            new AliasAction.Add("test", "alias", null, null, null, true)));
+        assertTrue(after.metaData().index("test").getAliases().get("alias").writeIndex());
+        assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(),
+            equalTo(after.metaData().index("test")));
+    }
+
+    public void testAddWriteOnlyWithExistingWriteIndex() {
+        IndexMetaData.Builder indexMetaData = IndexMetaData.builder("test")
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        IndexMetaData.Builder indexMetaData2 = IndexMetaData.builder("test2")
+            .putAlias(AliasMetaData.builder("alias").writeIndex(true).build())
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        ClusterState before = ClusterState.builder(ClusterName.DEFAULT)
+            .metaData(MetaData.builder().put(indexMetaData).put(indexMetaData2)).build();
+
+        ClusterState after = service.innerExecute(before, Arrays.asList(
+            new AliasAction.Add("test", "alias", null, null, null, null)));
+        assertNull(after.metaData().index("test").getAliases().get("alias").writeIndex());
+        assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(),
+            equalTo(after.metaData().index("test2")));
+
+        Exception exception = expectThrows(IllegalStateException.class, () -> service.innerExecute(before, Arrays.asList(
+            new AliasAction.Add("test", "alias", null, null, null, true))));
+        assertThat(exception.getMessage(), startsWith("alias [alias] has more than one write index ["));
+    }
+
+    public void testSwapWriteOnlyIndex() {
+        IndexMetaData.Builder indexMetaData = IndexMetaData.builder("test")
+            .putAlias(AliasMetaData.builder("alias").writeIndex(true).build())
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        IndexMetaData.Builder indexMetaData2 = IndexMetaData.builder("test2")
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        ClusterState before = ClusterState.builder(ClusterName.DEFAULT)
+            .metaData(MetaData.builder().put(indexMetaData).put(indexMetaData2)).build();
+
+        Boolean unsetValue = randomBoolean() ? null : false;
+        List<AliasAction> swapActions = Arrays.asList(
+            new AliasAction.Add("test", "alias", null, null, null, unsetValue),
+            new AliasAction.Add("test2", "alias", null, null, null, true)
+        );
+        Collections.shuffle(swapActions, random());
+        ClusterState after = service.innerExecute(before, swapActions);
+        assertThat(after.metaData().index("test").getAliases().get("alias").writeIndex(), equalTo(unsetValue));
+        assertTrue(after.metaData().index("test2").getAliases().get("alias").writeIndex());
+        assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(),
+            equalTo(after.metaData().index("test2")));
+    }
+
+    public void testAddWriteOnlyWithExistingNonWriteIndices() {
+        IndexMetaData.Builder indexMetaData = IndexMetaData.builder("test")
+            .putAlias(AliasMetaData.builder("alias").writeIndex(randomBoolean() ? null : false).build())
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        IndexMetaData.Builder indexMetaData2 = IndexMetaData.builder("test2")
+            .putAlias(AliasMetaData.builder("alias").writeIndex(randomBoolean() ? null : false).build())
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        IndexMetaData.Builder indexMetaData3 = IndexMetaData.builder("test3")
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        ClusterState before = ClusterState.builder(ClusterName.DEFAULT)
+            .metaData(MetaData.builder().put(indexMetaData).put(indexMetaData2).put(indexMetaData3)).build();
+
+        assertNull(((AliasOrIndex.Alias) before.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex());
+
+        ClusterState after = service.innerExecute(before, Arrays.asList(
+            new AliasAction.Add("test3", "alias", null, null, null, true)));
+        assertTrue(after.metaData().index("test3").getAliases().get("alias").writeIndex());
+        assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(),
+            equalTo(after.metaData().index("test3")));
+
+    }
+
+    public void testAddWriteOnlyWithIndexRemoved() {
+        IndexMetaData.Builder indexMetaData = IndexMetaData.builder("test")
+            .putAlias(AliasMetaData.builder("alias").build())
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        IndexMetaData.Builder indexMetaData2 = IndexMetaData.builder("test2")
+            .putAlias(AliasMetaData.builder("alias").build())
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        ClusterState before = ClusterState.builder(ClusterName.DEFAULT)
+            .metaData(MetaData.builder().put(indexMetaData).put(indexMetaData2)).build();
+
+        assertNull(before.metaData().index("test").getAliases().get("alias").writeIndex());
+        assertNull(before.metaData().index("test2").getAliases().get("alias").writeIndex());
+        assertNull(((AliasOrIndex.Alias) before.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex());
+
+        ClusterState after = service.innerExecute(before, Collections.singletonList(new AliasAction.RemoveIndex("test")));
+        assertNull(after.metaData().index("test2").getAliases().get("alias").writeIndex());
+        assertThat(((AliasOrIndex.Alias) after.metaData().getAliasAndIndexLookup().get("alias")).getWriteIndex(),
+            equalTo(after.metaData().index("test2")));
+    }
+
+    public void testAddWriteOnlyValidatesAgainstMetaDataBuilder() {
+        IndexMetaData.Builder indexMetaData = IndexMetaData.builder("test")
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        IndexMetaData.Builder indexMetaData2 = IndexMetaData.builder("test2")
+            .settings(settings(Version.CURRENT)).numberOfShards(1).numberOfReplicas(1);
+        ClusterState before = ClusterState.builder(ClusterName.DEFAULT)
+            .metaData(MetaData.builder().put(indexMetaData).put(indexMetaData2)).build();
+
+        Exception exception = expectThrows(IllegalStateException.class, () -> service.innerExecute(before, Arrays.asList(
+            new AliasAction.Add("test", "alias", null, null, null, true),
+            new AliasAction.Add("test2", "alias", null, null, null, true)
+        )));
+        assertThat(exception.getMessage(), startsWith("alias [alias] has more than one write index ["));
+    }
+
     private ClusterState createIndex(ClusterState state, String index) {
         IndexMetaData indexMetaData = IndexMetaData.builder(index)
                 .settings(Settings.builder().put("index.version.created", VersionUtils.randomVersion(random())))

+ 35 - 0
server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataTests.java

@@ -99,6 +99,34 @@ public class MetaDataTests extends ESTestCase {
         }
     }
 
+    public void testValidateAliasWriteOnly() {
+        String alias = randomAlphaOfLength(5);
+        String indexA = randomAlphaOfLength(6);
+        String indexB = randomAlphaOfLength(7);
+        Boolean aWriteIndex = randomBoolean() ? null : randomBoolean();
+        Boolean bWriteIndex;
+        if (Boolean.TRUE.equals(aWriteIndex)) {
+            bWriteIndex = randomFrom(Boolean.FALSE, null);
+        } else {
+            bWriteIndex = randomFrom(Boolean.TRUE, Boolean.FALSE, null);
+        }
+        // when only one index/alias pair exist
+        MetaData metaData = MetaData.builder().put(buildIndexMetaData(indexA, alias, aWriteIndex)).build();
+
+        // when alias points to two indices, but valid
+        // one of the following combinations: [(null, null), (null, true), (null, false), (false, false)]
+        MetaData.builder(metaData).put(buildIndexMetaData(indexB, alias, bWriteIndex)).build();
+
+        // when too many write indices
+        Exception exception = expectThrows(IllegalStateException.class,
+            () -> {
+                IndexMetaData.Builder metaA = buildIndexMetaData(indexA, alias, true);
+                IndexMetaData.Builder metaB = buildIndexMetaData(indexB, alias, true);
+                MetaData.builder().put(metaA).put(metaB).build();
+            });
+        assertThat(exception.getMessage(), startsWith("alias [" + alias + "] has more than one write index ["));
+    }
+
     public void testResolveIndexRouting() {
         IndexMetaData.Builder builder = IndexMetaData.builder("index")
                 .settings(Settings.builder().put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT))
@@ -428,6 +456,13 @@ public class MetaDataTests extends ESTestCase {
         }
     }
 
+    private IndexMetaData.Builder buildIndexMetaData(String name, String alias, Boolean writeIndex) {
+        return IndexMetaData.builder(name)
+            .settings(settings(Version.CURRENT)).creationDate(randomNonNegativeLong())
+            .putAlias(AliasMetaData.builder(alias).writeIndex(writeIndex))
+            .numberOfShards(1).numberOfReplicas(0);
+    }
+
     @SuppressWarnings("unchecked")
     private static void assertIndexMappingsNoFields(ImmutableOpenMap<String, ImmutableOpenMap<String, MappingMetaData>> mappings,
                                                     String index) {

+ 6 - 3
server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetaDataTests.java

@@ -111,7 +111,7 @@ public class ToAndFromJsonMetaDataTests extends ESTestCase {
                         .putMapping("mapping1", MAPPING_SOURCE1)
                         .putMapping("mapping2", MAPPING_SOURCE2)
                         .putAlias(newAliasMetaDataBuilder("alias1").filter(ALIAS_FILTER1))
-                        .putAlias(newAliasMetaDataBuilder("alias2"))
+                        .putAlias(newAliasMetaDataBuilder("alias2").writeIndex(randomBoolean() ? null : randomBoolean()))
                         .putAlias(newAliasMetaDataBuilder("alias4").filter(ALIAS_FILTER2)))
                 .put(IndexTemplateMetaData.builder("foo")
                         .patterns(Collections.singletonList("bar"))
@@ -132,7 +132,7 @@ public class ToAndFromJsonMetaDataTests extends ESTestCase {
                         .putMapping("mapping1", MAPPING_SOURCE1)
                         .putMapping("mapping2", MAPPING_SOURCE2)
                         .putAlias(newAliasMetaDataBuilder("alias1").filter(ALIAS_FILTER1))
-                        .putAlias(newAliasMetaDataBuilder("alias2"))
+                        .putAlias(newAliasMetaDataBuilder("alias2").writeIndex(randomBoolean() ? null : randomBoolean()))
                         .putAlias(newAliasMetaDataBuilder("alias4").filter(ALIAS_FILTER2)))
                 .put(IndexTemplateMetaData.builder("foo")
                         .patterns(Collections.singletonList("bar"))
@@ -146,7 +146,6 @@ public class ToAndFromJsonMetaDataTests extends ESTestCase {
                 .build();
 
         String metaDataSource = MetaData.Builder.toXContent(metaData);
-//        System.out.println("ToJson: " + metaDataSource);
 
         MetaData parsedMetaData = MetaData.Builder.fromXContent(createParser(JsonXContent.jsonXContent, metaDataSource));
 
@@ -270,6 +269,8 @@ public class ToAndFromJsonMetaDataTests extends ESTestCase {
         assertThat(indexMetaData.getAliases().get("alias1").filter().string(), equalTo(ALIAS_FILTER1));
         assertThat(indexMetaData.getAliases().get("alias2").alias(), equalTo("alias2"));
         assertThat(indexMetaData.getAliases().get("alias2").filter(), nullValue());
+        assertThat(indexMetaData.getAliases().get("alias2").writeIndex(),
+            equalTo(metaData.index("test11").getAliases().get("alias2").writeIndex()));
         assertThat(indexMetaData.getAliases().get("alias4").alias(), equalTo("alias4"));
         assertThat(indexMetaData.getAliases().get("alias4").filter().string(), equalTo(ALIAS_FILTER2));
 
@@ -288,6 +289,8 @@ public class ToAndFromJsonMetaDataTests extends ESTestCase {
         assertThat(indexMetaData.getAliases().get("alias1").filter().string(), equalTo(ALIAS_FILTER1));
         assertThat(indexMetaData.getAliases().get("alias2").alias(), equalTo("alias2"));
         assertThat(indexMetaData.getAliases().get("alias2").filter(), nullValue());
+        assertThat(indexMetaData.getAliases().get("alias2").writeIndex(),
+            equalTo(metaData.index("test12").getAliases().get("alias2").writeIndex()));
         assertThat(indexMetaData.getAliases().get("alias4").alias(), equalTo("alias4"));
         assertThat(indexMetaData.getAliases().get("alias4").filter().string(), equalTo(ALIAS_FILTER2));
 

+ 3 - 3
server/src/test/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java

@@ -1518,9 +1518,9 @@ public class SharedClusterSnapshotRestoreIT extends AbstractSnapshotIntegTestCas
         ensureGreen();
 
         assertAcked(client.admin().indices().prepareAliases()
-                        .addAlias("test-idx-1", "alias-1")
-                        .addAlias("test-idx-2", "alias-2")
-                        .addAlias("test-idx-3", "alias-3")
+                        .addAlias("test-idx-1", "alias-1", false)
+                        .addAlias("test-idx-2", "alias-2", false)
+                        .addAlias("test-idx-3", "alias-3", false)
         );
 
         logger.info("--> indexing some data");

+ 4 - 0
test/framework/src/main/java/org/elasticsearch/index/RandomCreateIndexGenerator.java

@@ -138,6 +138,10 @@ public final class RandomCreateIndexGenerator {
             alias.filter("{\"term\":{\"year\":2016}}");
         }
 
+        if (randomBoolean()) {
+            alias.writeIndex(randomBoolean());
+        }
+
         return alias;
     }
 }