github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ui/src/redux/nodes.spec.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 { assert } from "chai"; 12 import { createHashHistory } from "history"; 13 14 import {MetricConstants, INodeStatus} from "src/util/proto"; 15 import * as protos from "src/js/protos"; 16 17 import { 18 nodeDisplayNameByIDSelector, 19 selectCommissionedNodeStatuses, 20 selectStoreIDsByNodeID, 21 LivenessStatus, 22 sumNodeStats, 23 } from "./nodes"; 24 import { nodesReducerObj, livenessReducerObj } from "./apiReducers"; 25 import { createAdminUIStore } from "./state"; 26 27 function makeNodesState(...addresses: { id: number, address: string, status?: LivenessStatus }[]) { 28 const nodeData = addresses.map(addr => { 29 return { 30 desc : { 31 node_id: addr.id, 32 address: { 33 address_field: addr.address, 34 }, 35 }, 36 }; 37 }); 38 const livenessData: {statuses: {[key: string]: LivenessStatus}} = { 39 statuses: {}, 40 }; 41 addresses.forEach(addr => { 42 livenessData.statuses[addr.id] = addr.status || LivenessStatus.LIVE; 43 }); 44 const store = createAdminUIStore(createHashHistory()); 45 store.dispatch(nodesReducerObj.receiveData(nodeData)); 46 store.dispatch(livenessReducerObj.receiveData(new protos.cockroach.server.serverpb.LivenessResponse(livenessData))); 47 return store.getState(); 48 } 49 50 describe("node data selectors", function() { 51 describe("display name by ID", function() { 52 it("display name is node id appended to address", function() { 53 const state: any = makeNodesState( 54 { id: 1, address: "addressA" }, 55 { id: 2, address: "addressB" }, 56 { id: 3, address: "addressC" }, 57 { id: 4, address: "addressD" }, 58 ); 59 60 const addressesByID = nodeDisplayNameByIDSelector(state); 61 assert.deepEqual(addressesByID, { 62 1: "addressA (n1)", 63 2: "addressB (n2)", 64 3: "addressC (n3)", 65 4: "addressD (n4)", 66 }); 67 }); 68 69 it("generates unique names for re-used addresses", function() { 70 const state: any = makeNodesState( 71 { id: 1, address: "addressA" }, 72 { id: 2, address: "addressB" }, 73 { id: 3, address: "addressC" }, 74 { id: 4, address: "addressD" }, 75 { id: 5, address: "addressA" }, 76 { id: 6, address: "addressC" }, 77 { id: 7, address: "addressA" }, 78 ); 79 80 const addressesByID = nodeDisplayNameByIDSelector(state); 81 assert.deepEqual(addressesByID, { 82 1: "addressA (n1)", 83 2: "addressB (n2)", 84 3: "addressC (n3)", 85 4: "addressD (n4)", 86 5: "addressA (n5)", 87 6: "addressC (n6)", 88 7: "addressA (n7)", 89 }); 90 }); 91 92 it("adds decommissioned flag to decommissioned nodes", function() { 93 const state: any = makeNodesState( 94 { id: 1, address: "addressA", status: LivenessStatus.DECOMMISSIONED }, 95 { id: 2, address: "addressB" }, 96 { id: 3, address: "addressC", status: LivenessStatus.DECOMMISSIONED }, 97 { id: 4, address: "addressD", status: LivenessStatus.DEAD }, 98 { id: 5, address: "addressA", status: LivenessStatus.DECOMMISSIONED }, 99 { id: 6, address: "addressC" }, 100 { id: 7, address: "addressA" }, 101 { id: 8, address: "addressE", status: LivenessStatus.DECOMMISSIONING }, 102 { id: 9, address: "addressF", status: LivenessStatus.UNAVAILABLE }, 103 ); 104 105 const addressesByID = nodeDisplayNameByIDSelector(state); 106 assert.equal(addressesByID[1], "[decommissioned] addressA (n1)"); 107 assert.deepEqual(addressesByID, { 108 1: "[decommissioned] addressA (n1)", 109 2: "addressB (n2)", 110 3: "[decommissioned] addressC (n3)", 111 4: "addressD (n4)", 112 5: "[decommissioned] addressA (n5)", 113 6: "addressC (n6)", 114 7: "addressA (n7)", 115 8: "addressE (n8)", 116 9: "addressF (n9)", 117 }); 118 }); 119 120 it("returns empty collection for empty state", function() { 121 const store = createAdminUIStore(createHashHistory()); 122 assert.deepEqual(nodeDisplayNameByIDSelector(store.getState()), {}); 123 }); 124 }); 125 126 describe("store IDs by node ID", function() { 127 it("correctly creates storeID map", function() { 128 const data = [ 129 { 130 desc: { node_id: 1 }, 131 store_statuses: [ 132 { desc: { store_id: 1 }}, 133 { desc: { store_id: 2 }}, 134 { desc: { store_id: 3 }}, 135 ], 136 }, 137 { 138 desc: { node_id: 2 }, 139 store_statuses: [ 140 { desc: { store_id: 4 }}, 141 ], 142 }, 143 { 144 desc: { node_id: 3 }, 145 store_statuses: [ 146 { desc: { store_id: 5 }}, 147 { desc: { store_id: 6 }}, 148 ], 149 }, 150 ]; 151 const store = createAdminUIStore(createHashHistory()); 152 store.dispatch(nodesReducerObj.receiveData(data)); 153 const state = store.getState(); 154 155 assert.deepEqual(selectStoreIDsByNodeID(state), { 156 1: ["1", "2", "3"], 157 2: ["4"], 158 3: ["5", "6"], 159 }); 160 }); 161 }); 162 }); 163 164 describe("selectCommissionedNodeStatuses", function() { 165 const nodeStatuses: INodeStatus[] = [ 166 { 167 desc: { 168 node_id: 1, 169 }, 170 }, 171 ]; 172 173 function makeStateForLiveness(livenessStatuses: { [id: string]: LivenessStatus }) { 174 return { 175 cachedData: { 176 nodes: { 177 data: nodeStatuses, 178 inFlight: false, 179 valid: true, 180 }, 181 liveness: { 182 data: { 183 statuses: livenessStatuses, 184 }, 185 inFlight: false, 186 valid: true, 187 }, 188 }, 189 }; 190 } 191 192 it("selects all nodes when liveness status missing", function() { 193 const state = makeStateForLiveness({}); 194 195 const result = selectCommissionedNodeStatuses(state); 196 197 assert.deepEqual(result, nodeStatuses); 198 }); 199 200 const testCases: [string, LivenessStatus, INodeStatus[]][] = [ 201 ["excludes decommissioned nodes", LivenessStatus.DECOMMISSIONED, []], 202 ["includes decommissioning nodes", LivenessStatus.DECOMMISSIONING, nodeStatuses], 203 ["includes live nodes", LivenessStatus.LIVE, nodeStatuses], 204 ["includes unavailable nodes", LivenessStatus.UNAVAILABLE, nodeStatuses], 205 ["includes dead nodes", LivenessStatus.DEAD, nodeStatuses], 206 ]; 207 208 testCases.forEach(([name, status, expected]) => { 209 it(name, function() { 210 const state = makeStateForLiveness({ "1": status }); 211 212 const result = selectCommissionedNodeStatuses(state); 213 214 assert.deepEqual(result, expected); 215 }); 216 }); 217 }); 218 219 describe("sumNodeStats", function() { 220 it("sums stats from an array of nodes", function() { 221 // Each of these nodes only has half of its capacity "usable" for cockroach data. 222 // See diagram for what these stats mean: 223 // https://github.com/cockroachdb/cockroach/blob/31e4299ab73a43f539b1ba63ed86be5ee18685f6/pkg/storage/metrics.go#L145-L153 224 const nodeStatuses: INodeStatus[] = [ 225 { 226 desc: { node_id: 1 }, 227 metrics: { 228 [MetricConstants.capacity]: 100, 229 [MetricConstants.usedCapacity]: 10, 230 [MetricConstants.availableCapacity]: 40, 231 }, 232 }, 233 { 234 desc: { node_id: 2 }, 235 metrics: { 236 [MetricConstants.capacity]: 100, 237 [MetricConstants.usedCapacity]: 10, 238 [MetricConstants.availableCapacity]: 40, 239 }, 240 }, 241 ]; 242 const livenessStatusByNodeID: { [key: string]: LivenessStatus } = { 243 1: LivenessStatus.LIVE, 244 2: LivenessStatus.LIVE, 245 }; 246 const actual = sumNodeStats(nodeStatuses, livenessStatusByNodeID); 247 assert.equal(actual.nodeCounts.healthy, 2); 248 assert.equal(actual.capacityTotal, 200); 249 assert.equal(actual.capacityUsed, 20); 250 // usable = used + available. 251 assert.equal(actual.capacityUsable, 100); 252 }); 253 });