github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/redux/nodes.ts (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  import _ from "lodash";
    12  import { createSelector } from "reselect";
    13  
    14  import * as protos from "src/js/protos";
    15  import { AdminUIState } from "./state";
    16  import { Pick } from "src/util/pick";
    17  import { NoConnection } from "src/views/reports/containers/network";
    18  import { INodeStatus, MetricConstants, BytesUsed } from "src/util/proto";
    19  import { nullOfReturnType } from "src/util/types";
    20  
    21  /**
    22   * LivenessStatus is a type alias for the fully-qualified NodeLivenessStatus
    23   * enumeration. As an enum, it needs to be imported rather than using the 'type'
    24   * keyword.
    25   */
    26  export import LivenessStatus = protos.cockroach.kv.kvserver.storagepb.NodeLivenessStatus;
    27  
    28  /**
    29   * livenessNomenclature resolves a mismatch between the terms used for liveness
    30   * status on our Admin UI and the terms used by the backend. Examples:
    31   * + "Live" on the server is "Healthy" on the Admin UI
    32   * + "Unavailable" on the server is "Suspect" on the Admin UI
    33   */
    34  export function livenessNomenclature(liveness: LivenessStatus) {
    35    switch (liveness) {
    36      case LivenessStatus.LIVE:
    37        return "healthy";
    38      case LivenessStatus.UNAVAILABLE:
    39        return "suspect";
    40      case LivenessStatus.DECOMMISSIONING:
    41        return "decommissioning";
    42      case LivenessStatus.DECOMMISSIONED:
    43        return "decommissioned";
    44      default:
    45        return "dead";
    46    }
    47  }
    48  
    49  // Functions to select data directly from the redux state.
    50  const livenessesSelector = (state: AdminUIState) => state.cachedData.liveness.data;
    51  
    52  /*
    53   * nodeStatusesSelector returns the current status for each node in the cluster.
    54   */
    55  type NodeStatusState = Pick<AdminUIState, "cachedData", "nodes">;
    56  export const nodeStatusesSelector = (state: NodeStatusState) => state.cachedData.nodes.data;
    57  
    58  /*
    59   * clusterSelector returns information about cluster.
    60   */
    61  export const clusterSelector = (state: AdminUIState) => state.cachedData.cluster.data;
    62  
    63  /*
    64   * clusterIdSelector returns Cluster Id (as UUID string).
    65   */
    66  export const clusterIdSelector = createSelector(
    67    clusterSelector,
    68    (clusterInfo) => clusterInfo && clusterInfo.cluster_id,
    69  );
    70  /*
    71   * selectNodeRequestStatus returns the current status of the node status request.
    72   */
    73  export function selectNodeRequestStatus(state: AdminUIState) {
    74    return state.cachedData.nodes;
    75  }
    76  
    77  /**
    78   * livenessByNodeIDSelector returns a map from NodeID to the Liveness record for
    79   * that node.
    80   */
    81  export const livenessByNodeIDSelector = createSelector(
    82    livenessesSelector,
    83    (livenesses) => {
    84      if (livenesses) {
    85        return _.keyBy(livenesses.livenesses, (l) => l.node_id);
    86      }
    87      return {};
    88    },
    89  );
    90  
    91  /*
    92   * selectLivenessRequestStatus returns the current status of the liveness request.
    93   */
    94  export function selectLivenessRequestStatus(state: AdminUIState) {
    95    return state.cachedData.liveness;
    96  }
    97  
    98  /**
    99   * livenessStatusByNodeIDSelector returns a map from NodeID to the
   100   * LivenessStatus of that node.
   101   */
   102  export const livenessStatusByNodeIDSelector = createSelector(
   103    livenessesSelector,
   104    (livenesses) => livenesses ? (livenesses.statuses || {}) : {},
   105  );
   106  
   107  /*
   108   * selectCommissionedNodeStatuses returns the node statuses for nodes that have
   109   * not been decommissioned.
   110   */
   111  export const selectCommissionedNodeStatuses = createSelector(
   112    nodeStatusesSelector,
   113    livenessStatusByNodeIDSelector,
   114    (nodeStatuses, livenessStatuses) => {
   115      return _.filter(nodeStatuses, (node) => {
   116        const livenessStatus = livenessStatuses[`${node.desc.node_id}`];
   117  
   118        return _.isNil(livenessStatus) || livenessStatus !== LivenessStatus.DECOMMISSIONED;
   119      });
   120    },
   121  );
   122  
   123  /**
   124   * nodeIDsSelector returns the NodeID of all nodes currently on the cluster.
   125   */
   126  const nodeIDsSelector = createSelector(
   127    nodeStatusesSelector,
   128    (nodeStatuses) => {
   129      return _.map(nodeStatuses, (ns) => ns.desc.node_id.toString());
   130    },
   131  );
   132  
   133  /**
   134   * nodeStatusByIDSelector returns a map from NodeID to a current INodeStatus.
   135   */
   136  const nodeStatusByIDSelector = createSelector(
   137    nodeStatusesSelector,
   138    (nodeStatuses) => {
   139      const statuses: {[s: string]: INodeStatus} = {};
   140      _.each(nodeStatuses, (ns) => {
   141        statuses[ns.desc.node_id.toString()] = ns;
   142      });
   143      return statuses;
   144    },
   145  );
   146  
   147  /**
   148   * nodeSumsSelector returns an object with certain cluster-wide totals which are
   149   * used in different places in the UI.
   150   */
   151  const nodeSumsSelector = createSelector(
   152    nodeStatusesSelector,
   153    livenessStatusByNodeIDSelector,
   154    sumNodeStats,
   155  );
   156  
   157  export function sumNodeStats(
   158    nodeStatuses: INodeStatus[],
   159    livenessStatusByNodeID: { [id: string]: LivenessStatus },
   160  ) {
   161    const result = {
   162      nodeCounts: {
   163        total: 0,
   164        healthy: 0,
   165        suspect: 0,
   166        dead: 0,
   167        decommissioned: 0,
   168      },
   169      capacityUsed: 0,
   170      capacityAvailable: 0,
   171      capacityTotal: 0,
   172      capacityUsable: 0,
   173      usedBytes: 0,
   174      usedMem: 0,
   175      totalRanges: 0,
   176      underReplicatedRanges: 0,
   177      unavailableRanges: 0,
   178      replicas: 0,
   179    };
   180    if (_.isArray(nodeStatuses) && _.isObject(livenessStatusByNodeID)) {
   181      nodeStatuses.forEach((n) => {
   182        const status = livenessStatusByNodeID[n.desc.node_id];
   183        if (status !== LivenessStatus.DECOMMISSIONED) {
   184          result.nodeCounts.total += 1;
   185        }
   186        switch (status) {
   187          case LivenessStatus.LIVE:
   188            result.nodeCounts.healthy++;
   189            break;
   190          case LivenessStatus.UNAVAILABLE:
   191          case LivenessStatus.DECOMMISSIONING:
   192            result.nodeCounts.suspect++;
   193            break;
   194          case LivenessStatus.DECOMMISSIONED:
   195            result.nodeCounts.decommissioned++;
   196            break;
   197          case LivenessStatus.DEAD:
   198          default:
   199            result.nodeCounts.dead++;
   200            break;
   201        }
   202        if (status !== LivenessStatus.DEAD && status !== LivenessStatus.DECOMMISSIONED) {
   203          const { available, used, usable } = nodeCapacityStats(n);
   204  
   205          result.capacityUsed += used;
   206          result.capacityAvailable += available;
   207          result.capacityUsable += usable;
   208          result.capacityTotal += n.metrics[MetricConstants.capacity];
   209          result.usedBytes += BytesUsed(n);
   210          result.usedMem += n.metrics[MetricConstants.rss];
   211          result.totalRanges += n.metrics[MetricConstants.ranges];
   212          result.underReplicatedRanges += n.metrics[MetricConstants.underReplicatedRanges];
   213          result.unavailableRanges += n.metrics[MetricConstants.unavailableRanges];
   214          result.replicas += n.metrics[MetricConstants.replicas];
   215        }
   216      });
   217    }
   218    return result;
   219  }
   220  
   221  export interface CapacityStats {
   222    used: number;
   223    usable: number;
   224    available: number;
   225  }
   226  
   227  export function nodeCapacityStats(n: INodeStatus): CapacityStats {
   228    const used = n.metrics[MetricConstants.usedCapacity];
   229    const available = n.metrics[MetricConstants.availableCapacity];
   230    return {
   231      used,
   232      available,
   233      usable: used + available,
   234    };
   235  }
   236  
   237  export function getDisplayName(node: INodeStatus | NoConnection, livenessStatus = LivenessStatus.LIVE) {
   238    const decommissionedString = livenessStatus === LivenessStatus.DECOMMISSIONED
   239      ? "[decommissioned] "
   240      : "";
   241  
   242    if (isNoConnection(node)) {
   243      return `${decommissionedString} (n${node.from.nodeID})`;
   244    }
   245    // as the only other type possible right now is INodeStatus we don't have a type guard for that
   246    return `${decommissionedString}${node.desc.address.address_field} (n${node.desc.node_id})`;
   247  }
   248  
   249  function isNoConnection(node: INodeStatus | NoConnection): node is NoConnection {
   250    return (node as NoConnection).to !== undefined && (node as NoConnection).from !== undefined;
   251  }
   252  
   253  // nodeDisplayNameByIDSelector provides a unique, human-readable display name
   254  // for each node.
   255  export const nodeDisplayNameByIDSelector = createSelector(
   256    nodeStatusesSelector,
   257    livenessStatusByNodeIDSelector,
   258    (nodeStatuses, livenessStatusByNodeID) => {
   259      const result: {[key: string]: string} = {};
   260      if (!_.isEmpty(nodeStatuses)) {
   261        nodeStatuses.forEach(ns => {
   262          result[ns.desc.node_id] = getDisplayName(
   263            ns, livenessStatusByNodeID[ns.desc.node_id],
   264          );
   265        });
   266      }
   267      return result;
   268    },
   269  );
   270  
   271  // selectStoreIDsByNodeID returns a map from node ID to a list of store IDs for
   272  // that node. Like nodeIDsSelector, the store ids are converted to strings.
   273  export const selectStoreIDsByNodeID = createSelector(
   274    nodeStatusesSelector,
   275    (nodeStatuses) => {
   276      const result: {[key: string]: string[]} = {};
   277      _.each(nodeStatuses, ns =>
   278          result[ns.desc.node_id] = _.map(ns.store_statuses, ss => ss.desc.store_id.toString()),
   279      );
   280      return result;
   281    },
   282  );
   283  
   284  /**
   285   * nodesSummarySelector returns a directory object containing a variety of
   286   * computed information based on the current nodes. This object is easy to
   287   * connect to components on child pages.
   288   */
   289  export const nodesSummarySelector = createSelector(
   290    nodeStatusesSelector,
   291    nodeIDsSelector,
   292    nodeStatusByIDSelector,
   293    nodeSumsSelector,
   294    nodeDisplayNameByIDSelector,
   295    livenessStatusByNodeIDSelector,
   296    livenessByNodeIDSelector,
   297    selectStoreIDsByNodeID,
   298    (nodeStatuses, nodeIDs, nodeStatusByID, nodeSums, nodeDisplayNameByID, livenessStatusByNodeID, livenessByNodeID, storeIDsByNodeID) => {
   299      return {
   300        nodeStatuses,
   301        nodeIDs,
   302        nodeStatusByID,
   303        nodeSums,
   304        nodeDisplayNameByID,
   305        livenessStatusByNodeID,
   306        livenessByNodeID,
   307        storeIDsByNodeID,
   308      };
   309    },
   310  );
   311  
   312  const nodesSummaryType = nullOfReturnType(nodesSummarySelector);
   313  export type NodesSummary = typeof nodesSummaryType;
   314  
   315  // selectNodesSummaryValid is a selector that returns true if the current
   316  // nodesSummary is "valid" (i.e. based on acceptably recent data). This is
   317  // included in the redux-connected state of some pages in order to support
   318  // automatically refreshing data.
   319  export function selectNodesSummaryValid(state: AdminUIState) {
   320    return state.cachedData.nodes.valid && state.cachedData.liveness.valid;
   321  }
   322  
   323  /*
   324   * clusterNameSelector returns the name of cluster which has to be the same for every node in the cluster.
   325   * - That is why it is safe to get first non empty cluster name.
   326   * - Empty cluster name is possible in case `DisableClusterNameVerification` flag is used (see pkg/base/config.go:176).
   327   */
   328  export const clusterNameSelector = createSelector(
   329    nodeStatusesSelector,
   330    livenessStatusByNodeIDSelector,
   331    (nodeStatuses, livenessStatusByNodeID): string => {
   332      if (_.isUndefined(nodeStatuses) || _.isEmpty(livenessStatusByNodeID)) {
   333        return undefined;
   334      }
   335      const liveNodesOnCluster = nodeStatuses.filter(
   336        nodeStatus => livenessStatusByNodeID[nodeStatus.desc.node_id] === LivenessStatus.LIVE);
   337  
   338      const nodesWithUniqClusterNames = _.chain(liveNodesOnCluster)
   339        .filter(node => !_.isEmpty(node.desc.cluster_name))
   340        .uniqBy(node => node.desc.cluster_name)
   341        .value();
   342  
   343      if (_.isEmpty(nodesWithUniqClusterNames)) {
   344        return undefined;
   345      } else {
   346        return _.head(nodesWithUniqClusterNames).desc.cluster_name;
   347      }
   348    });
   349  
   350  export const versionsSelector = createSelector(
   351    nodeStatusesSelector,
   352    livenessByNodeIDSelector,
   353    (nodeStatuses, livenessStatusByNodeID) =>
   354      _.chain(nodeStatuses)
   355        // Ignore nodes for which we don't have any build info.
   356        .filter((status) => !!status.build_info )
   357        // Exclude this node if it's known to be decommissioning.
   358        .filter((status) => !status.desc ||
   359          !livenessStatusByNodeID[status.desc.node_id] ||
   360          !livenessStatusByNodeID[status.desc.node_id].decommissioning)
   361        // Collect the surviving nodes' build tags.
   362        .map((status) => status.build_info.tag)
   363        .uniq()
   364        .value(),
   365  );
   366  
   367  // Select the current build version of the cluster, returning undefined if the
   368  // cluster's version is currently staggered.
   369  export const singleVersionSelector = createSelector(
   370    versionsSelector,
   371    (builds) => {
   372      if (!builds || builds.length !== 1) {
   373        return undefined;
   374      }
   375      return builds[0];
   376    },
   377  );
   378  
   379  /**
   380   * partitionedStatuses divides the list of node statuses into "live" and "dead".
   381   */
   382  export const partitionedStatuses = createSelector(
   383    nodesSummarySelector,
   384    (summary) => {
   385      return _.groupBy(
   386        summary.nodeStatuses,
   387        (ns) => {
   388          switch (summary.livenessStatusByNodeID[ns.desc.node_id]) {
   389            case LivenessStatus.LIVE:
   390            case LivenessStatus.UNAVAILABLE:
   391            case LivenessStatus.DEAD:
   392            case LivenessStatus.DECOMMISSIONING:
   393              return "live";
   394            case LivenessStatus.DECOMMISSIONED:
   395              return "decommissioned";
   396            default:
   397              // TODO (koorosh): "live" has to be renamed to some partition which
   398              // represent all except "partitioned" nodes.
   399              return "live";
   400          }
   401        },
   402      );
   403    },
   404  );