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

org.apache.solr.handler.admin.ZookeeperInfoHandler 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.handler.admin;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.lang.invoke.MethodHandles;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.lucene.util.BytesRef;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.OnReconnect;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.ContentStream;
import org.apache.solr.common.util.Utils;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.JSONResponseWriter;
import org.apache.solr.response.RawResponseWriter;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.util.SimplePostTool.BAOS;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.data.Stat;
import org.apache.zookeeper.server.ByteBufferInputStream;
import org.noggit.CharArr;
import org.noggit.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.solr.common.params.CommonParams.OMIT_HEADER;
import static org.apache.solr.common.params.CommonParams.PATH;
import static org.apache.solr.common.params.CommonParams.WT;


/**
 * Zookeeper Info
 *
 * @since solr 4.0
 */
public final class ZookeeperInfoHandler extends RequestHandlerBase {
  private final CoreContainer cores;

  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  // used for custom sorting collection names looking like prefix##
  // only go out to 7 digits (which safely fits in an int)
  private static final Pattern endsWithDigits = Pattern.compile("^(\\D*)(\\d{1,7}?)$");

  public ZookeeperInfoHandler(CoreContainer cc) {
    this.cores = cc;
  }


  @Override
  public String getDescription() {
    return "Fetch Zookeeper contents";
  }

  @Override
  public Category getCategory() {
    return Category.ADMIN;
  }

  /**
   * Enumeration of ways to filter collections on the graph panel.
   */
  static enum FilterType {
    none, name, status
  }

  /**
   * Holds state of a single page of collections requested from the cloud panel.
   */
  static final class PageOfCollections {
    List selected;
    int numFound = 0; // total number of matches (across all pages)
    int start = 0;
    int rows = -1;
    FilterType filterType;
    String filter;

    PageOfCollections(int start, int rows, FilterType filterType, String filter) {
      this.start = start;
      this.rows = rows;
      this.filterType = filterType;
      this.filter = filter;
    }

    void selectPage(List collections) {
      numFound = collections.size();
      // start with full set and then find the sublist for the desired selected
      selected = collections;

      if (rows > 0) { // paging desired
        if (start > numFound)
          start = 0; // this might happen if they applied a new filter

        int lastIndex = Math.min(start + rows, numFound);
        if (start > 0 || lastIndex < numFound)
          selected = collections.subList(start, lastIndex);
      }
    }

    /**
     * Filters a list of collections by name if applicable.
     */
    List applyNameFilter(List collections) {

      if (filterType != FilterType.name || filter == null)
        return collections; // name filter doesn't apply

      // typically, a user will type a prefix and then *, e.g. tj*
      // when they really mean tj.*
      String regexFilter = (!filter.endsWith(".*") && filter.endsWith("*"))
          ? filter.substring(0, filter.length() - 1) + ".*" : filter;

      // case-insensitive
      if (!regexFilter.startsWith("(?i)"))
        regexFilter = "(?i)" + regexFilter;

      Pattern filterRegex = Pattern.compile(regexFilter);
      List filtered = new ArrayList();
      for (String next : collections) {
        if (matches(filterRegex, next))
          filtered.add(next);
      }

      return filtered;
    }

    /**
     * Walk the collection state JSON object to see if it has any replicas that match
     * the state the user is filtering by.
     */
    @SuppressWarnings("unchecked")
    final boolean matchesStatusFilter(Map collectionState, Set liveNodes) {

      if (filterType != FilterType.status || filter == null || filter.length() == 0)
        return true; // no status filter, so all match

      boolean isHealthy = true; // means all replicas for all shards active
      boolean hasDownedShard = false; // means one or more shards is down
      boolean replicaInRecovery = false;

      Map shards = (Map) collectionState.get("shards");
      for (Object o : shards.values()) {
        boolean hasActive = false;
        Map shard = (Map) o;
        Map replicas = (Map) shard.get("replicas");
        for (Object value : replicas.values()) {
          Map replicaState = (Map) value;
          Replica.State coreState = Replica.State.getState((String) replicaState.get(ZkStateReader.STATE_PROP));
          String nodeName = (String) replicaState.get("node_name");

          // state can lie to you if the node is offline, so need to reconcile with live_nodes too
          if (!liveNodes.contains(nodeName))
            coreState = Replica.State.DOWN; // not on a live node, so must be down

          if (coreState == Replica.State.ACTIVE) {
            hasActive = true; // assumed no replicas active and found one that is for this shard
          } else {
            if (coreState == Replica.State.RECOVERING) {
              replicaInRecovery = true;
            }
            isHealthy = false; // assumed healthy and found one replica that is not
          }
        }

        if (!hasActive)
          hasDownedShard = true; // this is bad
      }

      if ("healthy".equals(filter)) {
        return isHealthy;
      } else if ("degraded".equals(filter)) {
        return !hasDownedShard && !isHealthy; // means no shards offline but not 100% healthy either
      } else if ("downed_shard".equals(filter)) {
        return hasDownedShard;
      } else if (Replica.State.getState(filter) == Replica.State.RECOVERING) {
        return !isHealthy && replicaInRecovery;
      }

      return true;
    }

    final boolean matches(final Pattern filter, final String collName) {
      return filter.matcher(collName).matches();
    }

    String getPagingHeader() {
      return start + "|" + rows + "|" + numFound + "|" + (filterType != null ? filterType.toString() : "") + "|" + (filter != null ? filter : "");
    }

    public String toString() {
      return getPagingHeader();
    }

  }

  /**
   * Supports paged navigation of collections on the cloud panel. To avoid serving
   * stale collection data, this object watches the /collections znode, which will
   * change if a collection is added or removed.
   */
  static final class PagedCollectionSupport implements Watcher, Comparator, OnReconnect {

    // this is the full merged list of collections from ZooKeeper
    private List cachedCollections;

    /**
     * If the list of collections changes, mark the cache as stale.
     */
    @Override
    public void process(WatchedEvent event) {
      // session events are not change events, and do not remove the watcher
      if (Event.EventType.None.equals(event.getType())) {
        return;
      }
      synchronized (this) {
        cachedCollections = null;
      }
    }

    /**
     * Create a merged view of all collections (internal from /clusterstate.json and external from /collections/?/state.json
     */
    private synchronized List getCollections(SolrZkClient zkClient) throws KeeperException, InterruptedException {
      if (cachedCollections == null) {
        // cache is stale, rebuild the full list ...
        cachedCollections = new ArrayList();

        List fromZk = zkClient.getChildren("/collections", this, true);
        if (fromZk != null)
          cachedCollections.addAll(fromZk);

        // sort the final merged set of collections
        Collections.sort(cachedCollections, this);
      }

      return cachedCollections;
    }

    /**
     * Gets the requested page of collections after applying filters and offsets.
     */
    public PageOfCollections fetchPage(PageOfCollections page, SolrZkClient zkClient)
        throws KeeperException, InterruptedException {


      List children = getCollections(zkClient);
      page.selected = children; // start with the page being the full list

      // activate paging (if disabled) for large collection sets
      if (page.start == 0 && page.rows == -1 && page.filter == null && children.size() > 10) {
        page.rows = 20;
        page.start = 0;
      }

      // apply the name filter if supplied (we don't need to pull state
      // data from ZK to do name filtering
      if (page.filterType == FilterType.name && page.filter != null)
        children = page.applyNameFilter(children);

      // a little hacky ... we can't select the page when filtering by
      // status until reading all status objects from ZK
      if (page.filterType != FilterType.status)
        page.selectPage(children);

      return page;
    }

    @Override
    public int compare(String left, String right) {
      if (left == null)
        return -1;

      if (left.equals(right))
        return 0;

      // sort lexically unless the two collection names start with the same base prefix
      // and end in a number (which is a common enough naming scheme to have direct 
      // support for it)
      Matcher leftMatcher = endsWithDigits.matcher(left);
      if (leftMatcher.matches()) {
        Matcher rightMatcher = endsWithDigits.matcher(right);
        if (rightMatcher.matches()) {
          String leftGroup1 = leftMatcher.group(1);
          String rightGroup1 = rightMatcher.group(1);
          if (leftGroup1.equals(rightGroup1)) {
            // both start with the same prefix ... compare indexes
            // using longs here as we don't know how long the 2nd group is
            int leftGroup2 = Integer.parseInt(leftMatcher.group(2));
            int rightGroup2 = Integer.parseInt(rightMatcher.group(2));
            return (leftGroup2 > rightGroup2) ? 1 : ((leftGroup2 == rightGroup2) ? 0 : -1);
          }
        }
      }
      return left.compareTo(right);
    }

    /**
     * Called after a ZooKeeper session expiration occurs
     */
    @Override
    public void command() {
      // we need to re-establish the watcher on the collections list after session expires
      synchronized (this) {
        cachedCollections = null;
      }
    }
  }

  private PagedCollectionSupport pagingSupport;

  @Override
  @SuppressWarnings({"unchecked"})
  public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
    final SolrParams params = req.getParams();
    Map map = new HashMap<>(1);
    map.put(WT, "raw");
    map.put(OMIT_HEADER, "true");
    req.setParams(SolrParams.wrapDefaults(new MapSolrParams(map), params));
    synchronized (this) {
      if (pagingSupport == null) {
        pagingSupport = new PagedCollectionSupport();
        ZkController zkController = cores.getZkController();
        if (zkController != null) {
          // get notified when the ZK session expires (so we can clear the cached collections and rebuild)
          zkController.addOnReconnectListener(pagingSupport);
        }
      }
    }

    String path = params.get(PATH);

    if (params.get("addr") != null) {
      throw new SolrException(ErrorCode.BAD_REQUEST, "Illegal parameter \"addr\"");
    }

    String detailS = params.get("detail");
    boolean detail = detailS != null && detailS.equals("true");

    String dumpS = params.get("dump");
    boolean dump = dumpS != null && dumpS.equals("true");

    int start = params.getInt("start", 0);
    int rows = params.getInt("rows", -1);

    String filterType = params.get("filterType");
    if (filterType != null) {
      filterType = filterType.trim().toLowerCase(Locale.ROOT);
      if (filterType.length() == 0)
        filterType = null;
    }
    FilterType type = (filterType != null) ? FilterType.valueOf(filterType) : FilterType.none;

    String filter = (type != FilterType.none) ? params.get("filter") : null;
    if (filter != null) {
      filter = filter.trim();
      if (filter.length() == 0)
        filter = null;
    }

    ZKPrinter printer = new ZKPrinter(cores.getZkController());
    printer.detail = detail;
    printer.dump = dump;
    boolean isGraphView = "graph".equals(params.get("view"));
    printer.page = (isGraphView && "/clusterstate.json".equals(path))
        ? new PageOfCollections(start, rows, type, filter) : null;
    printer.pagingSupport = pagingSupport;

    try {
      printer.print(path);
    } finally {
      printer.close();
    }
    rsp.getValues().add(RawResponseWriter.CONTENT,printer);
  }

  //--------------------------------------------------------------------------------------
  //
  //--------------------------------------------------------------------------------------

  static class ZKPrinter implements ContentStream {
    static boolean FULLPATH_DEFAULT = false;

    boolean indent = true;
    boolean fullpath = FULLPATH_DEFAULT;
    boolean detail = false;
    boolean dump = false;

    String keeperAddr; // the address we're connected to

    final BAOS baos = new BAOS();
    final Writer out = new OutputStreamWriter(baos,  StandardCharsets.UTF_8);
    SolrZkClient zkClient;

    PageOfCollections page;
    PagedCollectionSupport pagingSupport;
    ZkController zkController;

    public ZKPrinter(ZkController controller) throws IOException {
      this.zkController = controller;
      keeperAddr = controller.getZkServerAddress();
      zkClient = controller.getZkClient();
    }

    public void close() {
      try {
        out.flush();
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }

    // main entry point
    void print(String path) throws IOException {
      if (zkClient == null) {
        return;
      }

      // normalize path
      if (path == null) {
        path = "/";
      } else {
        path = path.trim();
        if (path.length() == 0) {
          path = "/";
        }
      }

      if (path.endsWith("/") && path.length() > 1) {
        path = path.substring(0, path.length() - 1);
      }

      int idx = path.lastIndexOf('/');
      String parent = idx >= 0 ? path.substring(0, idx) : path;
      if (parent.length() == 0) {
        parent = "/";
      }

      CharArr chars = new CharArr();
      JSONWriter json = new JSONWriter(chars, 2);
      json.startObject();

      if (detail) {
        if (!printZnode(json, path)) {
          return;
        }
        json.writeValueSeparator();
      }

      json.writeString("tree");
      json.writeNameSeparator();
      json.startArray();
      if (!printTree(json, path)) {
        return; // there was an error
      }
      json.endArray();
      json.endObject();
      out.write(chars.toString());
    }

    void writeError(int code, String msg) throws IOException {
      throw new SolrException(ErrorCode.getErrorCode(code), msg);
      /*response.setStatus(code);

      CharArr chars = new CharArr();
      JSONWriter w = new JSONWriter(chars, 2);
      w.startObject();
      w.indent();
      w.writeString("status");
      w.writeNameSeparator();
      w.write(code);
      w.writeValueSeparator();
      w.indent();
      w.writeString("error");
      w.writeNameSeparator();
      w.writeString(msg);
      w.endObject();

      out.write(chars.toString());*/
    }


    boolean printTree(JSONWriter json, String path) throws IOException {
      String label = path;
      if (!fullpath) {
        int idx = path.lastIndexOf('/');
        label = idx > 0 ? path.substring(idx + 1) : path;
      }
      json.startObject();
      writeKeyValue(json, "text", label, true);
      json.writeValueSeparator();
      json.writeString("a_attr");
      json.writeNameSeparator();
      json.startObject();
      String href = "admin/zookeeper?detail=true&path=" + URLEncoder.encode(path, "UTF-8");
      writeKeyValue(json, "href", href, true);
      json.endObject();

      Stat stat = new Stat();
      try {
        // Trickily, the call to zkClient.getData fills in the stat variable
        byte[] data = zkClient.getData(path, null, stat, true);

        if (stat.getEphemeralOwner() != 0) {
          writeKeyValue(json, "ephemeral", true, false);
          writeKeyValue(json, "version", stat.getVersion(), false);
        }

        if (dump) {
          json.writeValueSeparator();
          printZnode(json, path);
        }

      } catch (IllegalArgumentException e) {
        // path doesn't exist (must have been removed)
        writeKeyValue(json, "warning", "(path gone)", false);
      } catch (KeeperException e) {
        writeKeyValue(json, "warning", e.toString(), false);
        log.warn("Keeper Exception", e);
      } catch (InterruptedException e) {
        writeKeyValue(json, "warning", e.toString(), false);
        log.warn("InterruptedException", e);
      }

      if (stat.getNumChildren() > 0) {
        json.writeValueSeparator();
        if (indent) {
          json.indent();
        }
        json.writeString("children");
        json.writeNameSeparator();
        json.startArray();

        try {
          List children = zkClient.getChildren(path, null, true);
          java.util.Collections.sort(children);

          boolean first = true;
          for (String child : children) {
            if (!first) {
              json.writeValueSeparator();
            }

            String childPath = path + (path.endsWith("/") ? "" : "/") + child;
            if (!printTree(json, childPath)) {
              return false;
            }
            first = false;
          }
        } catch (KeeperException e) {
          writeError(500, e.toString());
          return false;
        } catch (InterruptedException e) {
          writeError(500, e.toString());
          return false;
        } catch (IllegalArgumentException e) {
          // path doesn't exist (must have been removed)
          json.writeString("(children gone)");
        }

        json.endArray();
      }

      json.endObject();
      return true;
    }

    String time(long ms) {
      return (new Date(ms)).toString() + " (" + ms + ")";
    }

    public void writeKeyValue(JSONWriter json, String k, Object v, boolean isFirst) {
      if (!isFirst) {
        json.writeValueSeparator();
      }
      if (indent) {
        json.indent();
      }
      json.writeString(k);
      json.writeNameSeparator();
      json.write(v);
    }

    @SuppressWarnings("unchecked")
    boolean printZnode(JSONWriter json, String path) throws IOException {
      try {
        String dataStr = null;
        String dataStrErr = null;
        Stat stat = new Stat();
        // Trickily, the call to zkClient.getData fills in the stat variable
        byte[] data = zkClient.getData(path, null, stat, true);
        if (null != data) {
          try {
            dataStr = (new BytesRef(data)).utf8ToString();
          } catch (Exception e) {
            dataStrErr = "data is not parsable as a utf8 String: " + e.toString();
          }
        }
        // support paging of the collections graph view (in case there are many collections)
        if (page != null) {
          // we've already pulled the data for /clusterstate.json from ZooKeeper above,
          // but it needs to be parsed into a map so we can lookup collection states before
          // trying to find them in the /collections/?/state.json znode
          Map clusterstateJsonMap = null;
          if (dataStr != null) {
            try {
              clusterstateJsonMap = (Map) Utils.fromJSONString(dataStr);
            } catch (Exception e) {
              throw new SolrException(ErrorCode.SERVER_ERROR,
                  "Failed to parse /clusterstate.json from ZooKeeper due to: " + e, e);
            }
          } else {
            clusterstateJsonMap = Utils.makeMap();
          }

          // fetch the requested page of collections and then retrieve the state for each 
          page = pagingSupport.fetchPage(page, zkClient);
          // keep track of how many collections match the filter
          boolean applyStatusFilter =
              (page.filterType == FilterType.status && page.filter != null);
          List matchesStatusFilter = applyStatusFilter ? new ArrayList<>() : null;
          ClusterState cs = zkController.getZkStateReader().getClusterState();
          Set liveNodes = applyStatusFilter ? cs.getLiveNodes() : null;

          SortedMap collectionStates = new TreeMap(pagingSupport);
          for (String collection : page.selected) {
            Object collectionState = clusterstateJsonMap.get(collection);
            if (collectionState != null) {
              // collection state was in /clusterstate.json
              if (applyStatusFilter) {
                // verify this collection matches the status filter
                if (page.matchesStatusFilter((Map) collectionState, liveNodes)) {
                  matchesStatusFilter.add(collection);
                  collectionStates.put(collection, ClusterStatus.postProcessCollectionJSON((Map) collectionState));
                }
              } else {
                collectionStates.put(collection, ClusterStatus.postProcessCollectionJSON((Map) collectionState));
              }
            } else {
              // looks like an external collection ... just use DocCollection instead of reading ZK data directly to work with per replica states
              DocCollection dc = cs.getCollectionOrNull(collection);
              if (dc != null) {
                // TODO: for collections with perReplicaState, a ser/deser to JSON was needed to get the state to render correctly for the UI?
                collectionState = dc.isPerReplicaState() ? Utils.fromJSONString(Utils.toJSONString(dc)) : dc.getProperties();
                if (applyStatusFilter) {
                  // verify this collection matches the filtered state
                  if (page.matchesStatusFilter((Map) collectionState, liveNodes)) {
                    matchesStatusFilter.add(collection);
                    collectionStates.put(collection, ClusterStatus.postProcessCollectionJSON((Map) collectionState));
                  }
                } else {
                  collectionStates.put(collection, ClusterStatus.postProcessCollectionJSON((Map) collectionState));
                }
              }
            }
          }

          if (applyStatusFilter) {
            // update the paged navigation info after applying the status filter
            page.selectPage(matchesStatusFilter);

            // rebuild the Map of state data
            SortedMap map = new TreeMap(pagingSupport);
            for (String next : page.selected)
              map.put(next, collectionStates.get(next));
            collectionStates = map;
          }

          if (collectionStates != null) {
            CharArr out = new CharArr();
            new JSONWriter(out, 2).write(collectionStates);
            dataStr = out.toString();
          }
        }

        json.writeString("znode");
        json.writeNameSeparator();
        json.startObject();

        writeKeyValue(json, PATH, path, true);

        json.writeValueSeparator();
        json.writeString("prop");
        json.writeNameSeparator();
        json.startObject();
        writeKeyValue(json, "version", stat.getVersion(), true);
        writeKeyValue(json, "aversion", stat.getAversion(), false);
        writeKeyValue(json, "children_count", stat.getNumChildren(), false);
        writeKeyValue(json, "ctime", time(stat.getCtime()), false);
        writeKeyValue(json, "cversion", stat.getCversion(), false);
        writeKeyValue(json, "czxid", stat.getCzxid(), false);
        writeKeyValue(json, "ephemeralOwner", stat.getEphemeralOwner(), false);
        writeKeyValue(json, "mtime", time(stat.getMtime()), false);
        writeKeyValue(json, "mzxid", stat.getMzxid(), false);
        writeKeyValue(json, "pzxid", stat.getPzxid(), false);
        writeKeyValue(json, "dataLength", stat.getDataLength(), false);
        if (null != dataStrErr) {
          writeKeyValue(json, "dataNote", dataStrErr, false);
        }
        json.endObject();

        if (null != dataStr) {
          writeKeyValue(json, "data", dataStr, false);
        }

        if (page != null) {
          writeKeyValue(json, "paging", page.getPagingHeader(), false);
        }

        json.endObject();
      } catch (KeeperException e) {
        writeError(500, e.toString());
        return false;
      } catch (InterruptedException e) {
        writeError(500, e.toString());
        return false;
      }
      return true;
    }

   /* @Override
    public void write(OutputStream os) throws IOException {
      ByteBuffer bytes = baos.getByteBuffer();
      os.write(bytes.array(),0,bytes.limit());
    }
*/
    @Override
    public String getName() {
      return null;
    }

    @Override
    public String getSourceInfo() {
      return null;
    }

    @Override
    public String getContentType() {
      return JSONResponseWriter.CONTENT_TYPE_JSON_UTF8;
    }

    @Override
    public Long getSize() {
      return null;
    }

    @Override
    public InputStream getStream() throws IOException {
      return new ByteBufferInputStream(baos.getByteBuffer());
    }

    @Override
    public Reader getReader() throws IOException {
      return null;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy