diff --git a/fboss/agent/Main.cpp b/fboss/agent/Main.cpp index 6b5af9a86834e..a70f0d9a64ed4 100644 --- a/fboss/agent/Main.cpp +++ b/fboss/agent/Main.cpp @@ -53,6 +53,9 @@ DEFINE_int32(stat_publish_interval_ms, 1000, "How frequently to publish thread-local stats back to the " "global store. This should generally be less than 1 second."); DEFINE_int32(thrift_idle_timeout, 60, "Thrift idle timeout in seconds."); +// Programming 16K routes can take 20+ seconds +DEFINE_int32(thrift_task_expire_timeout, 30, + "Thrift task expire timeout in seconds."); DEFINE_bool(tun_intf, true, "Create tun interfaces to allow other processes to " "send and receive traffic via the switch ports"); @@ -266,6 +269,8 @@ int fbossMain(int argc, char** argv, PlatformInitFn initPlatform) { // Start the thrift server ThriftServer server; + server.setTaskExpireTime(std::chrono::milliseconds( + thrift_task_expire_timeout * 1000)); server.getEventBaseManager()->setEventBase(&eventBase, false); server.setInterface(handler); server.setDuplex(true); diff --git a/fboss/agent/ThriftHandler.cpp b/fboss/agent/ThriftHandler.cpp index be948c7377c51..d70b3bcd08310 100644 --- a/fboss/agent/ThriftHandler.cpp +++ b/fboss/agent/ThriftHandler.cpp @@ -485,9 +485,9 @@ void ThriftHandler::getPortStatus(map& statusMap, void ThriftHandler::getRouteTable(std::vector& route) { ensureConfigured(); for (const auto& routeTable : (*sw_->getState()->getRouteTables())) { - for (const auto& ipv4Rib : routeTable->getRibV4()->getAllNodes()) { + for (const auto& ipv4Rib : routeTable->getRibV4()->routes()) { UnicastRoute tempRoute; - auto ipv4 = ipv4Rib.second.get(); + auto ipv4 = ipv4Rib.value().get(); auto fwdInfo = ipv4->getForwardInfo(); tempRoute.dest.ip = toBinaryAddress(ipv4->prefix().network); tempRoute.dest.prefixLength = ipv4->prefix().mask; @@ -497,9 +497,9 @@ void ThriftHandler::getRouteTable(std::vector& route) { } route.push_back(tempRoute); } - for (const auto& ipv6Rib : routeTable->getRibV6()->getAllNodes()) { + for (const auto& ipv6Rib : routeTable->getRibV6()->routes()) { UnicastRoute tempRoute; - auto ipv6 = ipv6Rib.second.get(); + auto ipv6 = ipv6Rib.value().get(); auto fwdInfo = ipv6->getForwardInfo(); tempRoute.dest.ip = toBinaryAddress( ipv6->prefix().network); diff --git a/fboss/agent/hw/bcm/BcmSwitch.cpp b/fboss/agent/hw/bcm/BcmSwitch.cpp index adc39474ca182..84975c1ad5817 100644 --- a/fboss/agent/hw/bcm/BcmSwitch.cpp +++ b/fboss/agent/hw/bcm/BcmSwitch.cpp @@ -43,6 +43,7 @@ #include "fboss/agent/state/Interface.h" #include "fboss/agent/state/InterfaceMap.h" #include "fboss/agent/state/NodeMapDelta.h" +#include "fboss/agent/state/NodeMapDelta-defs.h" #include "fboss/agent/state/Port.h" #include "fboss/agent/state/PortMap.h" #include "fboss/agent/state/StateDelta.h" diff --git a/fboss/agent/state/NodeMapDelta-defs.h b/fboss/agent/state/NodeMapDelta-defs.h index c239a4328dfc9..eb3afb04f1710 100644 --- a/fboss/agent/state/NodeMapDelta-defs.h +++ b/fboss/agent/state/NodeMapDelta-defs.h @@ -13,15 +13,16 @@ namespace facebook { namespace fboss { -template +template std::shared_ptr - NodeMapDelta::Iterator::nullNode_; + NodeMapDelta::Iterator::nullNode_; -template -NodeMapDelta::Iterator::Iterator(const MapType* oldMap, - typename MapType::Iterator oldIt, - const MapType* newMap, - typename MapType::Iterator newIt) +template +NodeMapDelta::Iterator::Iterator( + const MapType* oldMap, + typename MapType::Iterator oldIt, + const MapType* newMap, + typename MapType::Iterator newIt) : oldIt_(oldIt), newIt_(newIt), oldMap_(oldMap), @@ -37,8 +38,8 @@ NodeMapDelta::Iterator::Iterator(const MapType* oldMap, updateValue(); } -template -NodeMapDelta::Iterator::Iterator() +template +NodeMapDelta::Iterator::Iterator() : oldIt_(), newIt_(), oldMap_(nullptr), @@ -46,8 +47,8 @@ NodeMapDelta::Iterator::Iterator() value_(nullNode_, nullNode_) { } -template -void NodeMapDelta::Iterator::updateValue() { +template +void NodeMapDelta::Iterator::updateValue() { if (oldIt_ == oldMap_->end()) { if (newIt_ == newMap_->end()) { value_.reset(nullNode_, nullNode_); @@ -71,8 +72,8 @@ void NodeMapDelta::Iterator::updateValue() { } } -template -void NodeMapDelta::Iterator::advance() { +template +void NodeMapDelta::Iterator::advance() { // If we have already hit the end of one side, advance the other. // We are immediately done after this. if (oldIt_ == oldMap_->end()) { diff --git a/fboss/agent/state/NodeMapDelta.h b/fboss/agent/state/NodeMapDelta.h index 7f9c56889385c..59088f864724c 100644 --- a/fboss/agent/state/NodeMapDelta.h +++ b/fboss/agent/state/NodeMapDelta.h @@ -21,6 +21,34 @@ namespace facebook { namespace fboss { template class DeltaValue; +template +class MapPointerTraits { + public: + using RawConstPointerType = const MAP*; + // Embed const in MapPointerType for raw pointers. + // Since we want to use both raw and unique_ptr, + // we don't want to add const to the Map pointer type + // in function declarations, where we will want to move + // from unique_ptr (moving from const unique_ptr is not + // permitted since its a modifying operation). + using MapPointerType = const MAP*; + static RawConstPointerType getRawPointer(MapPointerType map) { + return map; + } +}; + +template +class MapUniquePointerTraits { + public: + using RawConstPointerType = const MAP*; + // Non const MapPointer type, const unique_ptr is + // severly restrictive when received as a function + // argument, since it can't be moved from. + using MapPointerType = std::unique_ptr; + static RawConstPointerType getRawPointer(const MapPointerType& map) { + return map.get(); + } +}; /* * NodeMapDelta contains code for examining the differences between two NodeMap * objects. @@ -28,22 +56,25 @@ class DeltaValue; * The main function of this class is the Iterator that it provides. This * allows caller to walk over the changed, added, and removed nodes. */ -template> +template, + typename MAPPOINTERTRAITS = MapPointerTraits> class NodeMapDelta { public: - typedef MAP MapType; - typedef typename MAP::Node Node; + using MapPointerType = typename MAPPOINTERTRAITS::MapPointerType; + using RawConstPointerType = typename MAPPOINTERTRAITS::RawConstPointerType; + using Node = typename MAP::Node; class Iterator; - NodeMapDelta(const MapType* oldMap, const MapType* newMap) - : old_(oldMap), - new_(newMap) {} + NodeMapDelta(MapPointerType&& oldMap, MapPointerType&& newMap) + : old_(std::move(oldMap)), + new_(std::move(newMap)) {} - const MapType* getOld() const { - return old_; + RawConstPointerType getOld() const { + return MAPPOINTERTRAITS::getRawPointer(old_); } - const MapType* getNew() const { - return new_; + RawConstPointerType getNew() const { + return MAPPOINTERTRAITS::getRawPointer(new_); } /* @@ -58,12 +89,17 @@ class NodeMapDelta { private: /* - * Note that we assume NodeMapDelta is always used by StateDelta. StateDelta - * holds a shared_ptr to the old and new SwitchState objects, so that we can - * use raw pointers here, rather than shared_ptrs. + * NodeMapDelta is used by StateDelta. StateDelta holds a shared_ptr to + * the old and new SwitchState objects, which in turn holds + * shared_ptrs to NodeMaps, hence in the common case use raw pointers here + * and don't bother with ownership. However there are cases where collections + * are not easily mapped to a NodeMap structure, but we still want to box + * them into a NodeMap while computing delta. In this case NodeMap is created + * on the fly and we pass ownership to NodeMapDelta object via a unique_ptr. + * See MapPointerTraits and MapUniquePointerTraits classes for details. */ - const MapType* old_; - const MapType* new_; + MapPointerType old_; + MapPointerType new_; }; template @@ -102,8 +138,8 @@ class DeltaValue { * An iterator for walking over the Nodes that changed between the two * NodeMaps. */ -template -class NodeMapDelta::Iterator { +template +class NodeMapDelta::Iterator { public: typedef MAP MapType; typedef typename MAP::Node Node; @@ -161,9 +197,9 @@ class NodeMapDelta::Iterator { static std::shared_ptr nullNode_; }; -template -typename NodeMapDelta::Iterator -NodeMapDelta::begin() const { +template +typename NodeMapDelta::Iterator +NodeMapDelta::begin() const { if (old_ == new_) { return end(); } @@ -171,25 +207,25 @@ NodeMapDelta::begin() const { // nodes), point the old side of the iterator at the new node, but start it // at the end of the map. if (!old_) { - return Iterator(new_, new_->end(), new_, new_->begin()); + return Iterator(getNew(), new_->end(), getNew(), new_->begin()); } // And vice-versa for the new node being null (to represent removed nodes). if (!new_) { - return Iterator(old_, old_->begin(), old_, old_->end()); + return Iterator(getOld(), old_->begin(), getOld(), old_->end()); } - return Iterator(old_, old_->begin(), new_, new_->begin()); + return Iterator(getOld(), old_->begin(), getNew(), new_->begin()); } -template -typename NodeMapDelta::Iterator -NodeMapDelta::end() const { +template +typename NodeMapDelta::Iterator +NodeMapDelta::end() const { if (!old_) { - return Iterator(new_, new_->end(), new_, new_->end()); + return Iterator(getNew(), new_->end(), getNew(), new_->end()); } if (!new_) { - return Iterator(old_, old_->end(), old_, old_->end()); + return Iterator(getOld(), old_->end(), getOld(), old_->end()); } - return Iterator(old_, old_->end(), new_, new_->end()); + return Iterator(getOld(), old_->end(), getNew(), new_->end()); } }} // facebook::fboss diff --git a/fboss/agent/state/RouteDelta.h b/fboss/agent/state/RouteDelta.h index 4d75f915672f4..6386f6150c6af 100644 --- a/fboss/agent/state/RouteDelta.h +++ b/fboss/agent/state/RouteDelta.h @@ -14,20 +14,45 @@ #include "fboss/agent/state/RouteTable.h" #include "fboss/agent/state/RouteTableMap.h" + namespace facebook { namespace fboss { class RouteTablesDelta : public DeltaValue { public: - typedef NodeMapDelta> RoutesV4Delta; - typedef NodeMapDelta> RoutesV6Delta; + using NodeMapRibV4 = RouteTableRibNodeMap; + using NodeMapRibV6 = RouteTableRibNodeMap; + using RoutesV4Delta = NodeMapDelta, + MapUniquePointerTraits>; + using RoutesV6Delta = NodeMapDelta, + MapUniquePointerTraits>; + using DeltaValue::DeltaValue; + RoutesV4Delta getRoutesV4Delta() const { - return RoutesV4Delta(getOld() ? getOld()->getRibV4().get() : nullptr, - getNew() ? getNew()->getRibV4().get() : nullptr); + std::unique_ptr oldRib, newRib; + if (getOld()) { + oldRib.reset(new NodeMapRibV4()); + oldRib->addRoutes(*(getOld()->getRibV4())); + } + if (getNew()) { + newRib.reset(new NodeMapRibV4()); + newRib->addRoutes(*(getNew()->getRibV4())); + } + return RoutesV4Delta(std::move(oldRib), std::move(newRib)); } - RoutesV6Delta getRoutesV6Delta() const { - return RoutesV6Delta(getOld() ? getOld()->getRibV6().get() : nullptr, - getNew() ? getNew()->getRibV6().get() : nullptr); + RoutesV6Delta getRoutesV6Delta() const { + std::unique_ptr oldRib, newRib; + if (getOld()) { + oldRib.reset(new NodeMapRibV6()); + oldRib->addRoutes(*(getOld()->getRibV6())); + } + if (getNew()) { + newRib.reset(new NodeMapRibV6()); + newRib->addRoutes(*(getNew()->getRibV6())); + } + return RoutesV6Delta(std::move(oldRib), std::move(newRib)); } }; diff --git a/fboss/agent/state/RouteTableRib.cpp b/fboss/agent/state/RouteTableRib.cpp index b9ae4197a58ab..b6242869e4eb1 100644 --- a/fboss/agent/state/RouteTableRib.cpp +++ b/fboss/agent/state/RouteTableRib.cpp @@ -12,36 +12,39 @@ #include "fboss/agent/state/NodeMap-defs.h" #include "fboss/agent/state/Route.h" +namespace { +constexpr auto kRoutes = "routes"; +} + namespace facebook { namespace fboss { -template -RouteTableRib::RouteTableRib() { -} +FBOSS_INSTANTIATE_NODE_MAP(RouteTableRibNodeMap, + RouteTableRibNodeMapTraits); +FBOSS_INSTANTIATE_NODE_MAP(RouteTableRibNodeMap, + RouteTableRibNodeMapTraits); template -RouteTableRib::~RouteTableRib() { +folly::dynamic RouteTableRib::toFollyDynamic() const { + std::vector routesJson; + for (const auto& route: rib_) { + routesJson.emplace_back(route->value()->toFollyDynamic()); + } + folly::dynamic routes = folly::dynamic::object; + routes[kRoutes] = routesJson; + return routes; } template -std::shared_ptr> RouteTableRib::longestMatch( - const AddrT& nexthop) const { - std::shared_ptr> bestMatch; - int bestMatchMask = -1; - for (const auto& rt : Base::getAllNodes()) { - const auto& prefix = rt.first; - if (prefix.mask > bestMatchMask - && nexthop.inSubnet(prefix.network, prefix.mask)) { - bestMatch = rt.second; - bestMatchMask = prefix.mask; - } +std::shared_ptr> +RouteTableRib::fromFollyDynamic(const folly::dynamic& routes) { + auto rib = std::make_shared>(); + auto routesJson = routes[kRoutes]; + for (const auto& routeJson: routesJson) { + rib->addRoute(Route::fromFollyDynamic(routeJson)); } - return bestMatch; + return rib; } -FBOSS_INSTANTIATE_NODE_MAP(RouteTableRib, - RouteTableRibTraits); -FBOSS_INSTANTIATE_NODE_MAP(RouteTableRib, - RouteTableRibTraits); template class RouteTableRib; template class RouteTableRib; diff --git a/fboss/agent/state/RouteTableRib.h b/fboss/agent/state/RouteTableRib.h index 5fa71b4266436..d4110ed9c3025 100644 --- a/fboss/agent/state/RouteTableRib.h +++ b/fboss/agent/state/RouteTableRib.h @@ -10,30 +10,35 @@ // Copyright 2004-present Facebook. All rights reserved. #pragma once +#include "fboss/agent/FbossError.h" #include "fboss/agent/types.h" #include "fboss/agent/state/NodeMap.h" #include "fboss/agent/state/RouteTypes.h" +#include "neteng/fboss/lib/RadixTree.h" namespace facebook { namespace fboss { template class Route; -// FIXME: temporary solution before RadixTree -// use NodeMap to store all routes. -template using RouteTableRibTraits +template +class RouteTableRib; + +template using RouteTableRibNodeMapTraits = NodeMapTraits, Route>; template -class RouteTableRib - : public NodeMapT, RouteTableRibTraits> { +class RouteTableRibNodeMap + : public NodeMapT, + RouteTableRibNodeMapTraits> { public: - RouteTableRib(); - ~RouteTableRib() override; + explicit RouteTableRibNodeMap() {} + ~RouteTableRibNodeMap() override {} - typedef NodeMapT, RouteTableRibTraits> Base; - typedef RoutePrefix Prefix; - typedef typename Base::Node RouteType; + using Base = NodeMapT, + RouteTableRibNodeMapTraits>; + using Prefix = RoutePrefix; + using RouteType = typename Base::Node; bool empty() const { return Base::getAllNodes().size() == 0; @@ -43,10 +48,92 @@ class RouteTableRib return this->getAllNodes().size(); } + void addRoutes(const RouteTableRib& rib) { + for(const auto& routeItr: rib.routes()) { + addRoute(routeItr->value()); + } + } + private: + void addRoute(const std::shared_ptr>& rt) { + Base::addNode(rt); + } + // Inherit the constructors required for clone() + using Base::Base; + friend class CloneAllocator; +}; + +template +class RouteTableRib : public NodeBase { + public: + RouteTableRib() {} + RouteTableRib(NodeID id, uint32_t generation): + NodeBase(id, generation) {} + ~RouteTableRib() {} + + using Prefix = RoutePrefix; + using RouteType = Route; + using Routes = facebook::network::RadixTree>>; + + bool empty() const { + return size() == 0; + } + + uint64_t size() const { + return rib_.size(); + } + + const Routes& routes() const { return rib_; } + Routes& writableRoutes() { + CHECK(!isPublished()); + return rib_; + } + + void publish() override { + NodeBase::publish(); + for (auto routeIter: rib_) { + routeIter->value()->publish(); + } + } std::shared_ptr> exactMatch(const Prefix& prefix) const { - return Base::getNodeIf(prefix); + auto citr = rib_.exactMatch(prefix.network, prefix.mask); + return citr != rib_.end() ? citr->value() : nullptr; + } + std::shared_ptr> longestMatch(const AddrT& nexthop) const { + auto citr = rib_.longestMatch(nexthop, nexthop.bitCount()); + return citr != rib_.end() ? citr->value() : nullptr; + } + + std::shared_ptr clone() const { + auto routeTableRib = std::make_shared(getNodeID(), + getGeneration() + 1); + for (auto& routeItr: rib_) { + /* Explicitly insert cloned routes rather than calling + * rib_.clone(), which will just use the shared_ptr copy + * constructor + */ + routeTableRib->rib_.insert(routeItr->ipAddress(), + routeItr->masklen(), routeItr->value()->clone()); + } + return routeTableRib; + } + /* + * Serialize to folly::dynamic + */ + folly::dynamic toFollyDynamic() const; + + /* + * Deserialize from folly::dynamic + */ + static std::shared_ptr + fromFollyDynamic(const folly::dynamic& json); + /* + * Serialize to json string + */ + static std::shared_ptr + fromJson(const folly::fbstring& jsonStr) { + return fromFollyDynamic(folly::parseJson(jsonStr)); } - std::shared_ptr> longestMatch(const AddrT& nexthop) const; /* * The following functions modify the static state. @@ -54,23 +141,31 @@ class RouteTableRib * to a single thread. */ void addRoute(const std::shared_ptr>& rt) { - Base::addNode(rt); + auto inserted = rib_.insert(rt->prefix().network, + rt->prefix().mask, rt).second; + if (!inserted) { + throw FbossError("Prefix for: ", rt->str(), " already exists"); + } } void updateRoute(const std::shared_ptr>& rt) { - /* - * NOTE: This function is called in the loop of this rib. It shall not - * invalidate any existing iterators. - */ - Base::updateNode(rt); + auto itr = rib_.exactMatch(rt->prefix().network, rt->prefix().mask); + if (itr == rib_.end()) { + throw FbossError("Update failed, prefix for: ", rt->str(), + " not present"); + } + itr->value() = rt; } void removeRoute(const std::shared_ptr>& rt) { - Base::removeNode(rt); + auto erased = rib_.erase(rt->prefix().network, rt->prefix().mask); + if (!erased) { + throw FbossError("Remove failed, prefix for: ", rt->str(), + " not present"); + } } + private: - // Inherit the constructors required for clone() - using Base::Base; - friend class CloneAllocator; + Routes rib_; }; }} diff --git a/fboss/agent/state/RouteUpdater.cpp b/fboss/agent/state/RouteUpdater.cpp index e1cde878b9453..abba7c8d892a8 100644 --- a/fboss/agent/state/RouteUpdater.cpp +++ b/fboss/agent/state/RouteUpdater.cpp @@ -314,8 +314,8 @@ void RouteUpdater::resolve(RouteT* route, RtRibT* rib, ClonedRib* ribCloned) { template void RouteUpdater::setRoutesWithNhopsForResolution(RibT* rib) { - for (auto& rt : rib->getAllNodes()) { - auto route = rt.second.get(); + for (auto& rt : rib->routes()) { + auto route = rt.value().get(); if (route->isWithNexthops()) { if (route->isPublished()) { auto newRoute = route->clone(RibT::RouteType::Fields::COPY_ONLY_PREFIX); @@ -331,8 +331,8 @@ void RouteUpdater::setRoutesWithNhopsForResolution(RibT* rib) { namespace { template bool allRouteFlagsCleared(const RibT* rib) { - for (const auto& rt : rib->getAllNodes()) { - const auto route = rt.second.get(); + for (const auto& rt : rib->routes()) { + const auto& route = rt.value(); if (route->isWithNexthops() && !route->flagsCleared()) { return false; } @@ -359,9 +359,9 @@ void RouteUpdater::resolve() { } else { DCHECK(allRouteFlagsCleared(rib)); } - for (auto& rt : rib->getAllNodes()) { - if (rt.second->needResolve()) { - resolve(rt.second.get(), rib, &ribCloned.second); + for (auto& rt : rib->routes()) { + if (rt.value()->needResolve()) { + resolve(rt.value().get(), rib, &ribCloned.second); } } } @@ -375,9 +375,9 @@ void RouteUpdater::resolve() { } else { DCHECK(allRouteFlagsCleared(rib)); } - for (auto& rt : rib->getAllNodes()) { - if (rt.second->needResolve()) { - resolve(rt.second.get(), rib, &ribCloned.second); + for (auto& rt : rib->routes()) { + if (rt.value()->needResolve()) { + resolve(rt.value().get(), rib, &ribCloned.second); } } } @@ -453,38 +453,35 @@ bool RouteUpdater::dedupRoutes(const RibT* oldRib, RibT* newRib) { if (oldRib == newRib) { return isSame; } - const auto& oldRoutes = oldRib->getAllNodes(); - auto& newRoutes = newRib->writableNodes(); - auto oldIter = oldRoutes.begin(); - auto newIter = newRoutes.begin(); - while (oldIter != oldRoutes.end() && newIter != newRoutes.end()) { - const auto& oldPrefix = oldIter->first; - const auto& newPrefix = newIter->first; - if (oldPrefix < newPrefix) { + const auto& oldRoutes = oldRib->routes(); + auto& newRoutes = newRib->writableRoutes(); + // Copy routes from old route table if they are + // same. For matching prefixes, which don't have + // same attributes inherit the generation number + for (auto oldIter : oldRoutes) { + const auto& oldRt = oldIter->value(); + auto newIter = newRoutes.exactMatch(oldIter->ipAddress(), + oldIter->masklen()); + if (newIter == newRoutes.end()) { isSame = false; - oldIter++; - continue; - } - if (oldPrefix > newPrefix) { - isSame = false; - newIter++; continue; } - const auto& oldRt = oldIter->second; - auto& newRt = newIter->second; + auto& newRt = newIter->value(); if (oldRt->isSame(newRt.get())) { - // both routes are complete same, instead of using the new route, + // both routes are completely same, instead of using the new route, // we re-use the old route. - newIter->second = oldRt; + newIter->value() = oldRt; } else { isSame = false; newRt->inheritGeneration(*oldRt); } - oldIter++; - newIter++; } - if (oldIter != oldRoutes.end() || newIter != newRoutes.end()) { + if (newRoutes.size() != oldRoutes.size()) { isSame = false; + } else { + // If sizes are same we would have already caught any + // difference while looking up all routes from oldRoutes + // in newRoutes, so do nothing } return isSame; } diff --git a/fboss/agent/state/StateDelta.cpp b/fboss/agent/state/StateDelta.cpp index 39af15f172b0a..c4153a6495d2e 100644 --- a/fboss/agent/state/StateDelta.cpp +++ b/fboss/agent/state/StateDelta.cpp @@ -58,7 +58,7 @@ RTMapDelta StateDelta::getRouteTablesDelta() const { template class NodeMapDelta; template class NodeMapDelta; template class NodeMapDelta; -template class NodeMapDelta>; -template class NodeMapDelta>; +template class NodeMapDelta>; +template class NodeMapDelta>; }} // facebook::fboss diff --git a/fboss/agent/state/tests/RouteTests.cpp b/fboss/agent/state/tests/RouteTests.cpp index 409fe58e7d1b3..d5c12cf8f54f5 100644 --- a/fboss/agent/state/tests/RouteTests.cpp +++ b/fboss/agent/state/tests/RouteTests.cpp @@ -21,6 +21,7 @@ #include "fboss/agent/state/RouteTableMap.h" #include "fboss/agent/state/RouteDelta.h" #include "fboss/agent/state/NodeMapDelta.h" +#include "fboss/agent/state/NodeMapDelta-defs.h" #include "fboss/agent/state/StateDelta.h" #include "fboss/agent/state/SwitchState.h" #include "fboss/agent/gen-cpp/switch_config_types.h"