All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.solr.common.cloud.Aliases Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.common.cloud;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CollectionAdminParams;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.common.util.Utils;

/**
 * Holds collection aliases -- virtual collections that point to one or more other collections. We
 * might add other types of aliases here some day. Immutable.
 */
public class Aliases {

  /**
   * An empty, minimal Aliases primarily used to support the non-cloud solr use cases. Not normally
   * useful in cloud situations where the version of the node needs to be tracked even if all
   * aliases are removed. The -1 version makes it subordinate to any real version, and furthermore
   * we never "set" this EMPTY instance into ZK.
   */
  public static final Aliases EMPTY =
      new Aliases(Collections.emptyMap(), Collections.emptyMap(), -1);

  // These two constants correspond to the top level elements in aliases.json. The first one denotes
  // a section containing a list of aliases and their attendant collections, the second contains a
  // list of aliases and their attendant properties (metadata) They probably should be named
  // "aliases" and "alias_properties" but for back compat reasons, we cannot change them
  private static final String COLLECTION = "collection";
  private static final String COLLECTION_METADATA = "collection_metadata";

  // aliasName -> list of collections.  (note: the Lists here should be unmodifiable)
  private final Map> collectionAliases; // not null

  // aliasName --> propertiesKey --> propertiesValue (note: the inner Map here should be
  // unmodifiable)
  private final Map> collectionAliasProperties; // notnull

  private final int zNodeVersion;

  /**
   * Construct aliases directly with this information -- caller should not retain. Any deeply nested
   * collections are assumed to already be unmodifiable.
   */
  Aliases(
      Map> collectionAliases,
      Map> collectionAliasProperties,
      int zNodeVersion) {
    this.collectionAliases = Objects.requireNonNull(collectionAliases);
    this.collectionAliasProperties = Objects.requireNonNull(collectionAliasProperties);
    this.zNodeVersion = zNodeVersion;
  }

  public void forEachAlias(BiConsumer> consumer) {
    collectionAliases.forEach(
        (s, colls) -> consumer.accept(s, Collections.unmodifiableList(colls)));
  }

  public int size() {
    return collectionAliases.size();
  }

  /**
   * Create an instance from the JSON bytes read from zookeeper. Generally this should only be done
   * by a ZkStateReader.
   *
   * @param bytes The bytes read via a getData request to zookeeper (possibly null)
   * @param zNodeVersion the version of the data in zookeeper that this instance corresponds to
   * @return A new immutable Aliases object
   */
  @SuppressWarnings({"unchecked", "rawtypes"})
  public static Aliases fromJSON(byte[] bytes, int zNodeVersion) {
    Map aliasMap = (Map) Utils.fromJSON(bytes);

    @SuppressWarnings({"rawtypes"})
    Map colAliases = aliasMap.getOrDefault(COLLECTION, Collections.emptyMap());
    colAliases = convertMapOfCommaDelimitedToMapOfList(colAliases); // also unmodifiable

    Map> colMeta =
        aliasMap.getOrDefault(COLLECTION_METADATA, Collections.emptyMap());
    colMeta.replaceAll((k, metaMap) -> Collections.unmodifiableMap(metaMap));

    return new Aliases(colAliases, colMeta, zNodeVersion);
  }

  /** Serialize our state. */
  public byte[] toJSON() {
    if (collectionAliases.isEmpty()) {
      assert collectionAliasProperties.isEmpty();
      return null;
    } else {
      @SuppressWarnings({"rawtypes"})
      Map tmp = new LinkedHashMap<>();
      tmp.put(COLLECTION, convertMapOfListToMapOfCommaDelimited(collectionAliases));
      if (!collectionAliasProperties.isEmpty()) {
        tmp.put(COLLECTION_METADATA, collectionAliasProperties);
      }
      return Utils.toJSON(tmp);
    }
  }

  public static Map> convertMapOfCommaDelimitedToMapOfList(
      Map collectionAliasMap) {
    return collectionAliasMap.entrySet().stream()
        .collect(
            Collectors.toMap(
                Map.Entry::getKey,
                e -> splitCollections(e.getValue()),
                (a, b) -> {
                  throw new IllegalStateException(
                      String.format(Locale.ROOT, "Duplicate key %s", b));
                },
                LinkedHashMap::new));
  }

  private static List splitCollections(String collections) {
    return Collections.unmodifiableList(StrUtils.splitSmart(collections, ",", true));
  }

  public static Map convertMapOfListToMapOfCommaDelimited(
      Map> collectionAliasMap) {
    return collectionAliasMap.entrySet().stream()
        .collect(
            Collectors.toMap(
                Map.Entry::getKey,
                e -> String.join(",", e.getValue()),
                (a, b) -> {
                  throw new IllegalStateException(
                      String.format(Locale.ROOT, "Duplicate key %s", b));
                },
                LinkedHashMap::new));
  }

  public int getZNodeVersion() {
    return zNodeVersion;
  }

  /**
   * Get a map similar to the JSON data as stored in zookeeper. Callers may prefer use of {@link
   * #getCollectionAliasListMap()} instead, if collection names will be iterated.
   *
   * @return an unmodifiable Map of collection aliases mapped to a comma delimited string of the
   *     collection(s) the alias maps to. Does not return null.
   */
  public Map getCollectionAliasMap() {
    return Collections.unmodifiableMap(convertMapOfListToMapOfCommaDelimited(collectionAliases));
  }

  /**
   * Get a fully parsed map of collection aliases.
   *
   * @return an unmodifiable Map of collection aliases mapped to a list of the collection(s) the
   *     alias maps to. Does not return null.
   */
  public Map> getCollectionAliasListMap() {
    // Note: Lists contained by this map are already unmodifiable and can be shared safely
    return Collections.unmodifiableMap(collectionAliases);
  }

  /**
   * Returns an unmodifiable Map of properties for a given alias. This method will never return
   * null.
   *
   * @param alias the name of an alias also found as a key in {@link #getCollectionAliasListMap()}
   * @return The properties for the alias (possibly empty).
   */
  public Map getCollectionAliasProperties(String alias) {
    // Note: map is already unmodifiable; it can be shared safely
    return collectionAliasProperties.getOrDefault(alias, Collections.emptyMap());
  }

  /**
   * List the collections associated with a particular alias. One level of alias indirection is
   * supported (alias to alias to collection). Such indirection may be deprecated in the future, use
   * with caution.
   *
   * @return An unmodifiable list of collections names that the input alias name maps to. If there
   *     are none, the input is returned.
   */
  public List resolveAliases(String aliasName) {
    return resolveAliasesGivenAliasMap(collectionAliases, aliasName);
  }

  /** Returns true if an alias is defined, false otherwise. */
  public boolean hasAlias(String aliasName) {
    return collectionAliases.containsKey(aliasName);
  }

  /** Returns true if an alias exists and is a routed alias, false otherwise. */
  public boolean isRoutedAlias(String aliasName) {
    if (!collectionAliases.containsKey(aliasName)) {
      return false;
    }
    Map props = collectionAliasProperties.get(aliasName);
    if (props == null) {
      return false;
    }
    return props.entrySet().stream()
        .anyMatch(e -> e.getKey().startsWith(CollectionAdminParams.ROUTER_PREFIX));
  }

  /**
   * Resolve an alias that points to a single collection. One level of alias indirection is
   * supported.
   *
   * @param aliasName alias name
   * @return original name if there's no such alias, or a resolved name. If an alias points to more
   *     than 1 collection (directly or indirectly) an exception is thrown
   * @throws SolrException if either direct or indirect alias points to more than 1 name.
   */
  public String resolveSimpleAlias(String aliasName) throws SolrException {
    return resolveSimpleAliasGivenAliasMap(collectionAliases, aliasName);
  }

  /**
   * @lucene.internal
   */
  @SuppressWarnings("JavaDoc")
  public static String resolveSimpleAliasGivenAliasMap(
      Map> collectionAliasListMap, String aliasName) throws SolrException {
    List level1 = collectionAliasListMap.get(aliasName);
    if (level1 == null || level1.isEmpty()) {
      return aliasName; // simple collection name
    }
    if (level1.size() > 1) {
      throw new SolrException(
          SolrException.ErrorCode.BAD_REQUEST,
          "Simple alias '" + aliasName + "' points to more than 1 collection: " + level1);
    }
    List level2 = collectionAliasListMap.get(level1.get(0));
    if (level2 == null || level2.isEmpty()) {
      return level1.get(0); // simple alias
    }
    if (level2.size() > 1) {
      throw new SolrException(
          SolrException.ErrorCode.BAD_REQUEST,
          "Simple alias '"
              + aliasName
              + "' resolves to '"
              + level1.get(0)
              + "' which points to more than 1 collection: "
              + level2);
    }
    return level2.get(0);
  }

  /**
   * @lucene.internal
   */
  @SuppressWarnings("JavaDoc")
  public static List resolveAliasesGivenAliasMap(
      Map> collectionAliasListMap, String aliasName) {
    // Due to another level of indirection, this is more complicated...
    List level1 = collectionAliasListMap.get(aliasName);
    if (level1 == null) {
      return Collections.singletonList(aliasName); // is a collection
    }
    // avoid allocating objects if possible
    LinkedHashSet uniqueResult = null;
    for (int i = 0; i < level1.size(); i++) {
      String level1Alias = level1.get(i);
      List level2 = collectionAliasListMap.get(level1Alias);
      if (level2 == null) {
        // will copy all level1alias-es so far on lazy init
        if (uniqueResult != null) {
          uniqueResult.add(level1Alias);
        }
      } else {
        if (uniqueResult == null) { // lazy init
          uniqueResult = new LinkedHashSet<>(level1.size());
          // add all level1Alias-es so far
          uniqueResult.addAll(level1.subList(0, i));
        }
        uniqueResult.addAll(level2);
      }
    }
    if (uniqueResult == null) {
      return level1;
    } else {
      return Collections.unmodifiableList(new ArrayList<>(uniqueResult));
    }
  }

  /**
   * Creates a new Aliases instance with the same data as the current one but with a modification
   * based on the parameters.
   *
   * 

Note that the state in zookeeper is unaffected by this method and the change must still be * persisted via {@code * ZkStateReader.AliasesManager#applyModificationAndExportToZk(UnaryOperator)} * * @param alias the alias to update, must not be null * @param collections the comma separated list of collections for the alias, null to remove the * alias */ public Aliases cloneWithCollectionAlias(String alias, String collections) { if (alias == null) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Alias name cannot be null"); } Map> newColProperties; Map> newColAliases = new LinkedHashMap<>(this.collectionAliases); // clone to modify if (collections == null) { // REMOVE: newColProperties = new LinkedHashMap<>(this.collectionAliasProperties); // clone to modify newColProperties.remove(alias); newColAliases.remove(alias); // remove second-level alias from compound aliases for (Map.Entry> entry : newColAliases.entrySet()) { List list = entry.getValue(); if (list.contains(alias)) { list = new ArrayList<>(list); list.remove(alias); entry.setValue(Collections.unmodifiableList(list)); } } newColAliases.entrySet().removeIf(entry -> entry.getValue().isEmpty()); } else { newColProperties = this.collectionAliasProperties; // no changes // java representation is a list, so split before adding to maintain consistency newColAliases.put(alias, splitCollections(collections)); // note: unmodifiableList } return new Aliases(newColAliases, newColProperties, zNodeVersion); } /** * Rename an alias. This performs a "deep rename", which changes also the second-level alias * lists. Renaming routed aliases is not supported. * *

Note that the state in zookeeper is unaffected by this method and the change must still be * persisted via {@code * ZkStateReader.AliasesManager#applyModificationAndExportToZk(UnaryOperator)} * * @param before previous alias name, must not be null * @param after new alias name. If this is null then it's equivalent to calling {@link * #cloneWithCollectionAlias(String, String)} with the second argument set to null, ie. * removing an alias. * @return new instance with the renamed alias * @throws SolrException when either before or after is empty, or the * before name is a routed alias */ public Aliases cloneWithRename(String before, String after) throws SolrException { if (before == null) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "'before' cannot be null"); } if (after == null) { return cloneWithCollectionAlias(before, after); } if (before.isEmpty() || after.isEmpty()) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "'before' and 'after' cannot be empty"); } if (before.equals(after)) { return this; } Map props = collectionAliasProperties.get(before); if (props != null) { if (props.keySet().stream() .anyMatch(k -> k.startsWith(CollectionAdminParams.ROUTER_PREFIX))) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "source name '" + before + "' is a routed alias."); } } Map> newColProperties = new LinkedHashMap<>(this.collectionAliasProperties); Map> newColAliases = new LinkedHashMap<>(this.collectionAliases); // clone to modify List level1 = newColAliases.remove(before); props = newColProperties.remove(before); if (level1 != null) { newColAliases.put(after, level1); } if (props != null) { newColProperties.put(after, props); } for (Map.Entry> entry : newColAliases.entrySet()) { List collections = entry.getValue(); if (collections.contains(before)) { LinkedHashSet newCollections = new LinkedHashSet<>(collections.size()); for (String coll : collections) { if (coll.equals(before)) { newCollections.add(after); } else { newCollections.add(coll); } } entry.setValue(Collections.unmodifiableList(new ArrayList<>(newCollections))); } } if (level1 == null) { // create an alias that points to the collection newColAliases.put(after, Collections.singletonList(before)); } return new Aliases(newColAliases, newColProperties, zNodeVersion); } /** * Set the value for some properties on a collection alias. This is done by creating a new Aliases * instance with the same data as the current one but with a modification based on the parameters. * *

Note that the state in zookeeper is unaffected by this method and the change must still be * persisted via {@code * ZkStateReader.AliasesManager#applyModificationAndExportToZk(UnaryOperator)} * * @param alias the alias to update * @param propertiesKey the key for the properties * @param propertiesValue the properties to add/replace, null to remove the key. * @return An immutable copy of the aliases with the new properties. */ public Aliases cloneWithCollectionAliasProperties( String alias, String propertiesKey, String propertiesValue) { return cloneWithCollectionAliasProperties( alias, Collections.singletonMap(propertiesKey, propertiesValue)); } /** * Set the values for some properties keys on a collection alias. This is done by creating a new * Aliases instance with the same data as the current one but with a modification based on the * parameters. * *

Note that the state in zookeeper is unaffected by this method and the change must still be * persisted via {@code * ZkStateReader.AliasesManager#applyModificationAndExportToZk(UnaryOperator)} * * @param alias the alias to update * @param properties the properties to add/replace, null values in the map will remove the key. * @return An immutable copy of the aliases with the new properties. */ public Aliases cloneWithCollectionAliasProperties(String alias, Map properties) throws SolrException { if (!collectionAliases.containsKey(alias)) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, alias + " is not a valid alias"); } if (properties == null) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Null is not a valid properties map"); } Map> newColProperties = new LinkedHashMap<>(this.collectionAliasProperties); // clone to modify Map newMetaMap = new LinkedHashMap<>(newColProperties.getOrDefault(alias, Collections.emptyMap())); for (Map.Entry metaEntry : properties.entrySet()) { if (metaEntry.getValue() != null) { newMetaMap.put(metaEntry.getKey(), metaEntry.getValue()); } else { newMetaMap.remove(metaEntry.getKey()); } } newColProperties.put(alias, Collections.unmodifiableMap(newMetaMap)); return new Aliases(collectionAliases, newColProperties, zNodeVersion); } @Override public String toString() { return "Aliases{" + "collectionAliases=" + collectionAliases + ", collectionAliasProperties=" + collectionAliasProperties + ", zNodeVersion=" + zNodeVersion + '}'; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy