-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
VRF implementation for dataplane #242
base: main
Are you sure you want to change the base?
Conversation
6b2cf70
to
591f52f
Compare
@@ -103,6 +103,7 @@ extern LogPriority logPriority; | |||
#define YANET_RIB_PRIORITY_DEFAULT ((uint32_t)10000) | |||
#define YANET_RIB_PRIORITY_ROUTE_TUNNEL_FALLBACK ((uint32_t)11000) | |||
#define YANET_RIB_PRIORITY_ROUTE_REPEAT ((uint32_t)12000) | |||
#define YANET_RIB_VRF_DEFAULT "default" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
constexpr auto YANET_RIB_VRF_DEFAULT = "default";
#define YANET_RIB_PRIORITY_ROUTE_REPEAT ((uint32_t)12000) | ||
#define YANET_RIB_VRF_DEFAULT "default" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should replace that in 97th line in librib/libyabird.cpp
too:
std::get<1>(request) = {peer_address,
{"default", ///< @todo: vrf
YANET_RIB_PRIORITY_DEFAULT}};
bool is_ignored_table(const std::string& table_name) const | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const method returning value should be marked [[nodiscard]]
controlplane/route.h
Outdated
bool is_ignored_table(const std::string& table_name) const | ||
{ | ||
for (const auto& [name, module] : routes) | ||
{ | ||
(void)name; | ||
if (exist(module.ignore_tables, table_name)) | ||
{ | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here we checking that exist
is true for at least one element of the sequence, so I suggest to implement it like this:
[[nodiscard]] bool is_ignored_table(const std::string& table_name) const
{
return std::any_of(routes.begin(), routes.end(), [&table_name](const auto& pair) {
return exist(pair.second.ignore_tables, table_name);
});
}
cli/show.h
Outdated
std::get<3>(logicalPort) ? "true" : "false"); | ||
std::get<3>(logicalPort), | ||
std::get<4>(logicalPort) ? "true" : "false"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that this is not your code initially, but I would do
const auto& [physicalPortName, vlanId, vrf, macAddress, promiscuousMode] = logicalPort;
table.insert(logicalPortName,
physicalPortName,
vlanId,
vrf,
macAddress,
promiscuousMode ? "true" : "false");
here to avoid confusion
common/blocks_allocator.h
Outdated
{ | ||
available = 0; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If my above proposal is correct, add
void ReserveFirstN(uint8_t n)
{
for (uint8_t i = 0; i < n; ++i)
{
available.reset(i);
}
}
common/blocks_allocator.h
Outdated
uint8_t result = common::bits::get_last_enabled_bit_64(available); | ||
common::bits::disable_bit_64(available, result); | ||
return result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uint8_t first_available = available._Find_first();
available.reset(first_available);
return first_available;
common/blocks_allocator.h
Outdated
|
||
void Free(uint8_t bit) | ||
{ | ||
common::bits::enable_bit_64(available, bit); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
available.set(bit);
common/unittest/bits.cpp
Outdated
} | ||
|
||
TEST(Bits, build_mask) | ||
{ | ||
for (uint8_t length = 0; length <= 32; length++) | ||
{ | ||
uint32_t value = 0; | ||
for (uint8_t bit = 0; bit < length; bit++) | ||
{ | ||
common::bits::enable_bit_32(value, 31 - bit); | ||
} | ||
ASSERT_EQ(common::bits::build_mask_32(length), value); | ||
} | ||
|
||
for (uint8_t length = 0; length <= 64; length++) | ||
{ | ||
uint64_t value = 0; | ||
for (uint8_t bit = 0; bit < length; bit++) | ||
{ | ||
common::bits::enable_bit_64(value, 63 - bit); | ||
} | ||
ASSERT_EQ(common::bits::build_mask_64(length), value); | ||
} | ||
} | ||
|
||
uint8_t TestFirstEnabledBit32(uint64_t value) | ||
{ | ||
for (uint8_t index = 0; index < 32; index++) | ||
{ | ||
if (common::bits::get_bit_32(value, 31 - index) == 1) | ||
{ | ||
return index; | ||
} | ||
} | ||
return 32; | ||
} | ||
|
||
uint8_t TestFirstEnabledBit64(uint64_t value) | ||
{ | ||
for (uint8_t index = 0; index < 64; index++) | ||
{ | ||
if (common::bits::get_bit_64(value, 63 - index) == 1) | ||
{ | ||
return index; | ||
} | ||
} | ||
return 64; | ||
} | ||
|
||
TEST(Bits, get_first_enabled_bit) | ||
{ | ||
for (uint8_t length = 0; length <= 32; length++) | ||
{ | ||
uint32_t mask = common::bits::build_mask_32(length); | ||
ASSERT_EQ(common::bits::get_first_enabled_bit_32(~mask), length); | ||
} | ||
|
||
for (uint8_t length = 0; length <= 64; length++) | ||
{ | ||
uint64_t mask = common::bits::build_mask_64(length); | ||
ASSERT_EQ(common::bits::get_first_enabled_bit_64(~mask), length); | ||
} | ||
|
||
Test32(common::bits::get_first_enabled_bit_32, TestFirstEnabledBit32); | ||
Test64(common::bits::get_first_enabled_bit_64, TestFirstEnabledBit64); | ||
} | ||
|
||
template<typename T> | ||
uint8_t TestGetLastEnabledBit(T value) | ||
{ | ||
for (uint8_t index = 0; index < 64; index++) | ||
{ | ||
if (value % 2 == 1) | ||
{ | ||
return index; | ||
} | ||
value /= 2; | ||
} | ||
return sizeof(T) * 8; | ||
} | ||
|
||
TEST(Bits, get_last_enabled_bit) | ||
{ | ||
for (uint8_t length = 0; length <= 32; length++) | ||
{ | ||
uint32_t mask = common::bits::build_mask_32(length); | ||
ASSERT_EQ(common::bits::get_last_enabled_bit_32(mask), 32 - length); | ||
} | ||
|
||
for (uint8_t length = 0; length <= 64; length++) | ||
{ | ||
uint64_t mask = common::bits::build_mask_64(length); | ||
ASSERT_EQ(common::bits::get_last_enabled_bit_64(mask), 64 - length); | ||
} | ||
|
||
Test32(common::bits::get_last_enabled_bit_32, TestGetLastEnabledBit<uint32_t>); | ||
Test64(common::bits::get_last_enabled_bit_64, TestGetLastEnabledBit<uint64_t>); | ||
} | ||
|
||
} // namespace |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not needed
common/unittest/blocks_allocator.cpp
Outdated
#include <gtest/gtest.h> | ||
#include <memory> | ||
|
||
#include "../blocks_allocator.h" | ||
|
||
namespace | ||
{ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are good. Though I would add a test that covers a case where number_first_reserved
gets equal to blocks_total_
, smth like this should work:
TEST(BlocksAllocator, SequentialAllocationFull)
{
size_t blocks_number = 1024;
uint32_t reserved = blocks_number;
size_t size = common::allocator::BlocksAllocator<64>::GetBufferSize(blocks_number);
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(size);
common::allocator::BlocksAllocator<64> allocator;
allocator.Init(buffer.get(), size, reserved);
// no more blocks shoudl be available
ASSERT_EQ(allocator.Allocate(), common::allocator::BlocksAllocator<64>::null_block);
ASSERT_EQ(allocator.Stat(), std::make_pair(blocks_number, blocks_number));
for (size_t i = 0; i < reserved; i++)
{
ASSERT_EQ(allocator[i], buffer.get() + 64 * i);
}
}
dataplane/vrf.h
Outdated
constexpr inline uint32_t lpmValueIdInvalid = dataplane::lpmValueIdInvalid; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just using dataplane::lpmValueIdInvalid;
dataplane/vrf.h
Outdated
eResult Insert(stats_t& stats, tVrfId vrfId, const uint32_t& ipAddress, const uint8_t& mask, const uint32_t& valueId); | ||
eResult Remove(stats_t& stats, tVrfId vrfId, const uint32_t& ipAddress, const uint8_t& mask); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using const references with simple fixed-size integer types is redunant and obscure, I would suggest to remove that:
eResult Insert(Stats& stats, tVrfId vrf_id, uint32_t ip_address, uint8_t mask, uint32_t value_id);
eResult Remove(Stats& stats, tVrfId vrf_id, uint32_t ip_address, uint8_t mask);
dataplane/vrf.h
Outdated
void Swap(VrfLpm4Linear& other); | ||
void Lookup(const uint32_t* ipAddresses, const tVrfId* vrfIds, uint32_t* valueIds, const unsigned int& count) const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
annotate Swap with noexcept
dataplane/vrf.h
Outdated
eResult Clear(); | ||
eResult CopyFrom(const VrfLpm4Linear& other, stats_t& stats); | ||
void Swap(VrfLpm4Linear& other); | ||
void Lookup(const uint32_t* ipAddresses, const tVrfId* vrfIds, uint32_t* valueIds, const unsigned int& count) const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const unsigned int& count
-> int count
dataplane/vrf.h
Outdated
{ | ||
VrfLpm4Linear::BlocksAllocator::Index next_block; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need to add VrfLpm4Linear::
here, we're already in that namespace, just BlocksAllocator::Index next_block;
dataplane/vrf.cpp
Outdated
while (block != nullptr) | ||
{ | ||
if (block->ipAddress == ipAddress && block->mask == mask) | ||
{ | ||
block->used = 0; | ||
UpdateStats(allocator_, stats); | ||
return eResult::success; | ||
} | ||
block = Block(block->next_block); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here unordered_map
will work too, by just finding the block
pointer in O(1)
dataplane/vrf.cpp
Outdated
{ | ||
if (vrfId >= YANET_RIB_VRF_MAX_NUMBER) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use extended_chunks_size_min
instead of YANET_RIB_VRF_MAX_NUMBER
, we done
constexpr static uint64_t extended_chunks_size_min = YANET_RIB_VRF_MAX_NUMBER;
for a reason
And in 13 other places in this commit where YANET_RIB_VRF_MAX_NUMBER
gets used
dataplane/vrf.cpp
Outdated
for (unsigned index = 0; index < YANET_RIB_VRF_MAX_NUMBER; index++) | ||
{ | ||
OneBlock* block = Block(index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Segmentation fault.
What if we create a VrfLpm4Linear with an allocator with it's void Init(void* pointer, size_t size, Index number_first_reserved)
being called with size
less than YANET_RIB_VRF_MAX_NUMBER * BlockSize
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this class is designed for VRF, we should add some guard against initializing that class with buffers less than vrf max number times BlockSize with a "throw" in a constuctor, for example
dataplane/vrf.cpp
Outdated
VrfLpm4Linear::OneBlock* VrfLpm4Linear::Block(BlocksAllocator::Index index) const | ||
{ | ||
return reinterpret_cast<VrfLpm4Linear::OneBlock*>((index == BlocksAllocator::null_block ? nullptr : allocator_[index])); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO this is cleaner:
VrfLpm4Linear::OneBlock* VrfLpm4Linear::Block(BlocksAllocator::Index index) const
{
if (index == BlocksAllocator::null_block)
{
return nullptr;
}
return reinterpret_cast<OneBlock*>(allocator_[index]);
}
dataplane/vrf.cpp
Outdated
VrfLpm6Linear::VrfLpm6Linear(BlocksAllocator& allocator) : | ||
allocator_(allocator) | ||
{ | ||
Clear(); | ||
} | ||
|
||
eResult VrfLpm6Linear::Insert(stats_t& stats, tVrfId vrfId, const std::array<uint8_t, 16>& ipAddress, const uint8_t& mask, const uint32_t& valueId) | ||
{ | ||
if (vrfId >= YANET_RIB_VRF_MAX_NUMBER) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same review comment as for VrfLpm4Linear
applies for VrfLpm6Linear
too
dataplane/unittest/meson.build
Outdated
'sdp.cpp', | ||
'random_prefixes.cpp', | ||
'vrf_lpm_linear.cpp', | ||
'../vrf.cpp', | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be done like in controlplane/unittest/meson.build
, with sources defined in a separate variable:
dataplane_sources = files('../vrf.cpp',
)
sources = files('unittest.cpp',
'ip_address.cpp',
'hashtable.cpp',
'sdp.cpp',
'random_prefixes.cpp',
'vrf_lpm_linear.cpp',
'vrf_lpm_binary.cpp',
)
arch = 'corei7'
cpp_args_append = ['-march=' + arch]
unittest = executable('yanet-dataplane-unittest',
[dataplane_sources, sources],
dataplane/unittest/random_prefixes.h
Outdated
std::string Description() const; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[[nodiscard]]
std::poisson_distribution<> poisson(test_data.depth_lambda); | ||
std::uniform_real_distribution<> uniform(0.0, 1.0); | ||
|
||
std::shared_ptr<Node> root = std::make_shared<Node>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auto root = std::make_shared<Node>();
work.push({root, 0}); | ||
std::vector<ipv4_prefix_t> prefixes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use emplace
instead of push with a temporary object:
work.emplace(root, 0);
prefixes.push_back({addr, static_cast<uint8_t>(node->depth)}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prefixes.emplace_back(addr, static_cast<uint8_t>(node->depth));
std::vector<ipv6_prefix_t> CreatePrefixesIpv6(const OneTestData& test_data) | ||
{ | ||
std::mt19937 generator(test_data.rand_init_value); | ||
std::uniform_int_distribution<> digits_distribution(0, 1); | ||
|
||
std::shared_ptr<Node> root = BuildRandomTree(128, test_data, generator); | ||
std::queue<std::pair<std::shared_ptr<Node>, std::array<uint8_t, 16>>> work; | ||
work.push({root, std::array<uint8_t, 16>()}); | ||
std::vector<ipv6_prefix_t> prefixes; | ||
|
||
while (!work.empty()) | ||
{ | ||
auto [node, value] = work.front(); | ||
work.pop(); | ||
if (node->has_value) | ||
{ | ||
ipv6_address_t addr = ipv6_address_t::convert(common::ipv6_address_t(value)); | ||
prefixes.push_back({addr, static_cast<uint8_t>(node->depth)}); | ||
} | ||
|
||
if (node->left != nullptr) | ||
{ | ||
work.push({node->left, CreatePartAddressIpv6(value, node->depth, node->left->depth, 0, digits_distribution, generator)}); | ||
} | ||
if (node->right != nullptr) | ||
{ | ||
work.push({node->right, CreatePartAddressIpv6(value, node->depth, node->right->depth, 1, digits_distribution, generator)}); | ||
} | ||
} | ||
|
||
std::shuffle(prefixes.begin(), prefixes.end(), generator); | ||
|
||
return prefixes; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comments as for v4 version
TEST(VrfLpm4, SimpleMap) | ||
{ | ||
VrfLpmMap<ipv4_prefix_t, uint32_t, uint32_t> vrf_map; | ||
SimpleTestVrfLpm4(vrf_map); | ||
} | ||
|
||
TEST(VrfLpm4, SimpleLinear) | ||
{ | ||
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(size_of_mem); | ||
VrfLpm4Linear::BlocksAllocator allocator; | ||
allocator.Init(buffer.get(), size_of_mem, YANET_RIB_VRF_MAX_NUMBER); | ||
VrfLpm4Linear vrf_linear(allocator); | ||
|
||
SimpleTestVrfLpm4(vrf_linear); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could greatly reduce code duplication by utilizing GoogleTests fixtures and TYPED_TEST_SUITE
to parametrize tests for both VrfLpmMap
and VrfLpmLinear
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here we can use that VrfLpmTestBase
to automatically define tests for both v4 and v6
dataplane/unittest/vrf_lpm_common.h
Outdated
uint64_t hi = rte_cpu_to_be_64(*(reinterpret_cast<const uint64_t*>(prefix.address.bytes))); | ||
uint64_t low = rte_cpu_to_be_64(*(reinterpret_cast<const uint64_t*>(prefix.address.bytes + 8))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use memcpy when dealing with simple u64 types instead of dereferencing a pointer with reintepret casts, much cleaner this way:
uint64_t hi = 0, low = 0;
std::memcpy(&hi, prefix.address.bytes, sizeof(hi));
std::memcpy(&low, prefix.address.bytes + 8, sizeof(low));
hi = rte_cpu_to_be_64(hi);
low = rte_cpu_to_be_64(low);
dataplane/unittest/vrf_lpm_common.h
Outdated
*(reinterpret_cast<uint64_t*>(result.bytes)) = rte_cpu_to_be_64(hi); | ||
*(reinterpret_cast<uint64_t*>(result.bytes + 8)) = rte_cpu_to_be_64(low); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hi = rte_cpu_to_be_64(hi);
low = rte_cpu_to_be_64(low);
std::memcpy(result.bytes.data(), &hi, sizeof(hi));
std::memcpy(result.bytes.data() + 8, &low, sizeof(low));
dataplane/unittest/vrf_lpm_common.h
Outdated
inline uint32_t IpAddressToInternal(const ipv4_address_t& address) | ||
{ | ||
return rte_cpu_to_be_32(address.address); | ||
} | ||
|
||
inline std::array<uint8_t, 16> IpAddressToInternal(const ipv6_address_t& address) | ||
{ | ||
return common::ipv6_address_t(address.bytes); | ||
} | ||
|
||
inline std::string AddressStr(const ipv6_address_t& address) | ||
{ | ||
return common::ipv6_address_t(address.bytes).toString(); | ||
} | ||
|
||
inline std::string AddressStr(const std::array<uint8_t, 16>& address) | ||
{ | ||
return common::ipv6_address_t(address).toString(); | ||
} | ||
|
||
inline std::string AddressStr(const ipv4_address_t& address) | ||
{ | ||
return common::ipv4_address_t(address.address).toString(); | ||
} | ||
|
||
inline std::string AddressStr(const uint32_t& address) | ||
{ | ||
return common::ipv4_address_t(address).toString(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are functions that are used only for this .h file, so they should be static
instead of inline
.
dataplane/unittest/vrf_lpm_common.h
Outdated
inline uint32_t IpAddressToInternal(const ipv4_address_t& address) | ||
{ | ||
return rte_cpu_to_be_32(address.address); | ||
} | ||
|
||
inline std::array<uint8_t, 16> IpAddressToInternal(const ipv6_address_t& address) | ||
{ | ||
return common::ipv6_address_t(address.bytes); | ||
} | ||
|
||
inline std::string AddressStr(const ipv6_address_t& address) | ||
{ | ||
return common::ipv6_address_t(address.bytes).toString(); | ||
} | ||
|
||
inline std::string AddressStr(const std::array<uint8_t, 16>& address) | ||
{ | ||
return common::ipv6_address_t(address).toString(); | ||
} | ||
|
||
inline std::string AddressStr(const ipv4_address_t& address) | ||
{ | ||
return common::ipv4_address_t(address.address).toString(); | ||
} | ||
|
||
inline std::string AddressStr(const uint32_t& address) | ||
{ | ||
return common::ipv4_address_t(address).toString(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Speaking about #242 (comment):
We need to create a struct, smth like AddressTraits
, explicitly instantiate it with uint32_t
and std::array<uint8_t, 16>
, and use it with fixture to parametrize tests.
smth like this:
template<typename AddressType>
struct AddressTraits;
template<>
struct AddressTraits<uint32_t> {
static uint32_t ToInternal(const ipv4_address_t& address) {
return rte_cpu_to_be_32(address.address);
}
static std::string ToString(const uint32_t& address) {
return common::ipv4_address_t(address).toString();
}
static std::vector<uint32_t> AddressesForTest(const ipv4_prefix_t& prefix) {
return {rte_cpu_to_be_32(prefix.address.address), LastAddress(prefix)};
}
static uint32_t LastAddress(const ipv4_prefix_t& prefix) {
// impl
}
};
template<>
struct AddressTraits<std::array<uint8_t, 16>> {
static std::array<uint8_t, 16> ToInternal(const ipv6_address_t& address) {
return common::ipv6_address_t(address.bytes);
}
static std::string ToString(const std::array<uint8_t, 16>& address) {
return common::ipv6_address_t(address).toString();
}
static std::vector<ipv6_address_t> AddressesForTest(const ipv6_prefix_t& prefix) {
return {prefix.address, LastAddress(prefix)};
}
static ipv6_address_t LastAddress(const ipv6_prefix_t& prefix) {
// impl
}
};
Maybe we should also add to corresponding instantiations these things, consider it
static constexpr OneTestData tests_ipv4[] = {
{1000, 20.0, 0.8, true, 42, 10}, // tree sizes: 1-4
{1000, 15.0, 0.8, true, 42, 10}, // tree sizes: 2-7
{1000, 10.0, 0.8, true, 42, 10}, // tree sizes: 6-15
{1000, 5.0, 0.8, true, 42, 10}, // tree sizes: 86-271
{10000, 3.0, 0.1, true, 42, 10}, // tree sizes: 271-992
{10000, 3.0, 0.2, true, 42, 10} // tree sizes: 574-2053
};
static constexpr size_t number_tests_ipv4 = sizeof(tests_ipv4) / sizeof(OneTestData);
dataplane/unittest/vrf_lpm_common.h
Outdated
while ((iter1 != list1.end()) && (iter2 != list2.end())) | ||
{ | ||
const auto& [vrf1, address1, mask1, value1] = *iter1; | ||
const auto& [vrf2, address2, mask2, value2] = *iter2; | ||
if ((vrf1 != vrf2) || (address1 != address2) || (mask1 != mask2) || (value1 != value2)) | ||
{ | ||
YANET_LOG_ERROR("[vrf=%d, %s/%d, value=%d] != [vrf=%d, %s/%d, value=%d]\n", | ||
vrf1, | ||
AddressStr(address1).c_str(), | ||
uint16_t(mask1), | ||
value1, | ||
vrf2, | ||
AddressStr(address2).c_str(), | ||
uint16_t(mask2), | ||
value2); | ||
return false; | ||
} | ||
++iter1; | ||
++iter2; | ||
} | ||
if (iter1 != list1.end()) | ||
{ | ||
const auto& [vrf1, address1, mask1, value1] = *iter1; | ||
YANET_LOG_ERROR("list2 empty, in list1: vrf=%d, %s/%d, value=%d\n", | ||
vrf1, | ||
AddressStr(address1).c_str(), | ||
uint16_t(mask1), | ||
value1); | ||
return false; | ||
} | ||
if (iter2 != list2.end()) | ||
{ | ||
const auto& [vrf2, address2, mask2, value2] = *iter2; | ||
YANET_LOG_ERROR("list1 empty, in list2: vrf=%d, %s/%d, value=%d\n", | ||
vrf2, | ||
AddressStr(address2).c_str(), | ||
uint16_t(mask2), | ||
value2); | ||
return false; | ||
} | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just list1 == list2
? std::vector
and std::tuple
have ==
operator
dataplane/unittest/vrf_lpm_common.h
Outdated
template<typename PrefixType, typename PrefixTree> | ||
bool FillPrefixTree(const std::vector<PrefixType>& prefixes, PrefixTree& tree, tVrfId vrf_number) | ||
{ | ||
uint32_t value = 0; | ||
stats_t stats = {0, 0}; | ||
size_t prefix_number = prefixes.size(); | ||
for (tVrfId vrf = 0; vrf < vrf_number; vrf++) | ||
{ | ||
for (size_t index = 0; index < prefix_number; index += 1 + 5 * vrf) | ||
{ | ||
auto address = prefixes[index].address; | ||
if (tree.Insert(stats, vrf, IpAddressToInternal(address), prefixes[index].mask, value) != eResult::success) | ||
{ | ||
YANET_LOG_ERROR("Error add prefix, vrf=%d, prefix=%s/%d, value=%d\n", | ||
vrf, | ||
AddressStr(IpAddressToInternal(prefixes[index].address)).c_str(), | ||
(uint16_t)prefixes[index].mask, | ||
value); | ||
return false; | ||
} | ||
value++; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
template<typename PrefixTree1, typename PrefixTree2> | ||
bool CompareTrees(const PrefixTree1& tree1, const PrefixTree2& tree2) | ||
{ | ||
auto list1 = tree1.GetFullList(); | ||
auto list2 = tree2.GetFullList(); | ||
std::sort(list1.begin(), list1.end()); | ||
std::sort(list2.begin(), list2.end()); | ||
auto iter1 = list1.begin(); | ||
auto iter2 = list2.begin(); | ||
|
||
while ((iter1 != list1.end()) && (iter2 != list2.end())) | ||
{ | ||
const auto& [vrf1, address1, mask1, value1] = *iter1; | ||
const auto& [vrf2, address2, mask2, value2] = *iter2; | ||
if ((vrf1 != vrf2) || (address1 != address2) || (mask1 != mask2) || (value1 != value2)) | ||
{ | ||
YANET_LOG_ERROR("[vrf=%d, %s/%d, value=%d] != [vrf=%d, %s/%d, value=%d]\n", | ||
vrf1, | ||
AddressStr(address1).c_str(), | ||
uint16_t(mask1), | ||
value1, | ||
vrf2, | ||
AddressStr(address2).c_str(), | ||
uint16_t(mask2), | ||
value2); | ||
return false; | ||
} | ||
++iter1; | ||
++iter2; | ||
} | ||
if (iter1 != list1.end()) | ||
{ | ||
const auto& [vrf1, address1, mask1, value1] = *iter1; | ||
YANET_LOG_ERROR("list2 empty, in list1: vrf=%d, %s/%d, value=%d\n", | ||
vrf1, | ||
AddressStr(address1).c_str(), | ||
uint16_t(mask1), | ||
value1); | ||
return false; | ||
} | ||
if (iter2 != list2.end()) | ||
{ | ||
const auto& [vrf2, address2, mask2, value2] = *iter2; | ||
YANET_LOG_ERROR("list1 empty, in list2: vrf=%d, %s/%d, value=%d\n", | ||
vrf2, | ||
AddressStr(address2).c_str(), | ||
uint16_t(mask2), | ||
value2); | ||
return false; | ||
} | ||
return true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And these methods should be within a typed test from which we will inherit later, parametrized by AddressType, like:
template<typename PrefixType, typename AddressType, typename LookupAddressType, typename VrfLpmType>
class VrfLpmTestBase : public ::testing::Test {
protected:
using Traits = AddressTraits<AddressType>;
void SetUp() override {
// common init
}
bool FillPrefixTree(const std::vector<PrefixType>& prefixes, VrfLpmType& tree, tVrfId vrf_number) {
// implementation
// Use Traits::ToInternalinstead of IpAddressToInternal
}
bool CompareTrees(const VrfLpmType& tree1, const VrfLpmType& tree2) {
auto list1 = tree1.GetFullList();
auto list2 = tree2.GetFullList();
std::sort(list1.begin(), list1.end());
std::sort(list2.begin(), list2.end());
return list1 == list2;
}
bool CheckRequests(tVrfId vrf_number, const std::vector<PrefixType>& prefixes,
const VrfLpmType& tree1, const VrfLpmType& tree2) {
// implementation
// Use Traits::AddressesForTest instead of AddressesForTest
}
};
dataplane/unittest/vrf_lpm_common.h
Outdated
template<typename VrfLpmType> | ||
void SimpleTestVrfLpm4(VrfLpmType& vrf_lpm) | ||
{ | ||
std::vector<std::string> str_prefixes = {"1.0.0.0/24", "2.0.0.0/24", "3.0.0.0/24", "0.0.0.0/0", "1.0.0.0/28", "1.0.0.0/30"}; | ||
std::vector<std::string> str_addresses = {"1.0.0.17", "2.0.0.1", "3.0.0.1", "4.0.0.1", "1.0.0.5", "1.0.0.1"}; | ||
std::vector<uint32_t> values = {0, 1, 2, 3, 4, 5}; | ||
|
||
stats_t stats; | ||
uint32_t value = 0; | ||
for (const std::string& str_prefix : str_prefixes) | ||
{ | ||
common::ipv4_prefix_t prefix(str_prefix); | ||
ASSERT_EQ(vrf_lpm.Insert(stats, 0, uint32_t(prefix.address()), prefix.mask(), value++), eResult::success); | ||
} | ||
|
||
for (size_t index = 0; index < str_addresses.size(); index++) | ||
{ | ||
common::ipv4_address_t address(str_addresses[index]); | ||
uint32_t addr = rte_cpu_to_be_32(uint32_t(address)); | ||
tVrfId vrfId = 0; | ||
uint32_t value; | ||
vrf_lpm.Lookup(&addr, &vrfId, &value, 1); | ||
ASSERT_EQ(value, values[index]) << "Error check for " << str_addresses[index]; | ||
} | ||
} | ||
|
||
template<typename VrfLpmType> | ||
void SimpleTestVrfLpm6(VrfLpmType& vrf_lpm) | ||
{ | ||
std::vector<std::string> str_prefixes = {"::/0", "7e01::/64", "7e02::/64", "7e03::/64", "7e01::/96", "7e01::f0/124"}; | ||
std::vector<std::string> str_addresses = {"7e00::1", "7e01::1:0:0:f1", "7e02::1", "7e03::1", "7e01::1:f1", "7e01::f1"}; | ||
std::vector<uint32_t> values = {0, 1, 2, 3, 4, 5}; | ||
|
||
stats_t stats; | ||
uint32_t value = 0; | ||
for (const std::string& str_prefix : str_prefixes) | ||
{ | ||
common::ipv6_prefix_t prefix(str_prefix); | ||
ASSERT_EQ(vrf_lpm.Insert(stats, 0, prefix.address(), prefix.mask(), value++), eResult::success); | ||
} | ||
|
||
for (size_t index = 0; index < str_addresses.size(); index++) | ||
{ | ||
ipv6_address_t address = ipv6_address_t::convert(common::ipv6_address_t(str_addresses[index])); | ||
tVrfId vrfId = 0; | ||
uint32_t value; | ||
vrf_lpm.Lookup(&address, &vrfId, &value, 1); | ||
ASSERT_EQ(value, values[index]) << "Error check for " << str_addresses[index]; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be added to VrfLpmTestBase
as well, so we can use AddressType
with things like if constexpr
to not duplicate the code.
Those strings can be declared as constexpr static arrays of string_view
in the AddressTraits:
constexpr std::array myStrings = { "::/0"sv, "7e01::/64"sv, ...};
TEST(VrfLpm4, SimpleMap) | ||
{ | ||
VrfLpmMap<ipv4_prefix_t, uint32_t, uint32_t> vrf_map; | ||
SimpleTestVrfLpm4(vrf_map); | ||
} | ||
|
||
TEST(VrfLpm4, SimpleLinear) | ||
{ | ||
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(size_of_mem); | ||
VrfLpm4Linear::BlocksAllocator allocator; | ||
allocator.Init(buffer.get(), size_of_mem, YANET_RIB_VRF_MAX_NUMBER); | ||
VrfLpm4Linear vrf_linear(allocator); | ||
|
||
SimpleTestVrfLpm4(vrf_linear); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here we can use that VrfLpmTestBase
to automatically define tests for both v4 and v6
dataplane/updater.h
Outdated
if (stats.extended_chunks_size > 2 * ObjectType::extended_chunks_size_min && | ||
stats.extended_chunks_count < stats.extended_chunks_size / 4) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add should_shrink
and use it here
dataplane/updater.h
Outdated
{ | ||
limits.emplace_back(name + ".extended_chunks", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
limits.emplace_back(std::string(name) + ".extended_chunks",
dataplane/updater.h
Outdated
eResult create(const uint64_t chunks_size, bool save_old) | ||
{ | ||
size_t buffer_size = BlocksAllocator::GetBufferSize(chunks_size); | ||
void* next_pointer = memory_manager->create<void*>(name.data(), socket_id, buffer_size); | ||
if (next_pointer == nullptr) | ||
{ | ||
return eResult::errorAllocatingMemory; | ||
} | ||
BlocksAllocator tmp_allocator; | ||
tmp_allocator.Init(next_pointer, buffer_size, YANET_RIB_VRF_MAX_NUMBER); | ||
ObjectType tmp_object(tmp_allocator); | ||
|
||
stats_t next_stats; | ||
next_stats.extended_chunks_count = 0; | ||
next_stats.extended_chunks_size = chunks_size; | ||
|
||
if (save_old) | ||
{ | ||
eResult result = tmp_object.CopyFrom(vrf_lpm_object, next_stats); | ||
if (result != eResult::success) | ||
{ | ||
return result; | ||
} | ||
} | ||
|
||
stats = next_stats; | ||
vrf_lpm_object.Swap(tmp_object); | ||
if (tmp_allocator.GetPointer() != nullptr) | ||
{ | ||
memory_manager->destroy(tmp_allocator.GetPointer()); | ||
} | ||
|
||
return eResult::success; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Current implementation with ObjectType storing reference to an allocator is really obscure. One has no
way of understanding why this code is correct if he don't know that ObjectType actually stores a reference to an allocator. Cause it looks like a regular value semantics, which should lead to an allocator
object being in a wrong state.
But here we have:
updater_vrf_lpm
gets constructed andvrf_lpm_object(allocator)
gets called from the member initializer list, storing the reference to thatallocator
object in anvrf_lpm_object
member.- user calls
init
which callscreate
create
creates atmp_allocator
and initializestmp_object
with it.tmp_object
now stores a reference to thattmp_allocator
vrf_lpm_object.Swap(tmp_object)
gets called which swaps allocators, which due to the fact that they are references, swaps the actualallocator
object- old pointer gets deleted through
tmp_object
Thi is obscure. Also manual memory managing though swappedtmp_object
looks bad.
As I suggested eariler, we shouldn't use reference members:
#242 (comment)
Instead, we should:
- Delete copy and move constructors from
ObjectType
. This is really important as apparently the memory that we have there gets managed by an external object, and if that pointer gets copied by a copy constructor, and the original gets deleted, we will have use-after-free - Just create
vrf_lpm_object
with default constructor inupdater_vrf_lpm
consturctor, it will have allocator with null pointer, which is fine ObjectType
already hasCopyFrom
, now we need to implementMoveFrom
that accepts memory_manager
and looks something like this:
void move_from(Obj obj, memory_manager)
{
if (allocator_.GetPointer() != nullptr) allocator_.GetPointer());
// move data
}
this way this code gets turned into:
eResult create(const uint64_t chunks_size, bool save_old)
{
void* next_pointer = memory_manager->create(...)
BlocksAllocator tmp_allocator;
tmp_allocator.Init(...);
ObjectType tmp_object(std::move(tmp_allocator));
if (save_old)
{
tmp_object.CopyFrom(vrf_lpm_object);
}
vrf_lpm_object.MoveFrom(tmp_object, memory_manager);
return eResult::success;
}
And if the vrf_lpm_object
was nullptr, memory_manager
from MoveFrom
will just ignore it.
If it wasn't, it will correctly delete it and fill with the new data.
This way we will ensure that ObjectType
has correct ownership of an allocator due to it being moved into a constructor
4. Delete BlocksAllocator allocator;
as a field, why do we even need it here?
report(nlohmann::json& report)
should get a pointer from an actual object anyway, like vrf_lpm_object.GetAllocator().GetPointer()
dataplane/vrf.cpp
Outdated
*(uint64_t*)result.data() = hi; | ||
*(uint64_t*)(result.data() + 8) = low; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auto hi_bytes = reinterpret_cast<const uint8_t*>(&hi);
auto low_bytes = reinterpret_cast<const uint8_t*>(&low);
std::copy(hi_bytes, hi_bytes + sizeof(hi), result.begin());
std::copy(low_bytes, low_bytes + sizeof(low), result.begin() + sizeof(hi));
dataplane/vrf.cpp
Outdated
VrfLpm4BinaryTree::VrfLpm4BinaryTree(BlocksAllocator& allocator) : | ||
allocator_(allocator) | ||
{ | ||
Init(); | ||
} | ||
|
||
eResult VrfLpm4BinaryTree::Insert(stats_t& stats, tVrfId vrfId, const uint32_t& ipAddress, const uint8_t& mask, const uint32_t& valueId) | ||
{ | ||
if (mask > 32 || valueId & 0xFF000000) | ||
{ | ||
YANET_LOG_DEBUG("invalid prefix or value\n"); | ||
return eResult::invalidArguments; | ||
} | ||
if (vrfId >= YANET_RIB_VRF_MAX_NUMBER) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the same issues as with Linear, please.
Like avoidance of reference field member, emplace
instead of push with braces initialization: push({...})
, should throw in constructor if the size allocated is not enough for YANET_RIB_VRF_MAX_NUMBER
, otherwise loop like this
for (tVrfId vrfId = 0; vrfId < YANET_RIB_VRF_MAX_NUMBER; vrfId++)
{
OneBlock* block = Block(vrfId);
will segfault on Block
when we'll call allocator_[index]
And also an observation that we can cache the blocks by a pair ipAddress + mask to avoid looking them up each time
- Fixed filling in lpm stats. - Checking that the route table is ignored has been moved.
- VRF storage has been added in some structures, and vrf is also passed in calls to some functions. - In controlplane.conf one can set the vrf in logicalPort. In the metadata of the network packet the vrfId is filled in according to the value from logicalPort. - A VrfIdStorage object has been added to the cControlPlane class, which issues an id named vrf.
In controlplane.conf, one can set the vrf for lan and wan in the nat64statefull and nat46clat sections, these values will be set in the packet metadata.
Previously, it was possible to add routing rules for different VRFs, but it was not possible to set VRFs for packets and only 'default' was used for them.
In the current change, the ability to set VRF for the entire logical port has been added. Later it will be possible to set VRF by acl rules.
To solve the LPM problem, an algorithm like to 'a path-compressed trie' is used.