xrpld
Loading...
Searching...
No Matches
resource/detail/Logic.h
1#pragma once
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/UnorderedContainers.h>
5#include <xrpl/basics/chrono.h>
6#include <xrpl/beast/clock/abstract_clock.h>
7#include <xrpl/beast/core/List.h>
8#include <xrpl/beast/insight/Collector.h>
9#include <xrpl/beast/net/IPEndpoint.h>
10#include <xrpl/beast/utility/Journal.h>
11#include <xrpl/beast/utility/PropertyStream.h>
12#include <xrpl/beast/utility/instrumentation.h>
13#include <xrpl/json/json_value.h>
14#include <xrpl/protocol/jss.h>
15#include <xrpl/resource/Charge.h>
16#include <xrpl/resource/Consumer.h>
17#include <xrpl/resource/Disposition.h>
18#include <xrpl/resource/Fees.h>
19#include <xrpl/resource/Gossip.h>
20#include <xrpl/resource/detail/Import.h>
21
22#include <mutex>
23#include <string>
24#include <tuple>
25#include <utility>
26
27namespace xrpl::resource {
28
29class Logic
30{
31private:
36
37 struct Stats
38 {
40 {
41 warn = collector->makeMeter("warn");
42 drop = collector->makeMeter("drop");
43 }
44
47 };
48
52
54
55 // Table of all entries
57
58 // Because the following are intrusive lists, a given Entry may be in
59 // at most list at a given instant. The Entry must be removed from
60 // one list before placing it in another.
61
62 // List of all active inbound entries
64
65 // List of all active outbound entries
67
68 // List of all active admin entries
70
71 // List of all inactive entries
73
74 // All imported gossip data
76
77 //--------------------------------------------------------------------------
78public:
80 beast::insight::Collector::ptr const& collector,
81 clock_type& clock,
82 beast::Journal journal)
83 : stats_(collector), clock_(clock), journal_(journal)
84 {
85 }
86
88 {
89 // These have to be cleared before the Logic is destroyed
90 // since their destructors call back into the class.
91 // Order matters here as well, the import table has to be
92 // destroyed before the consumer table.
93 //
94 importTable_.clear();
95 table_.clear();
96 }
97
100 {
101 Entry* entry(nullptr);
102
103 {
104 std::scoped_lock const _(lock_);
105 auto [resultIt, resultInserted] = table_.emplace(
107 std::make_tuple(Kind::Inbound, address.atPort(0)), // Key
108 std::make_tuple(clock_.now())); // Entry
109
110 entry = &resultIt->second;
111 entry->key = &resultIt->first;
112 ++entry->refcount;
113 if (entry->refcount == 1)
114 {
115 if (!resultInserted)
116 {
117 inactive_.erase(inactive_.iteratorTo(*entry));
118 }
119 inbound_.pushBack(*entry);
120 }
121 }
122
123 JLOG(journal_.debug()) << "New inbound endpoint " << *entry;
124
125 return Consumer(*this, *entry);
126 }
127
130 {
131 Entry* entry(nullptr);
132
133 {
134 std::scoped_lock const _(lock_);
135 auto [resultIt, resultInserted] = table_.emplace(
137 std::make_tuple(Kind::Outbound, address), // Key
138 std::make_tuple(clock_.now())); // Entry
139
140 entry = &resultIt->second;
141 entry->key = &resultIt->first;
142 ++entry->refcount;
143 if (entry->refcount == 1)
144 {
145 if (!resultInserted)
146 inactive_.erase(inactive_.iteratorTo(*entry));
147 outbound_.pushBack(*entry);
148 }
149 }
150
151 JLOG(journal_.debug()) << "New outbound endpoint " << *entry;
152
153 return Consumer(*this, *entry);
154 }
155
163 {
164 Entry* entry(nullptr);
165
166 {
167 std::scoped_lock const _(lock_);
168 auto [resultIt, resultInserted] = table_.emplace(
170 std::make_tuple(Kind::Unlimited, address.atPort(1)), // Key
171 std::make_tuple(clock_.now())); // Entry
172
173 entry = &resultIt->second;
174 entry->key = &resultIt->first;
175 ++entry->refcount;
176 if (entry->refcount == 1)
177 {
178 if (!resultInserted)
179 inactive_.erase(inactive_.iteratorTo(*entry));
180 admin_.pushBack(*entry);
181 }
182 }
183
184 JLOG(journal_.debug()) << "New unlimited endpoint " << *entry;
185
186 return Consumer(*this, *entry);
187 }
188
191 {
193 }
194
199 getJson(int threshold)
200 {
201 clock_type::time_point const now(clock_.now());
202
204 std::scoped_lock const _(lock_);
205
206 for (auto& inboundEntry : inbound_)
207 {
208 int const localBalance = inboundEntry.localBalance.value(now);
209 if ((localBalance + inboundEntry.remoteBalance) >= threshold)
210 {
211 json::Value& entry = (ret[inboundEntry.toString()] = json::ValueType::Object);
212 entry[jss::local] = localBalance;
213 entry[jss::remote] = inboundEntry.remoteBalance;
214 entry[jss::type] = "inbound";
215 }
216 }
217 for (auto& outboundEntry : outbound_)
218 {
219 int const localBalance = outboundEntry.localBalance.value(now);
220 if ((localBalance + outboundEntry.remoteBalance) >= threshold)
221 {
222 json::Value& entry = (ret[outboundEntry.toString()] = json::ValueType::Object);
223 entry[jss::local] = localBalance;
224 entry[jss::remote] = outboundEntry.remoteBalance;
225 entry[jss::type] = "outbound";
226 }
227 }
228 for (auto& adminEntry : admin_)
229 {
230 int const localBalance = adminEntry.localBalance.value(now);
231 if ((localBalance + adminEntry.remoteBalance) >= threshold)
232 {
233 json::Value& entry = (ret[adminEntry.toString()] = json::ValueType::Object);
234 entry[jss::local] = localBalance;
235 entry[jss::remote] = adminEntry.remoteBalance;
236 entry[jss::type] = "admin";
237 }
238 }
239
240 return ret;
241 }
242
243 Gossip
245 {
246 clock_type::time_point const now(clock_.now());
247
248 Gossip gossip;
249 std::scoped_lock const _(lock_);
250
251 gossip.items.reserve(inbound_.size());
252
253 for (auto& inboundEntry : inbound_)
254 {
255 Gossip::Item item;
256 item.balance = inboundEntry.localBalance.value(now);
257 if (item.balance >= kMinimumGossipBalance)
258 {
259 item.address = inboundEntry.key->address;
260 gossip.items.push_back(item);
261 }
262 }
263
264 return gossip;
265 }
266
267 //--------------------------------------------------------------------------
268
269 void
270 importConsumers(std::string const& origin, Gossip const& gossip)
271 {
272 auto const elapsed = clock_.now();
273 {
274 std::scoped_lock const _(lock_);
275 auto [resultIt, resultInserted] = importTable_.emplace(
277 std::make_tuple(origin), // Key
278 std::make_tuple(clock_.now().time_since_epoch().count())); // Import
279
280 if (resultInserted)
281 {
282 // This is a new import
283 Import& next(resultIt->second);
284 next.whenExpires = elapsed + kGossipExpirationSeconds;
285 next.items.reserve(gossip.items.size());
286
287 for (auto const& gossipItem : gossip.items)
288 {
289 Import::Item item;
290 item.balance = gossipItem.balance;
291 item.consumer = newInboundEndpoint(gossipItem.address);
292 item.consumer.entry().remoteBalance += item.balance;
293 next.items.push_back(item);
294 }
295 }
296 else
297 {
298 // Previous import exists so add the new remote
299 // balances and then deduct the old remote balances.
300
301 Import next;
302 next.whenExpires = elapsed + kGossipExpirationSeconds;
303 next.items.reserve(gossip.items.size());
304 for (auto const& gossipItem : gossip.items)
305 {
306 Import::Item item;
307 item.balance = gossipItem.balance;
308 item.consumer = newInboundEndpoint(gossipItem.address);
309 item.consumer.entry().remoteBalance += item.balance;
310 next.items.push_back(item);
311 }
312
313 Import& prev(resultIt->second);
314 for (auto& item : prev.items)
315 {
316 item.consumer.entry().remoteBalance -= item.balance;
317 }
318
319 std::swap(next, prev);
320 }
321 }
322 }
323
324 //--------------------------------------------------------------------------
325
326 // Called periodically to expire entries and groom the table.
327 //
328 void
330 {
331 std::scoped_lock const _(lock_);
332
333 auto const elapsed = clock_.now();
334
335 for (auto iter(inactive_.begin()); iter != inactive_.end();)
336 {
337 if (iter->whenExpires <= elapsed)
338 {
339 JLOG(journal_.debug()) << "Expired " << *iter;
340 auto tableIter = table_.find(*iter->key);
341 ++iter;
342 erase(tableIter);
343 }
344 else
345 {
346 break;
347 }
348 }
349
350 auto iter = importTable_.begin();
351 while (iter != importTable_.end())
352 {
353 Import& import(iter->second);
354 if (iter->second.whenExpires <= elapsed)
355 {
356 for (auto& item : import.items)
357 {
358 item.consumer.entry().remoteBalance -= item.balance;
359 }
360
361 iter = importTable_.erase(iter);
362 }
363 else
364 {
365 ++iter;
366 }
367 }
368 }
369
370 //--------------------------------------------------------------------------
371
372 // Returns the disposition based on the balance and thresholds
373 static Disposition
375 {
376 if (balance >= kDropThreshold)
377 return Disposition::Drop;
378
380 return Disposition::Warn;
381
382 return Disposition::Ok;
383 }
384
385 void
386 erase(Table::iterator iter)
387 {
388 std::scoped_lock const _(lock_);
389 Entry& entry(iter->second);
390 XRPL_ASSERT(entry.refcount == 0, "xrpl::resource::Logic::erase : entry not used");
391 inactive_.erase(inactive_.iteratorTo(entry));
392 table_.erase(iter);
393 }
394
395 void
397 {
398 std::scoped_lock const _(lock_);
399 ++entry.refcount;
400 }
401
402 void
404 {
405 std::scoped_lock const _(lock_);
406 if (--entry.refcount == 0)
407 {
408 JLOG(journal_.debug()) << "Inactive " << entry;
409
410 switch (entry.key->kind)
411 {
412 case Kind::Inbound:
413 inbound_.erase(inbound_.iteratorTo(entry));
414 break;
415 case Kind::Outbound:
416 outbound_.erase(outbound_.iteratorTo(entry));
417 break;
418 case Kind::Unlimited:
419 admin_.erase(admin_.iteratorTo(entry));
420 break;
421 default:
422 // LCOV_EXCL_START
423 UNREACHABLE(
424 "xrpl::resource::Logic::release : invalid entry "
425 "kind");
426 break;
427 // LCOV_EXCL_STOP
428 }
429 inactive_.pushBack(entry);
430 entry.whenExpires = clock_.now() + kSecondsUntilExpiration;
431 }
432 }
433
435 charge(Entry& entry, Charge const& fee, std::string context = {})
436 {
437 static constexpr Charge::value_type kFeeLogAsWarn = 3000;
438 static constexpr Charge::value_type kFeeLogAsInfo = 1000;
439 static constexpr Charge::value_type kFeeLogAsDebug = 100;
440 static_assert(
441 kFeeLogAsWarn > kFeeLogAsInfo && kFeeLogAsInfo > kFeeLogAsDebug && kFeeLogAsDebug > 10);
442
443 static auto kGetStream = [](resource::Charge::value_type cost, beast::Journal& journal) {
444 if (cost >= kFeeLogAsWarn)
445 return journal.warn();
446 if (cost >= kFeeLogAsInfo)
447 return journal.info();
448 if (cost >= kFeeLogAsDebug)
449 return journal.debug();
450 return journal.trace();
451 };
452
453 if (!context.empty())
454 context = " (" + context + ")";
455
456 std::scoped_lock const _(lock_);
457 clock_type::time_point const now(clock_.now());
458 int const balance(entry.add(fee.cost(), now));
459 JLOG(kGetStream(fee.cost(), journal_)) << "Charging " << entry << " for " << fee << context;
460 return disposition(balance);
461 }
462
463 bool
464 warn(Entry& entry)
465 {
466 if (entry.isUnlimited())
467 return false;
468
469 std::scoped_lock const _(lock_);
470 bool notify(false);
471 auto const elapsed = clock_.now();
472 if (entry.balance(clock_.now()) >= kWarningThreshold && elapsed != entry.lastWarningTime)
473 {
474 charge(entry, kFeeWarning);
475 notify = true;
476 entry.lastWarningTime = elapsed;
477 }
478 if (notify)
479 {
480 JLOG(journal_.info()) << "Load warning: " << entry;
481 ++stats_.warn;
482 }
483 return notify;
484 }
485
486 bool
488 {
489 if (entry.isUnlimited())
490 return false;
491
492 std::scoped_lock const _(lock_);
493 bool drop(false);
494 clock_type::time_point const now(clock_.now());
495 int const balance(entry.balance(now));
496 if (balance >= kDropThreshold)
497 {
498 JLOG(journal_.warn()) << "Consumer entry " << entry << " dropped with balance "
499 << balance << " at or above drop threshold " << kDropThreshold;
500
501 // Adding feeDrop at this point keeps the dropped connection
502 // from re-connecting for at least a little while after it is
503 // dropped.
504 charge(entry, kFeeDrop);
505 ++stats_.drop;
506 drop = true;
507 }
508 return drop;
509 }
510
511 int
513 {
514 std::scoped_lock const _(lock_);
515 return entry.balance(clock_.now());
516 }
517
518 //--------------------------------------------------------------------------
519
520 static void
522 clock_type::time_point const now,
524 EntryIntrusiveList& list)
525 {
526 for (auto& entry : list)
527 {
528 beast::PropertyStream::Map item(items);
529 if (entry.refcount != 0)
530 item["count"] = entry.refcount;
531 item["name"] = entry.toString();
532 item["balance"] = entry.balance(now);
533 if (entry.remoteBalance != 0)
534 item["remote_balance"] = entry.remoteBalance;
535 }
536 }
537
538 void
540 {
541 clock_type::time_point const now(clock_.now());
542
543 std::scoped_lock const _(lock_);
544
545 {
546 beast::PropertyStream::Set s("inbound", map);
547 writeList(now, s, inbound_);
548 }
549
550 {
551 beast::PropertyStream::Set s("outbound", map);
552 writeList(now, s, outbound_);
553 }
554
555 {
556 beast::PropertyStream::Set s("admin", map);
557 writeList(now, s, admin_);
558 }
559
560 {
561 beast::PropertyStream::Set s("inactive", map);
562 writeList(now, s, inactive_);
563 }
564 }
565};
566
567} // namespace xrpl::resource
std::chrono::steady_clock::time_point time_point
virtual time_point now() const =0
Returns the current time.
A generic endpoint for log messages.
Definition Journal.h:44
Intrusive doubly linked list.
Definition List.h:258
std::shared_ptr< Collector > ptr
Definition Collector.h:29
A metric for measuring an integral value.
Definition Meter.h:19
A version-independent IP address and port combination.
Definition IPEndpoint.h:24
Endpoint atPort(Port port) const
Returns a new Endpoint with a different port.
Definition IPEndpoint.h:65
Address const & address() const
Returns the address portion of this endpoint.
Definition IPEndpoint.h:74
Represents a JSON value.
Definition json_value.h:117
A consumption charge.
Definition Charge.h:13
int value_type
The type used to hold a consumption charge.
Definition Charge.h:18
value_type cost() const
Return the cost of the charge in resource::Manager units.
Definition Charge.cpp:22
An endpoint that consumes resources.
Definition Consumer.h:20
Logic(beast::insight::Collector::ptr const &collector, clock_type &clock, beast::Journal journal)
bool disconnect(Entry &entry)
Disposition charge(Entry &entry, Charge const &fee, std::string context={})
json::Value getJson(int threshold)
Returns a json::ValueType::Object.
void erase(Table::iterator iter)
Consumer newInboundEndpoint(beast::ip::Endpoint const &address)
void onWrite(beast::PropertyStream::Map &map)
static Disposition disposition(int balance)
Consumer newOutboundEndpoint(beast::ip::Endpoint const &address)
EntryIntrusiveList inactive_
hash_map< std::string, Import > Imports
hash_map< Key, Entry, Key::Hasher, Key::KeyEqual > Table
std::recursive_mutex lock_
EntryIntrusiveList inbound_
static void writeList(clock_type::time_point const now, beast::PropertyStream::Set &items, EntryIntrusiveList &list)
Consumer newUnlimitedEndpoint(beast::ip::Endpoint const &address)
Create endpoint that should not have resource limits applied.
beast::List< Entry > EntryIntrusiveList
void importConsumers(std::string const &origin, Gossip const &gossip)
EntryIntrusiveList outbound_
T make_tuple(T... args)
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
static constexpr auto kMinimumGossipBalance
static constexpr std::chrono::seconds kSecondsUntilExpiration
static constexpr auto kWarningThreshold
Tunable constants.
Disposition
The disposition of a consumer after applying a load charge.
Definition Disposition.h:8
@ Warn
Consumer should be disconnected for excess consumption.
Definition Disposition.h:18
@ Ok
No action required.
Definition Disposition.h:12
Charge const kFeeDrop
Charge const kFeeWarning
static constexpr auto kDropThreshold
static constexpr std::chrono::seconds kGossipExpirationSeconds
beast::AbstractClock< std::chrono::steady_clock > Stopwatch
A clock for measuring elapsed time.
Definition chrono.h:90
std::unordered_map< Key, Value, Hash, Pred, Allocator > hash_map
T piecewise_construct
Describes a single consumer.
Definition Gossip.h:20
beast::ip::Endpoint address
Definition Gossip.h:24
Data format for exchanging consumption information across peers.
Definition Gossip.h:13
std::vector< Item > items
Definition Gossip.h:27
A set of imported consumer data from a gossip origin.
Definition Import.h:14
Stats(beast::insight::Collector::ptr const &collector)
T swap(T... args)