xrpld
Loading...
Searching...
No Matches
Livecache.cpp
1#include <xrpl/peerfinder/detail/Livecache.h>
2
3#include <xrpl/basics/chrono.h>
4#include <xrpl/basics/random.h>
5#include <xrpl/beast/net/IPAddressV4.h>
6#include <xrpl/beast/net/IPAddressV6.h>
7#include <xrpl/beast/net/IPEndpoint.h>
8#include <xrpl/beast/utility/Journal.h>
9#include <xrpl/beast/utility/PropertyStream.h>
10#include <xrpl/json/JsonPropertyStream.h>
11#include <xrpl/json/json_forwards.h>
12#include <xrpl/json/json_value.h>
13#include <xrpl/peerfinder/Types.h>
14#include <xrpl/peerfinder/detail/Tuning.h>
15
16#include <boost/algorithm/string/classification.hpp>
17#include <boost/algorithm/string/split.hpp>
18#include <boost/algorithm/string/trim.hpp>
19#include <boost/lexical_cast.hpp>
20
21#include <gtest/gtest.h>
22#include <helpers/TestSink.h>
23
24#include <algorithm>
25#include <array>
26#include <cstdint>
27#include <iterator>
28#include <string>
29#include <utility>
30#include <vector>
31
32namespace xrpl::peer_finder {
33namespace {
34
35class LivecacheTest : public ::testing::Test
36{
37protected:
38 static beast::Journal
39 journal()
40 {
41 return beast::Journal{TestSink::instance()};
42 }
43
44 static beast::ip::Endpoint
45 endpoint(std::uint16_t index, bool v4 = true)
46 {
47 auto const port = static_cast<std::uint16_t>(10000 + index);
48
49 if (v4)
50 {
51 auto bytes = beast::ip::AddressV4::bytes_type{
52 {54,
53 static_cast<std::uint8_t>((index / 256) % 256),
54 static_cast<std::uint8_t>(index % 256),
55 1}};
56 return beast::ip::Endpoint{beast::ip::Address{beast::ip::AddressV4{bytes}}, port};
57 }
58
59 auto bytes = beast::ip::AddressV6::bytes_type{
60 {0x20,
61 0x01,
62 0x0d,
63 0xb8,
64 0,
65 0,
66 0,
67 0,
68 0,
69 0,
70 0,
71 0,
72 0,
73 static_cast<std::uint8_t>((index / 256) % 256),
74 static_cast<std::uint8_t>(index % 256),
75 1}};
76 return beast::ip::Endpoint{beast::ip::Address{beast::ip::AddressV6{bytes}}, port};
77 }
78
79 void
80 addEndpoint(beast::ip::Endpoint const& ep, std::uint32_t hops = 0)
81 {
82 cache_.insert(Endpoint{ep, hops});
83 }
84
85 TestStopwatch clock_;
86 Livecache<> cache_{clock_, journal()};
87};
88
89} // namespace
90
91TEST_F(LivecacheTest, basic_insert)
92{
93 EXPECT_TRUE(cache_.empty());
94
95 for (auto i = 0; i < 10; ++i)
96 addEndpoint(endpoint(i, true));
97
98 EXPECT_FALSE(cache_.empty());
99 EXPECT_EQ(cache_.size(), 10u);
100
101 for (auto i = 10; i < 20; ++i)
102 addEndpoint(endpoint(i, false));
103
104 EXPECT_FALSE(cache_.empty());
105 EXPECT_EQ(cache_.size(), 20u);
106}
107
108TEST_F(LivecacheTest, insert_update_keeps_lowest_hop_count)
109{
110 auto const ep1 = Endpoint{endpoint(1), 2};
111 cache_.insert(ep1);
112 ASSERT_EQ(cache_.size(), 1u);
113 EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u);
114
115 auto const ep2 = Endpoint{ep1.address, 4};
116 cache_.insert(ep2);
117 EXPECT_EQ(cache_.size(), 1u);
118 EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u);
119
120 auto const ep3 = Endpoint{ep1.address, 2};
121 cache_.insert(ep3);
122 EXPECT_EQ(cache_.size(), 1u);
123 EXPECT_EQ((cache_.hops.begin() + 2)->begin()->hops, 2u);
124
125 auto const ep4 = Endpoint{ep1.address, 1};
126 cache_.insert(ep4);
127 EXPECT_EQ(cache_.size(), 1u);
128 EXPECT_EQ((cache_.hops.begin() + 1)->begin()->hops, 1u);
129}
130
131TEST_F(LivecacheTest, hop_iterators_support_const_reverse_and_move_back)
132{
133 auto const ep1 = Endpoint{endpoint(1), 1};
134 auto const ep2 = Endpoint{endpoint(2), 1};
135 cache_.insert(ep1);
136 cache_.insert(ep2);
137
138 auto hop = *(cache_.hops.begin() + 1);
139 ASSERT_NE(hop.begin(), hop.end());
140 ASSERT_NE(hop.cbegin(), hop.cend());
141 ASSERT_NE(hop.rbegin(), hop.rend());
142 ASSERT_NE(hop.crbegin(), hop.crend());
143
144 auto const firstAddress = hop.begin()->address;
145 hop.moveBack(hop.begin());
146 EXPECT_EQ(hop.rbegin()->address, firstAddress);
147
148 auto const& constHops = cache_.hops;
149 EXPECT_NE(constHops.begin(), constHops.end());
150 EXPECT_NE(constHops.cbegin(), constHops.cend());
151 EXPECT_NE(constHops.rbegin(), constHops.rend());
152 EXPECT_NE(constHops.crbegin(), constHops.crend());
153
154 auto const constHop = *(constHops.cbegin() + 1);
155 EXPECT_EQ(std::distance(constHop.begin(), constHop.end()), 2);
156 EXPECT_EQ(std::distance(constHop.cbegin(), constHop.cend()), 2);
157 EXPECT_EQ(std::distance(constHop.rbegin(), constHop.rend()), 2);
158 EXPECT_EQ(std::distance(constHop.crbegin(), constHop.crend()), 2);
159}
160
161TEST_F(LivecacheTest, on_write_reports_entries_and_expiration)
162{
163 cache_.insert(Endpoint{endpoint(1), 1});
164 cache_.insert(Endpoint{endpoint(2), tuning::kMaxHops + 1});
165
166 JsonPropertyStream stream;
167 {
168 beast::PropertyStream::Map map(stream);
169 cache_.onWrite(map);
170 }
171
172 auto const& top = stream.top();
173 EXPECT_EQ(top["size"].asUInt(), 2u);
174 EXPECT_FALSE(top["hist"].asString().empty());
175 ASSERT_TRUE(top.isMember("entries"));
176 ASSERT_EQ(top["entries"].size(), 2u);
177 auto const& entry = top["entries"][json::UInt{0}];
178 EXPECT_TRUE(entry.isMember("hops"));
179 EXPECT_TRUE(entry.isMember("address"));
180 EXPECT_TRUE(entry.isMember("expires"));
181}
182
183TEST_F(LivecacheTest, expire_removes_entries_after_ttl)
184{
185 using namespace std::chrono_literals;
186
187 cache_.insert(Endpoint{endpoint(1), 1});
188 ASSERT_EQ(cache_.size(), 1u);
189
190 cache_.expire();
191 EXPECT_EQ(cache_.size(), 1u);
192
193 clock_.advance(tuning::kLiveCacheSecondsToLive - 1s);
194 cache_.expire();
195 EXPECT_EQ(cache_.size(), 1u);
196
197 clock_.advance(1s);
198 cache_.expire();
199 EXPECT_TRUE(cache_.empty());
200}
201
202TEST_F(LivecacheTest, expire_removes_multiple_entries_after_ttl)
203{
204 using namespace std::chrono_literals;
205
206 cache_.insert(Endpoint{endpoint(1), 1});
207 cache_.insert(Endpoint{endpoint(2), 2});
208
209 clock_.advance(tuning::kLiveCacheSecondsToLive);
210 cache_.expire();
211 EXPECT_TRUE(cache_.empty());
212}
213
214TEST_F(LivecacheTest, histogram_counts_all_entries)
215{
216 constexpr auto kNumEndpoints = 40;
217
218 for (auto i = 0; i < kNumEndpoints; ++i)
219 {
220 addEndpoint(endpoint(static_cast<std::uint16_t>(i)), xrpl::randInt<std::uint32_t>());
221 }
222
223 auto const histogram = cache_.hops.histogram();
224 ASSERT_FALSE(histogram.empty());
225
227 boost::split(values, histogram, boost::algorithm::is_any_of(","));
228
229 auto sum = 0;
230 for (auto const& value : values)
231 {
232 auto const count = boost::lexical_cast<int>(boost::trim_copy(value));
233 sum += count;
234 EXPECT_GE(count, 0);
235 }
236 EXPECT_EQ(sum, kNumEndpoints);
237}
238
239TEST_F(LivecacheTest, shuffle_preserves_bucket_contents)
240{
241 for (auto i = 0; i < 100; ++i)
242 {
243 addEndpoint(endpoint(static_cast<std::uint16_t>(i)), xrpl::randInt(tuning::kMaxHops + 1));
244 }
245
246 using AtHop = std::vector<Endpoint>;
248
249 auto const compareEndpoint = [](Endpoint const& lhs, Endpoint const& rhs) {
250 return rhs.hops < lhs.hops || (rhs.hops == lhs.hops && rhs.address < lhs.address);
251 };
252 auto const sameEndpoint = [](Endpoint const& lhs, Endpoint const& rhs) {
253 return lhs.hops == rhs.hops && lhs.address == rhs.address;
254 };
255 auto const sameEndpoints =
256 [&sameEndpoint](std::vector<Endpoint> const& lhs, std::vector<Endpoint> const& rhs) {
257 return lhs.size() == rhs.size() &&
258 std::equal(lhs.begin(), lhs.end(), rhs.begin(), sameEndpoint);
259 };
260
261 AllHops before;
262 AllHops beforeSorted;
263 for (auto i = std::make_pair(0, cache_.hops.begin()); i.second != cache_.hops.end();
264 ++i.first, ++i.second)
265 {
266 std::ranges::copy(*i.second, std::back_inserter(before[i.first]));
267 std::ranges::copy(*i.second, std::back_inserter(beforeSorted[i.first]));
268 std::ranges::sort(beforeSorted[i.first], compareEndpoint);
269 }
270
271 cache_.hops.shuffle();
272
273 AllHops after;
274 AllHops afterSorted;
275 for (auto i = std::make_pair(0, cache_.hops.begin()); i.second != cache_.hops.end();
276 ++i.first, ++i.second)
277 {
278 std::ranges::copy(*i.second, std::back_inserter(after[i.first]));
279 std::ranges::copy(*i.second, std::back_inserter(afterSorted[i.first]));
280 std::ranges::sort(afterSorted[i.first], compareEndpoint);
281 }
282
283 auto allBucketsKeptOriginalOrder = true;
284 for (auto i = 0u; i < before.size(); ++i)
285 {
286 EXPECT_EQ(before[i].size(), after[i].size());
287 allBucketsKeptOriginalOrder =
288 allBucketsKeptOriginalOrder && sameEndpoints(before[i], after[i]);
289 EXPECT_TRUE(sameEndpoints(beforeSorted[i], afterSorted[i]));
290 }
291 EXPECT_FALSE(allBucketsKeptOriginalOrder);
292}
293
294} // namespace xrpl::peer_finder
T back_inserter(T... args)
Address const & address() const
Returns the address portion of this endpoint.
Definition IPEndpoint.h:74
bool isMember(char const *key) const
Return true if the object has a member named key.
A PropertyStream::Sink which produces a json::Value of type ValueType::Object.
static TestSink & instance()
Definition TestSink.h:12
T copy(T... args)
T distance(T... args)
T equal(T... args)
T make_pair(T... args)
boost::asio::ip::address Address
Definition IPAddress.h:20
boost::asio::ip::address_v6 AddressV6
Definition IPAddressV6.h:7
boost::asio::ip::address_v4 AddressV4
Definition IPAddressV4.h:7
unsigned int UInt
constexpr std::chrono::seconds kLiveCacheSecondsToLive(30)
TEST_F(LivecacheTest, basic_insert)
Definition Livecache.cpp:91
static auto sum(TCollection const &col)
Integral randInt(Engine &engine, Integral min, Integral max)
Return a uniformly distributed random integer.
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:572
beast::ManualClock< std::chrono::steady_clock > TestStopwatch
A manual Stopwatch for unit tests.
Definition chrono.h:95
T sort(T... args)
Describes a connectable peer address along with some metadata.
beast::ip::Endpoint address