openr/decision/LinkState.h (426 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #pragma once #include <algorithm> #include <memory> #include <numeric> #include <string> #include <unordered_map> #include <unordered_set> #include <vector> #include <openr/common/Constants.h> #include <openr/if/gen-cpp2/Network_types.h> #include <openr/if/gen-cpp2/Types_types.h> namespace openr { using LinkStateMetric = uint64_t; // HoldableValue is the basic building block for ordered FIB programming // (rfc 6976) // // updateValue() will cause the previous value to be held for ttl // time, where the ttl is chosen based on if the update is an up or down event. // Subsequent updateValue() calls with the same value are no-ops. // An update call with a new value when hasHold() is true results in the hold // being clearted, thus changeing value(). // // value() will return the held value until decrementTtl() returns true and the // held value is cleared. template <class T> class HoldableValue { public: explicit HoldableValue(T val); void operator=(T val); const T& value() const; bool hasHold() const; // these methods return true if the call results in the value changing bool decrementTtl(); bool updateValue( T val, LinkStateMetric holdUpTtl, LinkStateMetric holdDownTtl); private: bool isChangeBringingUp(T val); T val_; std::optional<T> heldVal_; LinkStateMetric holdTtl_{0}; }; // // Why define Link and LinkState? Isn't link state fully captured by something // like std::unordered_map<std::string, thrift::AdjacencyDatabase>? // It is, but these classes provide a few major benefits over that simple // structure: // // 1. Only stores bidirectional links. i.e. for a link, the node at both ends // is advertising the adjancecy // // 2. Defines a hash and comparators that operate on the essential property of a // network link: the tuple: unorderedPair<orderedPair<nodeName, ifName>, // orderedPair<nodeName, ifName>> // // 3. For each unique link in the network, holds a single object that can be // quickly accessed and modified via the nodeName of either end of the link. // // 4. Provides useful apis to read and write link state. // // 5. Provides Shortest path results and handles memoizing this expesive // computation while the link state has not changed // class Link { public: Link( const std::string& area, const std::string& nodeName1, const std::string& if1, const std::string& nodeName2, const std::string& if2); Link( const std::string& area, const std::string& nodeName1, const openr::thrift::Adjacency& adj1, const std::string& nodeName2, const openr::thrift::Adjacency& adj2); private: const std::string area_; const std::string n1_, n2_, if1_, if2_; HoldableValue<LinkStateMetric> metric1_{1}, metric2_{1}; HoldableValue<bool> overload1_{false}, overload2_{false}; int32_t adjLabel1_{0}, adjLabel2_{0}; // Weight represents a link's capacity (ex: 100Gbps) int64_t weight1_, weight2_; thrift::BinaryAddress nhV41_, nhV42_, nhV61_, nhV62_; LinkStateMetric holdUpTtl_{0}; const std::pair< std::pair<std::string, std::string>, std::pair<std::string, std::string>> orderedNames_; public: const size_t hash{0}; void setHoldUpTtl(LinkStateMetric ttl); bool isUp() const; bool decrementHolds(); bool hasHolds() const; const std::string& getArea() const { return area_; } const std::string& getOtherNodeName(const std::string& nodeName) const; const std::string& firstNodeName() const; const std::string& secondNodeName() const; const std::string& getIfaceFromNode(const std::string& nodeName) const; LinkStateMetric getMetricFromNode(const std::string& nodeName) const; int32_t getAdjLabelFromNode(const std::string& nodeName) const; int64_t getWeightFromNode(const std::string& nodeName) const; bool getOverloadFromNode(const std::string& nodeName) const; const thrift::BinaryAddress& getNhV4FromNode( const std::string& nodeName) const; const thrift::BinaryAddress& getNhV6FromNode( const std::string& nodeName) const; void setNhV4FromNode( const std::string& nodeName, const thrift::BinaryAddress& nhV4); void setNhV6FromNode( const std::string& nodeName, const thrift::BinaryAddress& nhV6); bool setMetricFromNode( const std::string& nodeName, LinkStateMetric d, LinkStateMetric holdUpTtl, LinkStateMetric holdDownTtl); void setAdjLabelFromNode(const std::string& nodeName, int32_t adjLabel); void setWeightFromNode(const std::string& nodeName, int64_t weight); bool setOverloadFromNode( const std::string& nodeName, bool overload, LinkStateMetric holdUpTtl, LinkStateMetric holdDownTtl); bool operator<(const Link& other) const; bool operator==(const Link& other) const; std::string toString() const; std::string directionalToString(const std::string& fromNode) const; }; // class Link class LinkState { public: explicit LinkState(const std::string& area); struct LinkPtrHash { size_t operator()(const std::shared_ptr<Link>& l) const; }; struct LinkPtrLess { bool operator()( const std::shared_ptr<Link>& lhs, const std::shared_ptr<Link>& rhs) const; }; struct LinkPtrEqual { bool operator()( const std::shared_ptr<Link>& lhs, const std::shared_ptr<Link>& rhs) const; }; using LinkSet = std::unordered_set<std::shared_ptr<Link>, LinkPtrHash, LinkPtrEqual>; // Class holding a network node's SPF result. and useful apis to get and set // - nexthops toward the node // - ultimate link and previous nodes on shortest paths towards node class NodeSpfResult { public: // Represents a link in a path towards a node. For an SPF result, we can use // these to trace paths back to the source from any connected node class PathLink { public: PathLink(std::shared_ptr<Link> const& l, std::string const& n) : link(l), prevNode(n) {} std::shared_ptr<Link> const link; std::string const prevNode; }; explicit NodeSpfResult(LinkStateMetric m) : metric_(m) {} void reset(LinkStateMetric newMetric) { metric_ = newMetric; pathLinks_.clear(); nextHops_.clear(); } std::vector<PathLink> const& pathLinks() const { return pathLinks_; } std::unordered_set<std::string> const& nextHops() const { return nextHops_; } LinkStateMetric metric() const { return metric_; } void addPath(std::shared_ptr<Link> const& link, std::string const& prevNode) { pathLinks_.emplace_back(link, prevNode); } void addNextHops(std::unordered_set<std::string> const& toInsert) { nextHops_.insert(toInsert.begin(), toInsert.end()); } void addNextHop(std::string const& toInsert) { nextHops_.insert(toInsert); } private: LinkStateMetric metric_{std::numeric_limits<LinkStateMetric>::max()}; std::vector<PathLink> pathLinks_; std::unordered_set<std::string> nextHops_; }; using SpfResult = std::unordered_map<std::string /* otherNodeName */, NodeSpfResult>; // Class which holds the UCMP results for a specific node which is part of the // SPF graph from a root node to a list of weighted leaf nodes. The class // holds the following node UCMP results. // - Node's next-hop weights // - Node's advertised weight class NodeUcmpResult { public: class NextHopLink { public: NextHopLink( std::shared_ptr<Link> const& l, std::string const& n, int64_t w) : link(l), nextHopNode(n), weight(w) {} std::shared_ptr<Link> const link; std::string const nextHopNode; int64_t weight{0}; }; explicit NodeUcmpResult() {} std::unordered_map<std::string, NextHopLink> const& nextHopLinks() const { return nextHopLinks_; } std::optional<int64_t> weight() const { return weight_; } void addNextHopLink( const std::string& localIface, std::shared_ptr<Link> const& link, std::string const& nextHopNode, int64_t weight) { nextHopLinks_.emplace( std::piecewise_construct, std::forward_as_tuple(localIface), std::forward_as_tuple(link, nextHopNode, weight)); } void setWeight(int64_t weight) { weight_ = weight; } void normalizeNextHopWeights() { int64_t gcd{0}; for (auto& [_, nh] : nextHopLinks_) { gcd = std::gcd(gcd, nh.weight); } if (gcd > 1) { for (auto& [_, nh] : nextHopLinks_) { nh.weight = nh.weight / gcd; } } } private: std::unordered_map<std::string, NextHopLink> nextHopLinks_; std::optional<int64_t> weight_{std::nullopt}; }; using UcmpResult = std::unordered_map<std::string, NodeUcmpResult>; using Path = std::vector<std::shared_ptr<Link>>; // Shortest paths API: // - getSpfResult() // - getKthPaths() // // each is memoized all params. memoization invalidated for any topolgy // altering calls, i.e. if decrementHolds(), updateAdjacencyDatabase(), or // deleteAdjacencyDatabase() returns with LinkState::topologyChanged set true SpfResult const& getSpfResult( const std::string& nodeName, bool useLinkMetric = true) const; // API to resolve UCMP weights for all node's on the shortest path // between a root node and a list of weighted leaf nodes. UcmpResult resolveUcmpWeights( const SpfResult& spfGraph, const std::unordered_map<std::string, int64_t>& dstWeights, thrift::PrefixForwardingAlgorithm algo, bool useLinkMetric = true) const; private: // LinkState belongs to a unique area const std::string area_; // memoization structure for getSpfResult() mutable std::unordered_map< std::pair<std::string /* nodeName */, bool /* useLinkMetric */>, SpfResult> spfResults_; public: // Trace edge-disjoint paths from dest to src. // I.e., no two paths returned from this function can share any links // // Assertion: k >= 1 // For k = 1, the above algorithm is perfomered considering all links in the // network. // For k > 1, the algorithm is performed considering all links except links on // paths in the set {p in getKthPaths(src, dest, i) | 1 <= i < k}. std::vector<LinkState::Path> const& getKthPaths( const std::string& src, const std::string& dest, size_t k) const; private: // memoization structure for getKthPaths() mutable std::unordered_map< std::tuple<std::string /* src */, std::string /* dest */, size_t /* k */>, std::vector<LinkState::Path>> kthPathResults_; public: // non-const public methods // IMPT: clear memoization structures as appropirate in these functions class LinkStateChange { public: LinkStateChange() = default; LinkStateChange(bool topo, bool link, bool node) : topologyChanged(topo), linkAttributesChanged(link), nodeLabelChanged(node) {} bool operator==(LinkStateChange const& other) const { return topologyChanged == other.topologyChanged && linkAttributesChanged == other.linkAttributesChanged && nodeLabelChanged == other.nodeLabelChanged; } // Whether topology has changed bool topologyChanged{false}; // Newly added links in the topology. Today it is only populated in // `updateAdjacencyDatabase()`. std::vector<std::shared_ptr<Link>> addedLinks; // Whether attributes of links have changed bool linkAttributesChanged{false}; // Whehter node labels have changed bool nodeLabelChanged{false}; }; LinkStateChange decrementHolds(); // update adjacencies for the given router LinkStateChange updateAdjacencyDatabase( thrift::AdjacencyDatabase const& adjacencyDb, std::string area, LinkStateMetric holdUpTtl = 0, LinkStateMetric holdDownTtl = 0); // delete a node's adjacency database // return true if this has caused any change in graph LinkStateChange deleteAdjacencyDatabase(const std::string& nodeName); // const public methods // returns metric from a to b, // if nodes b is not reachable from a, returns std::nullopt std::optional<LinkStateMetric> getMetricFromAToB( std::string const& a, std::string const& b, bool useLinkMetric = true) const; const std::string& getArea() const { return area_; } bool hasNode(const std::string& nodeName) const { return 0 != adjacencyDatabases_.count(nodeName); } const LinkSet& linksFromNode(const std::string& nodeName) const; bool isNodeOverloaded(const std::string& nodeName) const; bool hasHolds() const; size_t numLinks() const { return allLinks_.size(); } size_t numNodes() const { return linkMap_.size(); } // get adjacency databases std::unordered_map< std::string /* nodeName */, thrift::AdjacencyDatabase> const& getAdjacencyDatabases() const { return adjacencyDatabases_; } // check if path A is part of path B. // Example: // path A: a->b->c // path B: d->a->b->c->d // return True static bool pathAInPathB(Path const& a, Path const& b) { if (a.size() <= b.size()) { for (size_t i = 0; i < (b.size() - a.size()) + 1; ++i) { size_t a_i = 0, b_i = i; while (a_i < a.size() && *a.at(a_i) == *b.at(b_i)) { ++a_i; ++b_i; } if (a.size() == a_i) { return true; } } } return false; } private: // helpers to update the link state graph // find one path from dest to src for a given SpfResult // ingnore links already in linksToIgnore std::optional<Path> traceOnePath( std::string const& src, std::string const& dest, SpfResult const& result, LinkSet& linksToIgnore) const; void addLink(std::shared_ptr<Link> link); void removeLink(std::shared_ptr<Link> link); void removeNode(const std::string& nodeName); bool updateNodeOverloaded( const std::string& nodeName, bool isOverloaded, LinkStateMetric holdUpTtl, LinkStateMetric holdDownTtl); // run Dijkstra's Shortest Path First algorithm on the link state graph SpfResult runSpf( const std::string& src, /* the source node for the SPF run */ bool useLinkMetric, /* if set, the algorithm will respect adjancecy weights as advertised from the adjacent nodes, otherwise it will consider the graph unweighted */ const LinkSet& linksToIgnore = {} /* optionaly specify a set of links to not use when running */) const; // returns Link object if the reverse adjancency is present in // adjacencyDatabases_.at(adj.otherNodeName), else returns nullptr std::shared_ptr<Link> maybeMakeLink( const std::string& nodeName, const thrift::Adjacency& adj) const; std::vector<std::shared_ptr<Link>> getOrderedLinkSet( const thrift::AdjacencyDatabase& adjDb) const; std::vector<std::shared_ptr<Link>> orderedLinksFromNode( const std::string& nodeName) const; // this stores the same link object accessible from either nodeName std::unordered_map<std::string /* nodeName */, LinkSet> linkMap_; // useful for iterating over all the links LinkSet allLinks_; std::unordered_map<std::string /* nodeName */, HoldableValue<bool>> nodeOverloads_; // the latest AdjacencyDatabase we've received from each node std::unordered_map<std::string, thrift::AdjacencyDatabase> adjacencyDatabases_; }; // class LinkState // Classes needed for running Dijkstra to build an SPF graph starting at a root // node to all other nodes the link state topology. In addition to implementing // the priority queue element at the heart of Dijkstra's algorithm, this // structure also allows us to store appication specfic data: nexthops. class DijkstraQSpfNode { public: DijkstraQSpfNode(const std::string& n, LinkStateMetric m) : nodeName(n), result(m) {} LinkStateMetric metric() { return result.metric(); } const std::string nodeName{""}; LinkState::NodeSpfResult result; }; // Dijkstra queue element used to derive UCMP weights for all node's // on the shortest path between a root node and a list of weighted lead nodes class DijkstraQUcmpNode { public: DijkstraQUcmpNode(const std::string& n, LinkStateMetric m) : nodeName(n), metric_(m) {} LinkStateMetric metric() { return metric_; } const std::string nodeName{""}; LinkStateMetric metric_{0}; LinkState::NodeUcmpResult result; }; // Dijkstra Q template class. // Implements a priority queue using a heap. // // Template object must have the following elements // - metric // - nodeName template <class T> class DijkstraQ { private: std::vector<std::shared_ptr<T>> heap_; std::unordered_map<std::string, std::shared_ptr<T>> nameToNode_; struct { bool operator()(std::shared_ptr<T> a, std::shared_ptr<T> b) const { if (a->metric() != b->metric()) { return a->metric() > b->metric(); } return a->nodeName > b->nodeName; } } DijkstraQNodeGreater; public: void insertNode(const std::string& nodeName, LinkStateMetric d) { heap_.emplace_back(std::make_shared<T>(nodeName, d)); nameToNode_[nodeName] = heap_.back(); std::push_heap(heap_.begin(), heap_.end(), DijkstraQNodeGreater); } std::shared_ptr<T> get(const std::string& nodeName) { if (nameToNode_.count(nodeName)) { return nameToNode_.at(nodeName); } return nullptr; } std::shared_ptr<T> extractMin() { if (heap_.empty()) { return nullptr; } auto min = heap_.at(0); CHECK(nameToNode_.erase(min->nodeName)); std::pop_heap(heap_.begin(), heap_.end(), DijkstraQNodeGreater); heap_.pop_back(); return min; } void reMake() { // this is a bit slow but is rarely called in our application. In fact, // in networks where the metric is hop count, this will never be called // and the Dijkstra run is no different than BFS std::make_heap(heap_.begin(), heap_.end(), DijkstraQNodeGreater); } }; } // namespace openr namespace std { // needed for certain containers template <> struct hash<openr::Link> { size_t operator()(openr::Link const& link) const; }; template <> struct hash<openr::LinkState::LinkSet> { size_t operator()(openr::LinkState::LinkSet const& set) const; }; template <> struct equal_to<openr::LinkState::LinkSet> { bool operator()( openr::LinkState::LinkSet const& a, openr::LinkState::LinkSet const& b) const; }; } // namespace std