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 );