| // 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. |
| |
| #include "master/allocator/sorter/random/sorter.hpp" |
| #include "master/allocator/sorter/random/utils.hpp" |
| |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| #include <mesos/mesos.hpp> |
| #include <mesos/resources.hpp> |
| #include <mesos/values.hpp> |
| |
| #include <process/pid.hpp> |
| |
| #include <stout/check.hpp> |
| #include <stout/foreach.hpp> |
| #include <stout/hashmap.hpp> |
| #include <stout/option.hpp> |
| #include <stout/strings.hpp> |
| |
| using std::set; |
| using std::string; |
| using std::vector; |
| |
| using process::UPID; |
| |
| namespace mesos { |
| namespace internal { |
| namespace master { |
| namespace allocator { |
| |
| |
| RandomSorter::RandomSorter() |
| : root(new Node("", Node::INTERNAL, nullptr)) {} |
| |
| |
| RandomSorter::RandomSorter( |
| const UPID& allocator, |
| const string& metricsPrefix) |
| : root(new Node("", Node::INTERNAL, nullptr)) {} |
| |
| |
| RandomSorter::~RandomSorter() |
| { |
| delete root; |
| } |
| |
| |
| void RandomSorter::initialize( |
| const Option<set<string>>& _fairnessExcludeResourceNames) {} |
| |
| |
| void RandomSorter::add(const string& clientPath) |
| { |
| vector<string> pathElements = strings::tokenize(clientPath, "/"); |
| CHECK(!pathElements.empty()); |
| |
| Node* current = root; |
| Node* lastCreatedNode = nullptr; |
| |
| // Traverse the tree to add new nodes for each element of the path, |
| // if that node doesn't already exist (similar to `mkdir -p`). |
| foreach (const string& element, pathElements) { |
| Node* node = nullptr; |
| |
| foreach (Node* child, current->children) { |
| if (child->name == element) { |
| node = child; |
| break; |
| } |
| } |
| |
| if (node != nullptr) { |
| current = node; |
| continue; |
| } |
| |
| // We didn't find `element`, so add a new child to `current`. |
| // |
| // If adding this child would result in turning `current` from a |
| // leaf node into an internal node, we need to create an |
| // additional child node: `current` must have been associated with |
| // a client and clients must always be associated with leaf nodes. |
| if (current->isLeaf()) { |
| Node* parent = CHECK_NOTNULL(current->parent); |
| |
| parent->removeChild(current); |
| |
| // Create a node under `parent`. This internal node will take |
| // the place of `current` in the tree. |
| Node* internal = new Node(current->name, Node::INTERNAL, parent); |
| parent->addChild(internal); |
| internal->allocation = current->allocation; |
| |
| CHECK_EQ(current->path, internal->path); |
| |
| // Update `current` to become a virtual leaf node and a child of |
| // `internal`. |
| current->name = "."; |
| current->parent = internal; |
| current->path = strings::join("/", parent->path, current->name); |
| |
| internal->addChild(current); |
| |
| CHECK_EQ(internal->path, current->clientPath()); |
| |
| current = internal; |
| } |
| |
| // Now actually add a new child to `current`. |
| Node* newChild = new Node(element, Node::INTERNAL, current); |
| current->addChild(newChild); |
| |
| current = newChild; |
| lastCreatedNode = newChild; |
| } |
| |
| CHECK(current->kind == Node::INTERNAL); |
| |
| // `current` is the node associated with the last element of the |
| // path. If we didn't add `current` to the tree above, create a leaf |
| // node now. For example, if the tree contains "a/b" and we add a |
| // new client "a", we want to create a new leaf node "a/." here. |
| if (current != lastCreatedNode) { |
| Node* newChild = new Node(".", Node::INACTIVE_LEAF, current); |
| current->addChild(newChild); |
| current = newChild; |
| } else { |
| // If we created `current` in the loop above, it was marked an |
| // `INTERNAL` node. It should actually be an inactive leaf node. |
| current->kind = Node::INACTIVE_LEAF; |
| |
| // `current` has changed from an internal node to an inactive |
| // leaf, so remove and re-add it to its parent. This moves it to |
| // the end of the parent's list of children. |
| CHECK_NOTNULL(current->parent); |
| |
| current->parent->removeChild(current); |
| current->parent->addChild(current); |
| } |
| |
| // `current` is the newly created node associated with the last |
| // element of the path. `current` should be an inactive leaf node. |
| CHECK(current->children.empty()); |
| CHECK(current->kind == Node::INACTIVE_LEAF); |
| |
| // Add a new entry to the lookup table. The full path of the newly |
| // added client should not already exist in `clients`. |
| CHECK_EQ(clientPath, current->clientPath()); |
| CHECK(!clients.contains(clientPath)); |
| |
| clients[clientPath] = current; |
| } |
| |
| |
| void RandomSorter::remove(const string& clientPath) |
| { |
| Node* current = CHECK_NOTNULL(find(clientPath)); |
| |
| // Save a copy of the leaf node's allocated resources, because we |
| // destroy the leaf node below. |
| const hashmap<SlaveID, Resources> leafAllocation = |
| current->allocation.resources; |
| |
| // Remove the lookup table entry for the client. |
| CHECK(clients.contains(clientPath)); |
| clients.erase(clientPath); |
| |
| // To remove a client from the tree, we have to do two things: |
| // |
| // (1) Update the tree structure to reflect the removal of the |
| // client. This means removing the client's leaf node, then |
| // walking back up the tree to remove any internal nodes that |
| // are now unnecessary. |
| // |
| // (2) Update allocations of ancestor nodes to reflect the removal |
| // of the client. |
| // |
| // We do both things at once: find the leaf node, remove it, and |
| // walk up the tree, updating ancestor allocations and removing |
| // ancestors when possible. |
| while (current != root) { |
| Node* parent = CHECK_NOTNULL(current->parent); |
| |
| // Update `parent` to reflect the fact that the resources in the |
| // leaf node are no longer allocated to the subtree rooted at |
| // `parent`. We skip `root`, because we never update the |
| // allocation made to the root node. |
| if (parent != root) { |
| foreachpair (const SlaveID& slaveId, |
| const Resources& resources, |
| leafAllocation) { |
| parent->allocation.subtract(slaveId, resources); |
| } |
| } |
| |
| if (current->children.empty()) { |
| parent->removeChild(current); |
| delete current; |
| } else if (current->children.size() == 1) { |
| // If `current` has only one child that was created to |
| // accommodate inserting `clientPath` (see `RandomSorter::add()`), |
| // we can remove the child node and turn `current` back into a |
| // leaf node. |
| Node* child = *(current->children.begin()); |
| |
| if (child->name == ".") { |
| CHECK(child->isLeaf()); |
| CHECK(clients.contains(current->path)); |
| CHECK_EQ(child, clients.at(current->path)); |
| |
| current->kind = child->kind; |
| current->removeChild(child); |
| |
| // `current` has changed kind (from `INTERNAL` to a leaf, |
| // which might be active or inactive). Hence we might need to |
| // change its position in the `children` list. |
| if (current->kind == Node::INTERNAL) { |
| CHECK_NOTNULL(current->parent); |
| |
| current->parent->removeChild(current); |
| current->parent->addChild(current); |
| } |
| |
| clients[current->path] = current; |
| |
| delete child; |
| } |
| } |
| |
| current = parent; |
| } |
| } |
| |
| |
| void RandomSorter::activate(const string& clientPath) |
| { |
| Node* client = CHECK_NOTNULL(find(clientPath)); |
| |
| if (client->kind == Node::INACTIVE_LEAF) { |
| client->kind = Node::ACTIVE_LEAF; |
| |
| // `client` has been activated, so move it to the beginning of its |
| // parent's list of children. |
| CHECK_NOTNULL(client->parent); |
| |
| client->parent->removeChild(client); |
| client->parent->addChild(client); |
| } |
| } |
| |
| |
| void RandomSorter::deactivate(const string& clientPath) |
| { |
| Node* client = CHECK_NOTNULL(find(clientPath)); |
| |
| if (client->kind == Node::ACTIVE_LEAF) { |
| client->kind = Node::INACTIVE_LEAF; |
| |
| // `client` has been deactivated, so move it to the end of its |
| // parent's list of children. |
| CHECK_NOTNULL(client->parent); |
| |
| client->parent->removeChild(client); |
| client->parent->addChild(client); |
| } |
| } |
| |
| |
| void RandomSorter::updateWeight(const string& path, double weight) |
| { |
| weights[path] = weight; |
| |
| // Update the weight of the corresponding internal node, |
| // if it exists (this client may not exist despite there |
| // being a weight). |
| Node* node = find(path); |
| |
| if (node == nullptr) { |
| return; |
| } |
| |
| // If there is a virtual leaf, we need to move up one level. |
| if (node->name == ".") { |
| node = CHECK_NOTNULL(node->parent); |
| } |
| |
| CHECK_EQ(path, node->path); |
| |
| node->weight = weight; |
| } |
| |
| |
| void RandomSorter::allocated( |
| const string& clientPath, |
| const SlaveID& slaveId, |
| const Resources& resources) |
| { |
| Node* current = CHECK_NOTNULL(find(clientPath)); |
| |
| // NOTE: We don't currently update the `allocation` for the root |
| // node. This is debatable, but the current implementation doesn't |
| // require looking at the allocation of the root node. |
| while (current != root) { |
| current->allocation.add(slaveId, resources); |
| current = CHECK_NOTNULL(current->parent); |
| } |
| } |
| |
| |
| void RandomSorter::update( |
| const string& clientPath, |
| const SlaveID& slaveId, |
| const Resources& oldAllocation, |
| const Resources& newAllocation) |
| { |
| // TODO(bmahler): Check invariants between old and new allocations. |
| // Namely, the roles and quantities of resources should be the same! |
| |
| Node* current = CHECK_NOTNULL(find(clientPath)); |
| |
| // NOTE: We don't currently update the `allocation` for the root |
| // node. This is debatable, but the current implementation doesn't |
| // require looking at the allocation of the root node. |
| while (current != root) { |
| current->allocation.update(slaveId, oldAllocation, newAllocation); |
| current = CHECK_NOTNULL(current->parent); |
| } |
| } |
| |
| |
| void RandomSorter::unallocated( |
| const string& clientPath, |
| const SlaveID& slaveId, |
| const Resources& resources) |
| { |
| Node* current = CHECK_NOTNULL(find(clientPath)); |
| |
| // NOTE: We don't currently update the `allocation` for the root |
| // node. This is debatable, but the current implementation doesn't |
| // require looking at the allocation of the root node. |
| while (current != root) { |
| current->allocation.subtract(slaveId, resources); |
| current = CHECK_NOTNULL(current->parent); |
| } |
| } |
| |
| |
| const hashmap<SlaveID, Resources>& RandomSorter::allocation( |
| const string& clientPath) const |
| { |
| const Node* client = CHECK_NOTNULL(find(clientPath)); |
| return client->allocation.resources; |
| } |
| |
| |
| const Resources& RandomSorter::allocationScalarQuantities( |
| const string& clientPath) const |
| { |
| const Node* client = CHECK_NOTNULL(find(clientPath)); |
| return client->allocation.scalarQuantities; |
| } |
| |
| |
| hashmap<string, Resources> RandomSorter::allocation( |
| const SlaveID& slaveId) const |
| { |
| hashmap<string, Resources> result; |
| |
| // We want to find the allocation that has been made to each client |
| // on a particular `slaveId`. Rather than traversing the tree |
| // looking for leaf nodes (clients), we can instead just iterate |
| // over the `clients` hashmap. |
| // |
| // TODO(jmlvanre): We can index the allocation by slaveId to make |
| // this faster. It is a tradeoff between speed vs. memory. For now |
| // we use existing data structures. |
| foreachvalue (const Node* client, clients) { |
| if (client->allocation.resources.contains(slaveId)) { |
| // It is safe to use `at()` here because we've just checked the |
| // existence of the key. This avoids unnecessary copies. |
| string path = client->clientPath(); |
| CHECK(!result.contains(path)); |
| result.emplace(path, client->allocation.resources.at(slaveId)); |
| } |
| } |
| |
| return result; |
| } |
| |
| |
| Resources RandomSorter::allocation( |
| const string& clientPath, |
| const SlaveID& slaveId) const |
| { |
| const Node* client = CHECK_NOTNULL(find(clientPath)); |
| |
| if (client->allocation.resources.contains(slaveId)) { |
| return client->allocation.resources.at(slaveId); |
| } |
| |
| return Resources(); |
| } |
| |
| |
| vector<string> RandomSorter::sort() |
| { |
| std::function<void (Node*)> shuffleTree = [this, &shuffleTree](Node* node) { |
| // Inactive leaves are always stored at the end of the |
| // `children` vector; this means that we should only shuffle |
| // the prefix of the vector before the first inactive leaf. |
| auto inactiveBegin = std::find_if( |
| node->children.begin(), |
| node->children.end(), |
| [](Node* n) { return n->kind == Node::INACTIVE_LEAF; }); |
| |
| vector<double> weights(inactiveBegin - node->children.begin()); |
| |
| for (int i = 0; i < inactiveBegin - node->children.begin(); ++i) { |
| weights[i] = getWeight(node->children[i]); |
| } |
| |
| weightedShuffle(node->children.begin(), inactiveBegin, weights, generator); |
| |
| foreach (Node* child, node->children) { |
| if (child->kind == Node::INTERNAL) { |
| shuffleTree(child); |
| } else if (child->kind == Node::INACTIVE_LEAF) { |
| break; |
| } |
| } |
| }; |
| |
| shuffleTree(root); |
| |
| // Return all active leaves in the tree via pre-order traversal. |
| // The children of each node are already shuffled, with |
| // inactive leaves stored after active leaves and internal nodes. |
| vector<string> result; |
| |
| // TODO(bmahler): This over-reserves where there are inactive |
| // clients, only reserve the number of active clients. |
| result.reserve(clients.size()); |
| |
| std::function<void (const Node*)> listClients = |
| [&listClients, &result](const Node* node) { |
| foreach (const Node* child, node->children) { |
| switch (child->kind) { |
| case Node::ACTIVE_LEAF: |
| result.push_back(child->clientPath()); |
| break; |
| |
| case Node::INACTIVE_LEAF: |
| // As soon as we see the first inactive leaf, we can stop |
| // iterating over the current node's list of children. |
| return; |
| |
| case Node::INTERNAL: |
| listClients(child); |
| break; |
| } |
| } |
| }; |
| |
| listClients(root); |
| |
| return result; |
| } |
| |
| |
| bool RandomSorter::contains(const string& clientPath) const |
| { |
| return find(clientPath) != nullptr; |
| } |
| |
| |
| size_t RandomSorter::count() const |
| { |
| return clients.size(); |
| } |
| |
| |
| double RandomSorter::getWeight(const Node* node) const |
| { |
| if (node->weight.isNone()) { |
| node->weight = weights.get(node->path).getOrElse(1.0); |
| } |
| |
| return CHECK_NOTNONE(node->weight); |
| } |
| |
| |
| RandomSorter::Node* RandomSorter::find(const string& clientPath) const |
| { |
| Option<Node*> client_ = clients.get(clientPath); |
| |
| if (client_.isNone()) { |
| return nullptr; |
| } |
| |
| Node* client = client_.get(); |
| |
| CHECK(client->isLeaf()); |
| |
| return client; |
| } |
| |
| } // namespace allocator { |
| } // namespace master { |
| } // namespace internal { |
| } // namespace mesos { |