245 lines
7.8 KiB
JavaScript
245 lines
7.8 KiB
JavaScript
// Copyright (c) Vitaliy Filippov, 2019+
|
|
// License: VNPL-1.1 (see README.md for details)
|
|
|
|
// Extract OSDs from the lowest affected tree level into a separate (flat) map
|
|
// to run PG optimisation on failure domains instead of individual OSDs
|
|
//
|
|
// node_list = same input as for index_tree()
|
|
// rules = [ level, operator, value ][][]
|
|
// returns { nodes: new_node_list, leaves: { new_folded_node_id: [ extracted_leaf_nodes... ] } }
|
|
function fold_failure_domains(node_list, rules)
|
|
{
|
|
const interest = {};
|
|
for (const level_rules of rules)
|
|
{
|
|
for (const rule of level_rules)
|
|
interest[rule[0]] = true;
|
|
}
|
|
const max_numeric_id = node_list.reduce((a, c) => a < (0|c.id) ? (0|c.id) : a, 0);
|
|
let next_id = max_numeric_id;
|
|
const node_map = node_list.reduce((a, c) => { a[c.id||''] = c; return a; }, {});
|
|
const old_ids_by_new = {};
|
|
const extracted_nodes = {};
|
|
let folded = true;
|
|
while (folded)
|
|
{
|
|
const per_parent = {};
|
|
for (const node_id in node_map)
|
|
{
|
|
const node = node_map[node_id];
|
|
const p = node.parent || '';
|
|
per_parent[p] = per_parent[p]||[];
|
|
per_parent[p].push(node);
|
|
}
|
|
folded = false;
|
|
for (const node_id in per_parent)
|
|
{
|
|
const fold_node = node_id !== '' && per_parent[node_id].length > 0 && per_parent[node_id].filter(child => per_parent[child.id||''] || interest[child.level]).length == 0;
|
|
if (fold_node)
|
|
{
|
|
const old_node = node_map[node_id];
|
|
const new_id = ++next_id;
|
|
node_map[new_id] = {
|
|
...old_node,
|
|
id: new_id,
|
|
name: node_id, // for use in murmur3 hashes
|
|
size: per_parent[node_id].reduce((a, c) => a + (Number(c.size)||0), 0),
|
|
};
|
|
delete node_map[node_id];
|
|
old_ids_by_new[new_id] = node_id;
|
|
extracted_nodes[new_id] = [];
|
|
for (const child of per_parent[node_id])
|
|
{
|
|
if (old_ids_by_new[child.id])
|
|
{
|
|
extracted_nodes[new_id].push(...extracted_nodes[child.id]);
|
|
delete extracted_nodes[child.id];
|
|
}
|
|
else
|
|
extracted_nodes[new_id].push(child);
|
|
delete node_map[child.id];
|
|
}
|
|
folded = true;
|
|
}
|
|
}
|
|
}
|
|
return { nodes: Object.values(node_map), leaves: extracted_nodes };
|
|
}
|
|
|
|
// Distribute PGs mapped to "folded" nodes to individual OSDs according to their weights
|
|
// folded_pgs = optimize_result.int_pgs before folding
|
|
// prev_pgs = optional previous PGs from optimize_change() input
|
|
// extracted_nodes = output from fold_failure_domains
|
|
function unfold_failure_domains(folded_pgs, prev_pgs, extracted_nodes)
|
|
{
|
|
const maps = {};
|
|
let found = false;
|
|
for (const new_id in extracted_nodes)
|
|
{
|
|
const weights = {};
|
|
for (const sub_node of extracted_nodes[new_id])
|
|
{
|
|
weights[sub_node.id] = sub_node.size;
|
|
}
|
|
maps[new_id] = { weights, prev: [], next: [], pos: 0 };
|
|
found = true;
|
|
}
|
|
if (!found)
|
|
{
|
|
return folded_pgs;
|
|
}
|
|
for (let i = 0; i < folded_pgs.length; i++)
|
|
{
|
|
for (let j = 0; j < folded_pgs[i].length; j++)
|
|
{
|
|
if (maps[folded_pgs[i][j]])
|
|
{
|
|
maps[folded_pgs[i][j]].prev.push(prev_pgs && prev_pgs[i] && prev_pgs[i][j] || 0);
|
|
}
|
|
}
|
|
}
|
|
for (const new_id in maps)
|
|
{
|
|
maps[new_id].next = adjust_distribution(maps[new_id].weights, maps[new_id].prev);
|
|
}
|
|
const mapped_pgs = [];
|
|
for (let i = 0; i < folded_pgs.length; i++)
|
|
{
|
|
mapped_pgs.push(folded_pgs[i].map(osd => (maps[osd] ? maps[osd].next[maps[osd].pos++] : osd)));
|
|
}
|
|
return mapped_pgs;
|
|
}
|
|
|
|
// Return the new array of items re-distributed as close as possible to weights in wanted_weights
|
|
// wanted_weights = { [key]: weight }
|
|
// cur_items = key[]
|
|
function adjust_distribution(wanted_weights, cur_items)
|
|
{
|
|
const item_map = {};
|
|
for (let i = 0; i < cur_items.length; i++)
|
|
{
|
|
const item = cur_items[i];
|
|
item_map[item] = (item_map[item] || { target: 0, cur: [] });
|
|
item_map[item].cur.push(i);
|
|
}
|
|
let total_weight = 0;
|
|
for (const item in wanted_weights)
|
|
{
|
|
total_weight += Number(wanted_weights[item]) || 0;
|
|
}
|
|
for (const item in wanted_weights)
|
|
{
|
|
const weight = wanted_weights[item] / total_weight * cur_items.length;
|
|
if (weight > 0)
|
|
{
|
|
item_map[item] = (item_map[item] || { target: 0, cur: [] });
|
|
item_map[item].target = weight;
|
|
}
|
|
}
|
|
const diff = (item) => (item_map[item].cur.length - item_map[item].target);
|
|
const most_underweighted = Object.keys(item_map)
|
|
.filter(item => item_map[item].target > 0)
|
|
.sort((a, b) => diff(a) - diff(b));
|
|
// Items with zero target weight MUST never be selected - remove them
|
|
// and remap each of them to a most underweighted item
|
|
for (const item in item_map)
|
|
{
|
|
if (!item_map[item].target)
|
|
{
|
|
const prev = item_map[item];
|
|
delete item_map[item];
|
|
for (const idx of prev.cur)
|
|
{
|
|
const move_to = most_underweighted[0];
|
|
item_map[move_to].cur.push(idx);
|
|
move_leftmost(most_underweighted, diff);
|
|
}
|
|
}
|
|
}
|
|
// Other over-weighted items are only moved if it improves the distribution
|
|
while (most_underweighted.length > 1)
|
|
{
|
|
const first = most_underweighted[0];
|
|
const last = most_underweighted[most_underweighted.length-1];
|
|
const first_diff = diff(first);
|
|
const last_diff = diff(last);
|
|
if (Math.abs(first_diff+1)+Math.abs(last_diff-1) < Math.abs(first_diff)+Math.abs(last_diff))
|
|
{
|
|
item_map[first].cur.push(item_map[last].cur.pop());
|
|
move_leftmost(most_underweighted, diff);
|
|
move_rightmost(most_underweighted, diff);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
const new_items = new Array(cur_items.length);
|
|
for (const item in item_map)
|
|
{
|
|
for (const idx of item_map[item].cur)
|
|
{
|
|
new_items[idx] = item;
|
|
}
|
|
}
|
|
return new_items;
|
|
}
|
|
|
|
function move_leftmost(sorted_array, diff)
|
|
{
|
|
// Re-sort by moving the leftmost item to the right if it changes position
|
|
const first = sorted_array[0];
|
|
const new_diff = diff(first);
|
|
let r = 0;
|
|
while (r < sorted_array.length-1 && diff(sorted_array[r+1]) <= new_diff)
|
|
r++;
|
|
if (r > 0)
|
|
{
|
|
for (let i = 0; i < r; i++)
|
|
sorted_array[i] = sorted_array[i+1];
|
|
sorted_array[r] = first;
|
|
}
|
|
}
|
|
|
|
function move_rightmost(sorted_array, diff)
|
|
{
|
|
// Re-sort by moving the rightmost item to the left if it changes position
|
|
const last = sorted_array[sorted_array.length-1];
|
|
const new_diff = diff(last);
|
|
let r = sorted_array.length-1;
|
|
while (r > 0 && diff(sorted_array[r-1]) > new_diff)
|
|
r--;
|
|
if (r < sorted_array.length-1)
|
|
{
|
|
for (let i = sorted_array.length-1; i > r; i--)
|
|
sorted_array[i] = sorted_array[i-1];
|
|
sorted_array[r] = last;
|
|
}
|
|
}
|
|
|
|
// map previous PGs to folded nodes
|
|
function fold_prev_pgs(pgs, extracted_nodes)
|
|
{
|
|
const unmap = {};
|
|
for (const new_id in extracted_nodes)
|
|
{
|
|
for (const sub_node of extracted_nodes[new_id])
|
|
{
|
|
unmap[sub_node.id] = new_id;
|
|
}
|
|
}
|
|
const mapped_pgs = [];
|
|
for (let i = 0; i < pgs.length; i++)
|
|
{
|
|
mapped_pgs.push(pgs[i].map(osd => (unmap[osd] || osd)));
|
|
}
|
|
return mapped_pgs;
|
|
}
|
|
|
|
module.exports = {
|
|
fold_failure_domains,
|
|
unfold_failure_domains,
|
|
adjust_distribution,
|
|
fold_prev_pgs,
|
|
};
|