xrpld
Loading...
Searching...
No Matches
SHAMapStore_test.cpp
1#include <test/jtx/Account.h>
2#include <test/jtx/Env.h>
3#include <test/jtx/amount.h>
4#include <test/jtx/envconfig.h>
5#include <test/jtx/noop.h>
6
7#include <xrpld/app/ledger/LedgerMaster.h>
8#include <xrpld/app/main/Application.h>
9#include <xrpld/app/main/NodeStoreScheduler.h>
10#include <xrpld/app/misc/SHAMapStore.h>
11#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
12#include <xrpld/core/Config.h>
13
14#include <xrpl/basics/ByteUtilities.h>
15#include <xrpl/basics/Log.h>
16#include <xrpl/basics/base_uint.h>
17#include <xrpl/beast/unit_test/suite.h>
18#include <xrpl/beast/utility/Journal.h>
19#include <xrpl/config/BasicConfig.h>
20#include <xrpl/config/Constants.h>
21#include <xrpl/core/JobQueue.h>
22#include <xrpl/json/json_value.h>
23#include <xrpl/nodestore/Backend.h>
24#include <xrpl/nodestore/Manager.h>
25#include <xrpl/nodestore/detail/DatabaseRotatingImp.h>
26#include <xrpl/protocol/ErrorCodes.h>
27#include <xrpl/protocol/LedgerHeader.h>
28#include <xrpl/protocol/Protocol.h>
29#include <xrpl/protocol/XRPAmount.h>
30#include <xrpl/protocol/jss.h>
31#include <xrpl/server/NetworkOPs.h>
32
33#include <algorithm>
34#include <atomic>
35#include <chrono>
36#include <condition_variable>
37#include <cstddef>
38#include <cstdint>
39#include <exception>
40#include <filesystem>
41#include <functional>
42#include <limits>
43#include <map>
44#include <memory>
45#include <mutex>
46#include <optional>
47#include <ostream>
48#include <sstream>
49#include <string>
50#include <thread>
51#include <utility>
52#include <vector>
53
54namespace xrpl::test {
55
57{
58 static constexpr int kDeleteInterval = 8;
59
60 // Mirrors SHAMapStoreImp::kMinimumDeletionIntervalSa, the floor that
61 // online_delete is held to in standalone mode. Note that the
62 // max_waiting_ledgers floor is derived from this minimum rather than from
63 // the configured interval -- SHAMapStoreImp.cpp computes it as
64 // minInterval / 4 -- so both constants below stay put if kDeleteInterval is
65 // ever raised.
66 static constexpr int kMinDeleteInterval = 8;
67 static constexpr int kMinWaitingLedgers = kMinDeleteInterval / 4;
68 static_assert(kDeleteInterval >= kMinDeleteInterval);
69
70 // The two wait durations healthWait() can choose from, spelled as they
71 // appear in its log message. onlineDelete() below sets
72 // recovery_wait_seconds to 1, so the full wait is 1000ms and the shortened
73 // wait is a tenth of that. Note that "Waiting 1000ms" does not contain
74 // "Waiting 100ms", so the two are distinguishable by substring.
75 static constexpr char const* kFullWait = "Waiting 1000ms for node to stabilize";
76 static constexpr char const* kShortWait = "Waiting 100ms for node to stabilize";
77
78 // Distinctive fragments of the other messages the tests below key off. Each
79 // is unique among everything SHAMapStoreImp logs, so a substring match
80 // identifies the message unambiguously.
81 //
82 // kRotating is logged once run() has committed to a rotation, immediately
83 // after the health check that gates it, and kFinished once a rotation has
84 // run to completion. kExpired is logged by healthWait() when the circuit
85 // breaker trips.
86 static constexpr char const* kRotating = "rotating";
87 static constexpr char const* kFinished = "finished rotation";
88 static constexpr char const* kExpired = "unable to make progress";
89
90 // A Logs implementation that records every message the store's own
91 // partition emits, keeping each message's severity alongside its text, and
92 // lets a test block until a given message has appeared.
93 //
94 // Severity is recorded because healthWait() picks the severity and the wait
95 // duration together, so the pair identifies which of its three logging
96 // branches ran: warn at the full wait when the server is unhealthy for a
97 // reason that is not expected to resolve on its own, trace at a tenth of
98 // the wait when the only missing ledger is the one currently being built,
99 // and info at the full wait otherwise. Matching on the pair is what lets
100 // the tests below assert which branch was taken instead of merely that some
101 // wait happened.
102 //
103 // waitFor() exists because run() logs on entry to a rotation, which is the
104 // only signal a test has that the store has passed the health check gating
105 // the rotation and is now inside it. Several of the branches under test are
106 // only reachable from there, and no other handle on the store exposes it.
107 class StoreLogs : public Logs
108 {
112
114 {
116
117 public:
119 : beast::Journal::Sink(threshold, false), owner_(owner)
120 {
121 }
122
123 // Env::AppBundle calls Logs::threshold() after the Application is
124 // built, which would otherwise raise this sink above Trace and
125 // discard the messages the buildingIndex branch logs.
126 void
128 {
129 }
130
131 void
132 write(beast::Severity level, std::string const& text) override
133 {
134 {
135 std::scoped_lock const lock(owner_.mutex_);
136 owner_.messages_.emplace_back(level, text);
137 }
138 owner_.cond_.notify_all();
139 }
140
141 void
142 writeAlways(beast::Severity level, std::string const& text) override
143 {
144 write(level, text);
145 }
146 };
147
148 // Caller must hold mutex_. A nullopt severity matches any severity.
149 [[nodiscard]] std::size_t
151 {
152 return std::count_if(messages_.begin(), messages_.end(), [&](auto const& message) {
153 return (!severity || message.first == *severity) &&
154 message.second.find(text) != std::string::npos;
155 });
156 }
157
158 public:
160 {
161 }
162
163 // Only the store's own partition is logged at Trace; everything else
164 // is silenced, so that enabling trace for this one branch does not pay
165 // for formatting every trace message in the server.
167 makeSink(std::string const& partition, beast::Severity) override
168 {
170 partition == "SHAMapStore" ? beast::Severity::Trace : beast::Severity::Disabled,
171 *this);
172 }
173
174 // How many recorded messages were logged at `severity` and contain
175 // `text`.
176 [[nodiscard]] std::size_t
177 count(beast::Severity severity, std::string const& text) const
178 {
179 std::scoped_lock const lock(mutex_);
180 return countLocked(severity, text);
181 }
182
183 // How many recorded messages contain `text`, at any severity.
184 [[nodiscard]] std::size_t
185 count(std::string const& text) const
186 {
187 std::scoped_lock const lock(mutex_);
188 return countLocked(std::nullopt, text);
189 }
190
191 // Blocks until `text` has been logged at least `expected` times at
192 // `severity` -- or at any severity, if that is nullopt -- or until the
193 // timeout expires. Returns whether it got there.
194 //
195 // Waiting rather than sleeping-then-counting matters for the branches
196 // that are only reached after a rotation has started: the store gets
197 // there when it gets there, so a fixed sleep has to be sized for the
198 // slowest plausible machine, whereas this returns as soon as the
199 // message appears.
200 [[nodiscard]] bool
203 std::string const& text,
205 std::size_t expected = 1)
206 {
208 return cond_.wait_for(
209 lock, timeout, [&] { return countLocked(severity, text) >= expected; });
210 }
211
212 // As above, at any severity.
213 [[nodiscard]] bool
215 std::string const& text,
217 std::size_t expected = 1)
218 {
219 return waitFor(std::nullopt, text, timeout, expected);
220 }
221 };
222
223 static auto
225 {
226 cfg = jtx::onlineDelete(std::move(cfg), kDeleteInterval);
228 return cfg;
229 }
230
231 // online delete tuned so that a rotation, once it has started, spends a
232 // long time in clearPrior() before reaching the first health check inside
233 // the rotation body.
234 //
235 // clearSql() sleeps back_off_milliseconds at the top of every iteration and
236 // advances by delete_batch rows per iteration, so a delete_batch of 1 costs
237 // one sleep per ledger removed, for each of the three tables it is called
238 // on. That is what gives parkMidRotation() below a window measured in
239 // seconds rather than in microseconds. delete_batch is therefore the actual
240 // lever; back_off_milliseconds is set to the value SHAMapStoreImp already
241 // defaults to, and is spelled out only so the arithmetic above can be
242 // checked against the config rather than against the implementation.
243 //
244 // max_waiting_ledgers is pinned to its floor so that tripping the circuit
245 // breaker takes the fewest possible ledger closes.
246 static auto
248 {
249 cfg = onlineDelete(std::move(cfg));
250 auto& section = cfg->section(Sections::kNodeDatabase);
251 section.set(Keys::kDeleteBatch, "1");
252 section.set(Keys::kBackOffMilliseconds, "100");
254 return cfg;
255 }
256
257 static auto
259 {
260 cfg = onlineDelete(std::move(cfg));
261 cfg->section(Sections::kNodeDatabase).set(Keys::kAdvisoryDelete, "1");
262 return cfg;
263 }
264
265 static bool
266 goodLedger(jtx::Env& env, json::Value const& json, std::string ledgerID, bool checkDB = false)
267 {
268 auto good = json.isMember(jss::result) && !rpc::containsError(json[jss::result]) &&
269 json[jss::result][jss::ledger][jss::ledger_index] == ledgerID;
270 if (!good || !checkDB)
271 return good;
272
273 auto const seq = json[jss::result][jss::ledger_index].asUInt();
274
277 if (!outInfo)
278 return false;
279 LedgerHeader const& info = outInfo.value();
280
281 std::string const outHash = to_string(info.hash);
282 LedgerIndex const outSeq = info.seq;
283 std::string const outParentHash = to_string(info.parentHash);
284 std::string const outDrops = to_string(info.drops);
285 std::uint64_t const outCloseTime = info.closeTime.time_since_epoch().count();
286 std::uint64_t const outParentCloseTime = info.parentCloseTime.time_since_epoch().count();
287 std::uint64_t const outCloseTimeResolution = info.closeTimeResolution.count();
288 std::uint64_t const outCloseFlags = info.closeFlags;
289 std::string const outAccountHash = to_string(info.accountHash);
290 std::string const outTxHash = to_string(info.txHash);
291
292 auto const& ledger = json[jss::result][jss::ledger];
293 return outHash == ledger[jss::ledger_hash].asString() && outSeq == seq &&
294 outParentHash == ledger[jss::parent_hash].asString() &&
295 outDrops == ledger[jss::total_coins].asString() &&
296 outCloseTime == ledger[jss::close_time].asUInt() &&
297 outParentCloseTime == ledger[jss::parent_close_time].asUInt() &&
298 outCloseTimeResolution == ledger[jss::close_time_resolution].asUInt() &&
299 outCloseFlags == ledger[jss::close_flags].asUInt() &&
300 outAccountHash == ledger[jss::account_hash].asString() &&
301 outTxHash == ledger[jss::transaction_hash].asString();
302 }
303
304 static bool
306 {
307 return json.isMember(jss::result) && rpc::containsError(json[jss::result]) &&
308 json[jss::result][jss::error_code] == error;
309 }
310
313 {
314 BEAST_EXPECT(
315 json.isMember(jss::result) && json[jss::result].isMember(jss::ledger) &&
316 json[jss::result][jss::ledger].isMember(jss::ledger_hash) &&
317 json[jss::result][jss::ledger][jss::ledger_hash].isString());
318 return json[jss::result][jss::ledger][jss::ledger_hash].asString();
319 }
320
321 void
322 ledgerCheck(jtx::Env& env, int const rows, int const first)
323 {
324 auto const [actualRows, actualFirst, actualLast] =
326
327 BEAST_EXPECT(actualRows == rows);
328 BEAST_EXPECT(actualFirst == first);
329 BEAST_EXPECT(actualLast == first + rows - 1);
330 }
331
332 void
333 transactionCheck(jtx::Env& env, int const rows)
334 {
335 BEAST_EXPECT(env.app().getRelationalDatabase().getTransactionCount() == rows);
336 }
337
338 void
339 accountTransactionCheck(jtx::Env& env, int const rows)
340 {
341 BEAST_EXPECT(env.app().getRelationalDatabase().getAccountTransactionCount() == rows);
342 }
343
344 // Wait until the SHAMapStore has finished processing the ledger that the
345 // preceding env.close() produced.
346 //
347 // env.close() returns as soon as the ledger_accept RPC returns, but the
348 // validated ledger path -- LedgerMaster::setValidLedger() ->
349 // SHAMapStore::onLedgerClosed() -- runs on a job queue thread. Without
350 // draining the job queue first, the store may not have been handed the
351 // ledger at all, in which case rendezvous() observes working_ == false and
352 // returns immediately, before any work has been done.
353 [[nodiscard]] static bool
355 {
356 // Drain the job queue first, so that onLedgerClosed() has run and
357 // working_ is set. Then use the store's timeout overload, so a store
358 // that never finishes fails this test instead of blocking on it.
359 //
360 // Only the second wait is bounded: JobQueue::rendezvous() has no
361 // timeout overload, so a job that never completes hangs here. That is
362 // pre-existing -- ~AppBundle waits on it the same way for every jtx
363 // test -- but it does mean this helper is not hang-proof end to end.
364 env.app().getJobQueue().rendezvous();
366 }
367
368 // Bring the SHAMapStore to the point where it has been handed a validated
369 // ledger and initialized lastRotated, and report how many extra ledgers had
370 // to be closed to get it there (normally none). Returns std::nullopt if
371 // syncStore() itself failed.
372 //
373 // syncStore() alone does not guarantee that, because
374 // SHAMapStoreImp::run()'s loop does not use the notification and the
375 // working_ flag safely:
376 //
377 // * onLedgerClosed() notifies cond_ whether or not run()'s thread is
378 // parked on it, and run() waits on cond_ without a predicate, so a
379 // notification that lands while the thread is still starting up --
380 // before it first reaches that wait -- is lost.
381 // * run() clears working_ at the top of its loop without checking
382 // whether newLedger_ is still set, so rendezvous() can report the
383 // store idle with a validated ledger queued.
384 //
385 // Either way the store ends up parked with work pending, and only another
386 // notification gets it moving again. In a standalone test nothing else
387 // closes ledgers, so that has to come from here: this closes a ledger
388 // rather than polling getLastRotated(), because polling would just time
389 // out. onLedgerClosed() keeps only the most recent ledger in newLedger_,
390 // so the ledger the store picks up -- and therefore lastRotated -- is a
391 // timing detail, which is why the callers derive their expectations from
392 // the value they observe instead of assuming one.
393 //
394 // run() is deliberately left as it is. In production the only effect is
395 // latency: the trigger is validatedSeq >= lastRotated + deleteInterval, so
396 // a lost notification delays rotation to the next validated ledger and
397 // nothing is skipped or accumulated -- starting at 513 instead of 512 does
398 // not matter. Two consequences do follow from leaving it in place, and both
399 // hold today: nothing in production decides anything from working_ or
400 // rendezvous() (rendezvous() has no production callers at all), and a node
401 // whose ledgers only advance on demand -- standalone, driven by
402 // ledger_accept -- can sit on a queued ledger until something closes the
403 // next one, which is exactly the situation this helper is working around.
404 //
405 // So this helper is permanent rather than a stopgap. Working around the
406 // race must not make it invisible, so every extra close is logged. That
407 // keeps how often it is actually hit observable in the unit test output --
408 // which is the only signal left once these testcases stop flaking on it.
409 [[nodiscard]] std::optional<int>
410 initializeStore(jtx::Env& env, int const maxExtraCloses = 3)
411 {
412 auto& store = env.app().getSHAMapStore();
413
414 for (int extraCloses = 0;; ++extraCloses)
415 {
416 if (!syncStore(env))
417 return std::nullopt;
418 if (store.getLastRotated() != 0 || extraCloses == maxExtraCloses)
419 {
420 if (extraCloses != 0)
421 {
422 log << "initializeStore: the store needed " << extraCloses
423 << " extra ledger close(s) to pick up a validated ledger. "
424 "SHAMapStoreImp::run() dropped the notification for the "
425 "first one; see the comment on initializeStore()."
426 << std::endl;
427 }
428 return extraCloses;
429 }
430 env.close();
431 }
432 }
433
434 int
436 {
437 using namespace std::chrono_literals;
438
439 auto& store = env.app().getSHAMapStore();
440
441 int ledgerSeq = 3;
442 BEAST_EXPECT(syncStore(env));
443 BEAST_EXPECT(!store.getLastRotated());
444
445 env.close();
446 BEAST_EXPECT(syncStore(env));
447
448 auto ledger = env.rpc("ledger", "validated");
449 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++)));
450
451 BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1);
452 return ledgerSeq;
453 }
454
455 // Construct an Env whose config has online_delete enabled and is then
456 // mutated by `tweak`, and report how SHAMapStoreImp's constructor judged
457 // it: the message of the exception it threw, or std::nullopt if the Env was
458 // constructed successfully.
459 //
460 // SHAMapStoreImp is built from ApplicationImp's member initializer list, so
461 // a configuration it rejects surfaces as an exception thrown out of the Env
462 // constructor rather than as a failure at some later point.
463 //
464 // Note that ~AppBundle does not run when the Env constructor throws, so the
465 // global debug log sink it installed -- which holds a reference to this
466 // suite -- would outlive the suite. The catch below clears it, so callers
467 // are free to end on a configuration that is rejected.
470 {
471 using namespace test::jtx;
472
473 try
474 {
475 Env const env{
476 *this,
477 envconfig([&tweak](std::unique_ptr<Config> cfg) {
478 cfg = onlineDelete(std::move(cfg));
479 tweak(*cfg);
480 return cfg;
481 }),
482 nullptr,
484 return std::nullopt;
485 }
486 // Deliberately broader than the std::runtime_error that
487 // SHAMapStoreImp throws: an unexpected exception type then shows up as
488 // a message mismatch naming the actual failure, rather than escaping
489 // this testcase.
490 catch (std::exception const& e)
491 {
492 // ~AppBundle did not run, so drop the sink it installed by hand
493 // rather than leaving a reference to this suite live in a global.
494 setDebugLogSink(nullptr);
495 return std::string{e.what()};
496 }
497 }
498
499 void
500 expectConfigRejected(std::string const& expected, std::function<void(Config&)> const& tweak)
501 {
502 auto const result = storeConfigResult(tweak);
503 BEAST_EXPECTS(result == expected, result.value_or("<accepted>"));
504 }
505
506 void
508 {
509 auto const result = storeConfigResult(tweak);
510 BEAST_EXPECTS(!result, result.value_or(""));
511 }
512
513 // The state parkInHealthWait() leaves behind.
514 struct Parked
515 {
516 // Value of getLastRotated() before the rotation attempt began. The
517 // store must still report this for as long as it stays parked.
519 // The validated ledger the store is waiting on, and the sequence
520 // getLastRotated() will report once the rotation finally completes.
522 // Sequence removed from LedgerMaster to create the gap, or 0 if
523 // `createGap` was false.
525 };
526
527 // Drive the store to the point where it is parked inside healthWait(),
528 // unable to proceed with a rotation: close ledgers until one more close
529 // would make the store due to rotate, optionally remove the newest ledger
530 // from LedgerMaster so that the attempt sees a gap in the range, close the
531 // triggering ledger, and then set the operating mode to `modeAfterClose`.
532 //
533 // Once parked, the store stays parked indefinitely. Its wait loop reruns
534 // every recovery_wait_seconds and exits only when the server looks healthy,
535 // when it is stopped, or when the validated ledger index reaches the
536 // circuit breaker -- and that index only advances when this test closes
537 // another ledger. So a caller can establish any server state it likes,
538 // hold it, and be sure the store observes it. That is what makes the tests
539 // below state machines rather than races.
540 //
541 // Returns std::nullopt if the setup did not reach a parked store, having
542 // already reported the failure.
544 parkInHealthWait(jtx::Env& env, bool createGap, OperatingMode modeAfterClose)
545 {
546 using namespace std::chrono_literals;
547 using namespace test::jtx;
548
549 auto& lm = env.app().getLedgerMaster();
550 auto& store = env.app().getSHAMapStore();
551 auto& netOPs = env.app().getOPs();
552
553 env.fund(XRP(1000), Account("alice"));
554 env.close();
555 if (!BEAST_EXPECT(initializeStore(env).has_value()))
556 return std::nullopt;
557
558 Parked parked;
559 // The store adopts the first validated ledger it sees as lastRotated,
560 // and which one that is depends on timing, so read it rather than
561 // assuming a value.
562 parked.lastRotated = store.getLastRotated();
563 if (!BEAST_EXPECT(parked.lastRotated))
564 return std::nullopt;
565
566 // Close ledgers until the next close is the one that makes
567 // validatedSeq reach lastRotated + deleteInterval.
568 LedgerIndex maxSeq = env.closed()->header().seq;
569 while (maxSeq + 1 < parked.lastRotated + kDeleteInterval)
570 {
571 env.close();
572 ++maxSeq;
573 if (!BEAST_EXPECT(syncStore(env)))
574 return std::nullopt;
575 if (!BEAST_EXPECTS(
576 store.getLastRotated() == parked.lastRotated,
577 std::to_string(store.getLastRotated())))
578 return std::nullopt;
579 }
580
581 // Drop out of FULL before touching LedgerMaster's internals, matching
582 // testLedgerGaps. This also keeps the store from rotating on the
583 // triggering close before the caller has set the state it wants
584 // observed.
585 netOPs.setMode(OperatingMode::CONNECTED);
586
587 // The gap goes one below the sequence that the close further down makes
588 // validated, never at that sequence itself: healthWait() derives
589 // buildingIndex from numMissing == 1 && !haveLedger(index), so a gap at
590 // the validated index reads as "that ledger is about to be built" and
591 // takes the short trace wait, whereas a gap below it reads as a
592 // genuinely incomplete range and takes the full wait. Nothing refills
593 // the gap, so the wait loop ends only when the circuit breaker trips or
594 // stop() intervenes.
595 if (createGap)
596 {
597 std::size_t iterations = 30;
598 while (!lm.haveLedger(maxSeq) && --iterations > 0)
599 {
601 }
602 if (!BEAST_EXPECTS(lm.haveLedger(maxSeq), std::to_string(maxSeq)))
603 return std::nullopt;
604
605 // Give the server a moment to finish any internal work on the
606 // ledger about to be removed, as testLedgerGaps does.
608
609 lm.clearLedger(maxSeq);
610 if (!BEAST_EXPECT(!lm.haveLedger(maxSeq)))
611 return std::nullopt;
612 parked.gap = maxSeq;
613 }
614
615 // This close makes the store due to rotate.
616 env.close();
617 ++maxSeq;
618 parked.validated = maxSeq;
619 netOPs.setMode(modeAfterClose);
620
621 // Drain the job queue so that onLedgerClosed() has handed the ledger to
622 // the store. Without this, working_ may still be false from the
623 // previous cycle and rendezvous() would report "done" before the store
624 // has even looked at this ledger.
625 env.app().getJobQueue().rendezvous();
626
627 if (!BEAST_EXPECT(!store.rendezvous(1s)))
628 return std::nullopt;
629 if (!BEAST_EXPECTS(
630 store.getLastRotated() == parked.lastRotated,
631 std::to_string(store.getLastRotated())))
632 return std::nullopt;
633
634 return parked;
635 }
636
637 // Drive the store to the point where it has passed the health check that
638 // gates a rotation and is inside the rotation body, then make the server
639 // unhealthy so that the next health check in there parks it.
640 //
641 // The gap cannot be created up front the way parkInHealthWait() does it,
642 // because the gating check would see it and refuse to start the rotation at
643 // all -- which is what testLedgerGaps() exercises. So this waits for the
644 // message run() logs immediately after that check, which is the store
645 // publishing that it is committed to the rotation, and creates the gap then.
646 //
647 // The margin that makes that safe is clearPrior(), which runs between the
648 // log and the first health check inside the rotation. Under
649 // slowOnlineDelete() it works through three tables one sequence at a time,
650 // sleeping back_off_milliseconds before each, and checks health after every
651 // one of those sleeps. So the store spends on the order of a second per
652 // table repeatedly asking whether it is healthy, against the microseconds
653 // this function needs to clear a ledger once waitFor() has returned.
654 //
655 // The gap has to be the validated ledger itself. healthWait() counts missing
656 // ledgers over the range from lastGoodValidatedLedger_ to the validated
657 // index, and run() sets the former to the latter just before starting the
658 // rotation, so for the duration of the rotation that range begins as a
659 // single sequence and grows only as the caller closes more ledgers.
660 //
661 // Which health check inside the rotation ends up observing the gap is not
662 // pinned down, and does not need to be: whichever one it is returns the same
663 // answer, clearPrior() gives up, and run() reaches its first switch on
664 // healthWait() with the condition still in force. Every assertion below
665 // holds for any of them.
666 //
667 // Returns std::nullopt if the setup did not reach a parked store, having
668 // already reported the failure.
671 {
672 using namespace std::chrono_literals;
673 using namespace test::jtx;
674
675 auto& lm = env.app().getLedgerMaster();
676 auto& store = env.app().getSHAMapStore();
677
678 auto const alice = Account("alice");
679 env.fund(XRP(1000), alice);
680 env.close();
681 if (!BEAST_EXPECT(initializeStore(env).has_value()))
682 return std::nullopt;
683
684 LedgerIndex maxSeq = env.closed()->header().seq;
685 // Close one ledger, carrying a transaction so that the sequence has rows
686 // in all three of the tables clearSql() works through.
687 auto closeOne = [&]() -> bool {
688 env(noop(alice));
689 env.close();
690 ++maxSeq;
691 return BEAST_EXPECT(syncStore(env));
692 };
693
694 // Let one rotation complete before setting up the one to be parked. The
695 // window this helper depends on only exists once the tables hold a full
696 // delete interval of rows: on the very first rotation there is at most
697 // one sequence to remove, so clearSql() sleeps once or not at all.
698 LedgerIndex const firstRotated = store.getLastRotated();
699 if (!BEAST_EXPECT(firstRotated))
700 return std::nullopt;
701 while (store.getLastRotated() == firstRotated)
702 {
703 if (!closeOne())
704 return std::nullopt;
705 // The rotation is due once maxSeq reaches firstRotated +
706 // kDeleteInterval. Allow one close beyond that before giving up,
707 // rather than closing ledgers forever.
708 if (!BEAST_EXPECTS(maxSeq <= firstRotated + kDeleteInterval, std::to_string(maxSeq)))
709 return std::nullopt;
710 }
711
712 Parked parked;
713 parked.lastRotated = store.getLastRotated();
714
715 // Close ledgers until the next close is the one that makes the store due
716 // to rotate again.
717 while (maxSeq + 1 < parked.lastRotated + kDeleteInterval)
718 {
719 if (!closeOne())
720 return std::nullopt;
721 if (!BEAST_EXPECTS(
722 store.getLastRotated() == parked.lastRotated,
723 std::to_string(store.getLastRotated())))
724 return std::nullopt;
725 }
726
727 // One rotation has already been logged, so wait for the next one rather
728 // than for the first.
729 auto const rotationsBefore = log.count(kRotating);
730
731 // This close makes the store due to rotate. Deliberately do not drain
732 // the store here: the point is to interrupt it partway through.
733 env(noop(alice));
734 env.close();
735 ++maxSeq;
736 parked.validated = maxSeq;
737
738 // Draining the job queue, on the other hand, is required. In standalone
739 // mode switchLCL() inserts the closed ledger into LedgerMaster's
740 // complete range and then posts an advance job, and publishing the
741 // ledger from that job inserts it a second time. Clearing the ledger
742 // between those two inserts does not leave a lasting gap: publication
743 // puts it straight back, the rotation's health checks see a healthy
744 // node, and the rotation runs to completion. Waiting for the queue to
745 // drain closes that window, because publication is what advances
746 // pubLedger_ -- once it has happened, the ledger is never published, and
747 // so never inserted, again.
748 //
749 // This waits only for the job queue, not for the store, whose thread is
750 // its own and is the thing being interrupted here.
751 env.app().getJobQueue().rendezvous();
752
753 // Publishing the ledger is what advances pubLedger_, so this is the
754 // observable confirmation that the window above has closed. Asserting it
755 // here means that if anything ever reopens it, this setup step says so
756 // directly instead of the tests below failing for reasons that look
757 // nothing like the cause.
758 auto const published = lm.getPublishedLedger();
759 if (!BEAST_EXPECTS(
760 published && published->header().seq >= parked.validated,
761 std::to_string(published ? published->header().seq : 0)))
762 return std::nullopt;
763
764 if (!BEAST_EXPECT(log.waitFor(kRotating, 10s, rotationsBefore + 1)))
765 return std::nullopt;
766
767 // The store is now inside clearPrior(). Remove the validated ledger so
768 // that every health check from here on reports a gap.
769 if (!BEAST_EXPECTS(lm.haveLedger(parked.validated), std::to_string(parked.validated)))
770 return std::nullopt;
771 lm.clearLedger(parked.validated);
772 parked.gap = parked.validated;
773 if (!BEAST_EXPECT(!lm.haveLedger(parked.gap)))
774 return std::nullopt;
775
776 // The rotation must now be stuck. Wait longer than
777 // recovery_wait_seconds, so that this is a settled state rather than a
778 // store that has yet to reach its next health check.
779 if (!BEAST_EXPECT(!store.rendezvous(1500ms)))
780 return std::nullopt;
781 if (!BEAST_EXPECTS(
782 store.getLastRotated() == parked.lastRotated,
783 std::to_string(store.getLastRotated())))
784 return std::nullopt;
785 // The rotation started, has not finished, and has not yet given up.
786 if (!BEAST_EXPECTS(
787 log.count(kRotating) == rotationsBefore + 1, std::to_string(log.count(kRotating))))
788 return std::nullopt;
789 if (!BEAST_EXPECTS(log.count(kFinished) == 1, std::to_string(log.count(kFinished))))
790 return std::nullopt;
791 if (!BEAST_EXPECTS(log.count(kExpired) == 0, std::to_string(log.count(kExpired))))
792 return std::nullopt;
793
794 return parked;
795 }
796
797public:
798 // Cover the [node_db] validation that SHAMapStoreImp performs when it is
799 // constructed. The rejected cases stop inside SHAMapStoreImp's constructor,
800 // so they cost only a partial Application construction; the accepted ones
801 // start a full node and immediately tear it down.
802 void
804 {
805 testcase("config validation");
806
807 // online_delete below the standalone minimum. ledger_history is still
808 // kDeleteInterval here, so it is too large for this online_delete as
809 // well; the assertion pins which of the two errors wins.
811 "online_delete must be at least " + std::to_string(kMinDeleteInterval),
812 [](Config& cfg) {
815 });
816
817 // ledger_history above online_delete asks the node to retain more
818 // history than online delete is allowed to keep.
820 "online_delete must not be less than ledger_history (currently " +
822 [](Config& cfg) { cfg.ledgerHistory = kDeleteInterval + 1; });
823
824 // recovery_wait_seconds is the interval at which online delete rechecks
825 // the node's health while it waits for missing ledgers to arrive, so a
826 // zero wait would turn that into a spin.
827 expectConfigRejected("recovery_wait_seconds must be at least 1 second", [](Config& cfg) {
829 });
830
831 // max_waiting_ledgers is the circuit breaker that eventually lets
832 // online delete stop waiting, so it has a floor rather than being
833 // free-form.
834 auto const tooFewWaiting =
835 "max_waiting_ledgers must be at least " + std::to_string(kMinWaitingLedgers);
836 expectConfigRejected(tooFewWaiting, [](Config& cfg) {
839 });
840 // 0 is not a magic "never give up" value, just a value below the floor.
841 expectConfigRejected(tooFewWaiting, [](Config& cfg) {
843 });
844
845 // The floor itself is accepted, and so is a value far above
846 // online_delete: there is no upper bound.
850 });
854 });
855
856 // All of the above is gated on online_delete being enabled. With it
857 // turned off, the same values are ignored rather than rejected.
859 auto& section = cfg.section(Sections::kNodeDatabase);
860 section.set(Keys::kOnlineDelete, "0");
861 section.set(Keys::kMaxWaitingLedgers, "0");
862 section.set(Keys::kRecoveryWaitSeconds, "0");
863 });
864 }
865
866 void
868 {
869 using namespace std::chrono_literals;
870
871 testcase("clearPrior");
872 using namespace jtx;
873
874 Env env(*this, envconfig(onlineDelete));
875
876 auto& store = env.app().getSHAMapStore();
877 env.fund(XRP(10000), noripple("alice"));
878
879 ledgerCheck(env, 1, 2);
880 transactionCheck(env, 0);
882
884
885 auto ledgerTmp = env.rpc("ledger", "0");
886 BEAST_EXPECT(bad(ledgerTmp));
887
888 ledgers.emplace(1, env.rpc("ledger", "1"));
889 BEAST_EXPECT(goodLedger(env, ledgers[1], "1"));
890
891 ledgers.emplace(2, env.rpc("ledger", "2"));
892 BEAST_EXPECT(goodLedger(env, ledgers[2], "2"));
893
894 ledgerTmp = env.rpc("ledger", "current");
895 BEAST_EXPECT(goodLedger(env, ledgerTmp, "3"));
896
897 ledgerTmp = env.rpc("ledger", "4");
898 BEAST_EXPECT(bad(ledgerTmp));
899
900 ledgerTmp = env.rpc("ledger", "100");
901 BEAST_EXPECT(bad(ledgerTmp));
902
903 auto const firstSeq = waitForReady(env);
904 auto lastRotated = firstSeq - 1;
905
906 for (auto i = firstSeq + 1; i < kDeleteInterval + firstSeq; ++i)
907 {
908 env.fund(XRP(10000), noripple("test" + std::to_string(i)));
909 env.close();
910
911 ledgerTmp = env.rpc("ledger", "current");
912 BEAST_EXPECT(goodLedger(env, ledgerTmp, std::to_string(i)));
913 }
914 BEAST_EXPECT(store.getLastRotated() == lastRotated);
915
916 for (auto i = 3; i < kDeleteInterval + lastRotated; ++i)
917 {
918 ledgers.emplace(i, env.rpc("ledger", std::to_string(i)));
919 BEAST_EXPECT(
920 goodLedger(env, ledgers[i], std::to_string(i), true) &&
921 !getHash(ledgers[i]).empty());
922 }
923
924 ledgerCheck(env, kDeleteInterval + 1, 2);
927
928 {
929 // Closing one more ledger triggers a rotate
930 env.close();
931
932 auto ledger = env.rpc("ledger", "current");
933 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(kDeleteInterval + 4)));
934 }
935
936 BEAST_EXPECT(syncStore(env));
937
938 BEAST_EXPECT(store.getLastRotated() == kDeleteInterval + 3);
939 lastRotated = store.getLastRotated();
940 BEAST_EXPECT(lastRotated == 11);
941
942 // That took care of the fake hashes
943 ledgerCheck(env, kDeleteInterval + 1, 3);
946
947 // The last iteration of this loop should trigger a rotate
948 for (auto i = lastRotated - 1; i < lastRotated + kDeleteInterval - 1; ++i)
949 {
950 env.close();
951
952 ledgerTmp = env.rpc("ledger", "current");
953 BEAST_EXPECT(goodLedger(env, ledgerTmp, std::to_string(i + 3)));
954
955 ledgers.emplace(i, env.rpc("ledger", std::to_string(i)));
956 BEAST_EXPECT(
957 store.getLastRotated() == lastRotated || i == lastRotated + kDeleteInterval - 2);
958 BEAST_EXPECT(
959 goodLedger(env, ledgers[i], std::to_string(i), true) &&
960 !getHash(ledgers[i]).empty());
961 }
962
963 BEAST_EXPECT(syncStore(env));
964
965 BEAST_EXPECT(store.getLastRotated() == kDeleteInterval + lastRotated);
966
967 ledgerCheck(env, kDeleteInterval + 1, lastRotated);
968 transactionCheck(env, 0);
970 }
971
972 void
974 {
975 testcase("automatic online_delete");
976 using namespace jtx;
977 using namespace std::chrono_literals;
978
979 Env env(*this, envconfig(onlineDelete));
980 auto& store = env.app().getSHAMapStore();
981
982 auto ledgerSeq = waitForReady(env);
983 auto lastRotated = ledgerSeq - 1;
984 BEAST_EXPECT(store.getLastRotated() == lastRotated);
985 BEAST_EXPECT(lastRotated != 2);
986
987 // Because advisory_delete is unset,
988 // "can_delete" is disabled.
989 auto const canDelete = env.rpc("can_delete");
990 BEAST_EXPECT(bad(canDelete, RpcNotEnabled));
991
992 // Close ledgers without triggering a rotate
993 for (; ledgerSeq < lastRotated + kDeleteInterval; ++ledgerSeq)
994 {
995 env.close();
996
997 auto ledger = env.rpc("ledger", "validated");
998 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
999 }
1000
1001 BEAST_EXPECT(syncStore(env));
1002
1003 // The database will always have back to ledger 2,
1004 // regardless of lastRotated.
1005 ledgerCheck(env, ledgerSeq - 2, 2);
1006 BEAST_EXPECT(lastRotated == store.getLastRotated());
1007
1008 {
1009 // Closing one more ledger triggers a rotate
1010 env.close();
1011
1012 auto ledger = env.rpc("ledger", "validated");
1013 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
1014 }
1015
1016 BEAST_EXPECT(syncStore(env));
1017
1018 ledgerCheck(env, ledgerSeq - lastRotated, lastRotated);
1019 BEAST_EXPECT(lastRotated != store.getLastRotated());
1020
1021 lastRotated = store.getLastRotated();
1022
1023 // Close enough ledgers to trigger another rotate
1024 for (; ledgerSeq < lastRotated + kDeleteInterval + 1; ++ledgerSeq)
1025 {
1026 env.close();
1027
1028 auto ledger = env.rpc("ledger", "validated");
1029 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
1030 }
1031
1032 BEAST_EXPECT(syncStore(env));
1033
1034 ledgerCheck(env, kDeleteInterval + 1, lastRotated);
1035 BEAST_EXPECT(lastRotated != store.getLastRotated());
1036 }
1037
1038 void
1040 {
1041 testcase("online_delete with advisory_delete");
1042 using namespace jtx;
1043 using namespace std::chrono_literals;
1044
1045 // Same config with advisory_delete enabled
1046 Env env(*this, envconfig(advisoryDelete));
1047 auto& store = env.app().getSHAMapStore();
1048
1049 auto ledgerSeq = waitForReady(env);
1050 auto lastRotated = ledgerSeq - 1;
1051 BEAST_EXPECT(store.getLastRotated() == lastRotated);
1052 BEAST_EXPECT(lastRotated != 2);
1053
1054 auto canDelete = env.rpc("can_delete");
1055 BEAST_EXPECT(!rpc::containsError(canDelete[jss::result]));
1056 BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == 0);
1057
1058 canDelete = env.rpc("can_delete", "never");
1059 BEAST_EXPECT(!rpc::containsError(canDelete[jss::result]));
1060 BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == 0);
1061
1062 auto const firstBatch = kDeleteInterval + ledgerSeq;
1063 for (; ledgerSeq < firstBatch; ++ledgerSeq)
1064 {
1065 env.close();
1066
1067 auto ledger = env.rpc("ledger", "validated");
1068 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
1069 }
1070
1071 BEAST_EXPECT(syncStore(env));
1072
1073 ledgerCheck(env, ledgerSeq - 2, 2);
1074 BEAST_EXPECT(lastRotated == store.getLastRotated());
1075
1076 // This does not kick off a cleanup
1077 canDelete = env.rpc("can_delete", std::to_string(ledgerSeq + (kDeleteInterval / 2)));
1078 BEAST_EXPECT(!rpc::containsError(canDelete[jss::result]));
1079 BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == ledgerSeq + (kDeleteInterval / 2));
1080
1081 BEAST_EXPECT(syncStore(env));
1082
1083 ledgerCheck(env, ledgerSeq - 2, 2);
1084 BEAST_EXPECT(store.getLastRotated() == lastRotated);
1085
1086 {
1087 // This kicks off a cleanup, but it stays small.
1088 env.close();
1089
1090 auto ledger = env.rpc("ledger", "validated");
1091 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
1092 }
1093
1094 BEAST_EXPECT(syncStore(env));
1095
1096 ledgerCheck(env, ledgerSeq - lastRotated, lastRotated);
1097
1098 BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1);
1099 lastRotated = ledgerSeq - 1;
1100
1101 for (; ledgerSeq < lastRotated + kDeleteInterval; ++ledgerSeq)
1102 {
1103 // No cleanups in this loop.
1104 env.close();
1105
1106 auto ledger = env.rpc("ledger", "validated");
1107 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
1108 }
1109
1110 BEAST_EXPECT(syncStore(env));
1111
1112 BEAST_EXPECT(store.getLastRotated() == lastRotated);
1113
1114 {
1115 // This kicks off another cleanup.
1116 env.close();
1117
1118 auto ledger = env.rpc("ledger", "validated");
1119 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
1120 }
1121
1122 BEAST_EXPECT(syncStore(env));
1123
1124 ledgerCheck(env, ledgerSeq - firstBatch, firstBatch);
1125
1126 BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1);
1127 lastRotated = ledgerSeq - 1;
1128
1129 // This does not kick off a cleanup
1130 canDelete = env.rpc("can_delete", "always");
1131 BEAST_EXPECT(!rpc::containsError(canDelete[jss::result]));
1132 BEAST_EXPECT(
1133 canDelete[jss::result][jss::can_delete] == std::numeric_limits<unsigned int>::max());
1134
1135 for (; ledgerSeq < lastRotated + kDeleteInterval; ++ledgerSeq)
1136 {
1137 // No cleanups in this loop.
1138 env.close();
1139
1140 auto ledger = env.rpc("ledger", "validated");
1141 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
1142 }
1143
1144 BEAST_EXPECT(syncStore(env));
1145
1146 BEAST_EXPECT(store.getLastRotated() == lastRotated);
1147
1148 {
1149 // This kicks off another cleanup.
1150 env.close();
1151
1152 auto ledger = env.rpc("ledger", "validated");
1153 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
1154 }
1155
1156 BEAST_EXPECT(syncStore(env));
1157
1158 ledgerCheck(env, ledgerSeq - lastRotated, lastRotated);
1159
1160 BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1);
1161 lastRotated = ledgerSeq - 1;
1162
1163 // This does not kick off a cleanup
1164 canDelete = env.rpc("can_delete", "now");
1165 BEAST_EXPECT(!rpc::containsError(canDelete[jss::result]));
1166 BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == ledgerSeq - 1);
1167
1168 for (; ledgerSeq < lastRotated + kDeleteInterval; ++ledgerSeq)
1169 {
1170 // No cleanups in this loop.
1171 env.close();
1172
1173 auto ledger = env.rpc("ledger", "validated");
1174 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
1175 }
1176
1177 BEAST_EXPECT(syncStore(env));
1178
1179 BEAST_EXPECT(store.getLastRotated() == lastRotated);
1180
1181 {
1182 // This kicks off another cleanup.
1183 env.close();
1184
1185 auto ledger = env.rpc("ledger", "validated");
1186 BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
1187 }
1188
1189 BEAST_EXPECT(syncStore(env));
1190
1191 ledgerCheck(env, ledgerSeq - lastRotated, lastRotated);
1192
1193 BEAST_EXPECT(store.getLastRotated() == ledgerSeq - 1);
1194 lastRotated = ledgerSeq - 1;
1195 }
1196
1199 {
1201 std::filesystem::path newPath;
1202
1203 if (!BEAST_EXPECT(path.size()))
1204 return {};
1205 newPath = path;
1206 section.set(Keys::kPath, newPath.string());
1207
1209 section,
1210 megabytes(env.app().config().getValueFor(SizedItem::BurstSize, std::nullopt)),
1211 scheduler,
1212 env.app().getJournal("NodeStoreTest"))};
1213 backend->open();
1214 return backend;
1215 }
1216
1217 void
1219 {
1220 // The only purpose of this test is to ensure that if something that
1221 // should never happen happens, we don't get a deadlock.
1222 testcase("rotate with lock contention");
1223
1224 using namespace jtx;
1225 Env env(*this, envconfig(onlineDelete));
1226
1228 // Create NodeStore with two backends to allow online deletion of data.
1229 // Normally, SHAMapStoreImp handles all these details.
1230 auto nscfg = env.app().config().section(Sections::kNodeDatabase);
1231
1232 // Provide default values.
1233 if (!nscfg.exists(Keys::kCacheSize))
1234 {
1235 nscfg.set(
1238 env.app().config().getValueFor(SizedItem::TreeCacheSize, std::nullopt)));
1239 }
1240
1241 if (!nscfg.exists(Keys::kCacheAge))
1242 {
1243 nscfg.set(
1246 env.app().config().getValueFor(SizedItem::TreeCacheAge, std::nullopt)));
1247 }
1248
1249 NodeStoreScheduler scheduler(env.app().getJobQueue());
1250
1251 std::string const writableDb = "write";
1252 std::string const archiveDb = "archive";
1253 auto writableBackend = makeBackendRotating(env, scheduler, writableDb);
1254 auto archiveBackend = makeBackendRotating(env, scheduler, archiveDb);
1255
1256 static constexpr int kReadThreads = 4;
1258 scheduler,
1259 kReadThreads,
1260 std::move(writableBackend),
1261 std::move(archiveBackend),
1262 nscfg,
1263 env.app().getJournal("NodeStoreTest"));
1264
1266 // Check basic functionality
1267 using namespace std::chrono_literals;
1268 std::atomic<int> threadNum = 0;
1269
1270 {
1271 auto newBackend = makeBackendRotating(env, scheduler, std::to_string(++threadNum));
1272
1273 auto const cb = [&](std::string const& writableName, std::string const& archiveName) {
1274 BEAST_EXPECT(writableName == "1");
1275 BEAST_EXPECT(archiveName == "write");
1276 // Ensure that dbr functions can be called from within the
1277 // callback
1278 BEAST_EXPECT(dbr->getName() == "1");
1279 };
1280
1281 dbr->rotate(std::move(newBackend), cb);
1282 }
1283 BEAST_EXPECT(threadNum == 1);
1284 BEAST_EXPECT(dbr->getName() == "1");
1285
1287 // Do something stupid. Try to re-enter rotate from inside the callback.
1288 {
1289 auto const cb = [&](std::string const& writableName, std::string const& archiveName) {
1290 BEAST_EXPECT(writableName == "3");
1291 BEAST_EXPECT(archiveName == "2");
1292 // Ensure that dbr functions can be called from within the
1293 // callback
1294 BEAST_EXPECT(dbr->getName() == "3");
1295 };
1296 auto const cbReentrant = [&](std::string const& writableName,
1297 std::string const& archiveName) {
1298 BEAST_EXPECT(writableName == "2");
1299 BEAST_EXPECT(archiveName == "1");
1300 auto newBackend = makeBackendRotating(env, scheduler, std::to_string(++threadNum));
1301 // Reminder: doing this is stupid and should never happen
1302 dbr->rotate(std::move(newBackend), cb);
1303 };
1304 auto newBackend = makeBackendRotating(env, scheduler, std::to_string(++threadNum));
1305 dbr->rotate(std::move(newBackend), cbReentrant);
1306 }
1307
1308 BEAST_EXPECT(threadNum == 3);
1309 BEAST_EXPECT(dbr->getName() == "3");
1310 }
1311
1312 void
1314 {
1315 // Note that this test is intentionally very similar to
1316 // LedgerMaster_test::testCompleteLedgerRange, but has a different
1317 // focus.
1318
1319 testcase("Wait for ledger gaps to fill in");
1320
1321 using namespace test::jtx;
1322
1323 Env env{*this, envconfig(onlineDelete)};
1324
1325 auto failureMessage = [&](char const* label, auto expected, auto actual) {
1327 ss << label << ": Expected: " << expected << ", Got: " << actual;
1328 return ss.str();
1329 };
1330
1331 auto const alice = Account("alice");
1332 env.fund(XRP(1000), alice);
1333 env.close();
1334
1335 auto& lm = env.app().getLedgerMaster();
1336 LedgerIndex minSeq = 2;
1337 auto& store = env.app().getSHAMapStore();
1338 auto& netOPs = env.app().getOPs();
1339 // Which of the existing complete ledgers the store initializes
1340 // lastRotated from is a timing detail, so everything below derives from
1341 // the observed value rather than assuming a particular one. Spinning
1342 // until it equals a hard-coded value never terminates when a different
1343 // one legitimately wins.
1344 //
1345 // The range check and the initializeStore() one both end the testcase
1346 // rather than merely reporting, because lastRotated is the only value
1347 // from the store that enters minSeq. A lastRotated of 0 -- the value
1348 // getLastRotated() reports until the store has been handed a validated
1349 // ledger -- makes minSeq 0 below, and the minSeq - 1 and minSeq - 2
1350 // ranges then underflow to first > last, which aborts a Debug build
1351 // inside missingFromCompleteLedgerRange().
1352 auto const extraCloses = initializeStore(env);
1353 if (!BEAST_EXPECT(extraCloses.has_value()))
1354 return;
1355 LedgerIndex maxSeq = env.closed()->header().seq;
1356 LedgerIndex lastRotated = store.getLastRotated();
1357 if (!BEAST_EXPECTS(
1358 lastRotated >= minSeq && lastRotated <= maxSeq, std::to_string(lastRotated)))
1359 return;
1360 // The BEAST_EXPECT above already returned if this is nullopt, but that
1361 // is invisible to clang-tidy's optional model.
1362 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
1363 BEAST_EXPECTS(maxSeq == 3 + *extraCloses, std::to_string(maxSeq));
1364 std::stringstream initialRange;
1365 initialRange << minSeq << "-" << maxSeq;
1366 BEAST_EXPECTS(lm.getCompleteLedgers() == initialRange.str(), lm.getCompleteLedgers());
1367 BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
1368 // The inner range is empty unless initializeStore() had to close extra
1369 // ledgers, and missingFromCompleteLedgerRange() treats first > last as a
1370 // precondition violation that aborts a Debug build via UNREACHABLE, so
1371 // only check it when it is well formed.
1372 if (minSeq + 1 <= maxSeq - 1)
1373 {
1374 BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
1375 }
1376 BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
1377 BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
1378 BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
1379
1380 auto expectedRange =
1381 [](LedgerIndex minSeq, std::vector<LedgerIndex> const& deleteSeqs, LedgerIndex maxSeq) {
1382 std::stringstream expectedRange;
1383 expectedRange << minSeq;
1384 auto lastDelete = minSeq - 1;
1385 for (auto deleteSeq : deleteSeqs)
1386 {
1387 if (deleteSeq <= lastDelete)
1388 continue;
1389 expectedRange << "-" << (deleteSeq - 1);
1390 if (deleteSeq + 1 <= maxSeq)
1391 expectedRange << "," << (deleteSeq + 1);
1392 lastDelete = deleteSeq;
1393 }
1394 if (lastDelete + 1 < maxSeq)
1395 {
1396 expectedRange << "-" << maxSeq;
1397 }
1398 return expectedRange.str();
1399 };
1400
1401 auto deleteLedgerSeq =
1402 [&lm, &store, &netOPs, &minSeq, &lastRotated, &expectedRange, &failureMessage, this](
1403 Env& env,
1404 LedgerIndex& maxSeq,
1405 std::vector<LedgerIndex>& deleteSeqs) -> LedgerIndex {
1406 using namespace std::chrono_literals;
1407
1408 // The next ledger will trigger a rotation. Delete the
1409 // current ledger from LedgerMaster.
1410
1411 netOPs.setMode(OperatingMode::CONNECTED);
1412
1413 LedgerIndex const deleteSeq = maxSeq;
1414 std::size_t iterations = 30;
1415 while (!lm.haveLedger(deleteSeq) && --iterations > 0)
1416 {
1418 }
1419 // Even the slowest machines should be able to finalize deleteSeq within 10
1420 // loops (100ms). If this test ever actually fails feel free to lower this
1421 // cutoff. The intent of this test is to flag if the loop takes a very long
1422 // time, but still allow the rest of this function to finish.
1423 BEAST_EXPECTS(iterations > 20, std::to_string(iterations));
1424 if (!BEAST_EXPECT(lm.haveLedger(deleteSeq)))
1425 return 0;
1426
1427 // This test may be timing sensitive, because it's messing with server internals in ways
1428 // that they can't be messed with normally. Sleep a little bit to give the server time
1429 // to finish any internal work before we delete the ledger.
1431
1432 lm.clearLedger(deleteSeq);
1433 deleteSeqs.push_back(deleteSeq);
1434 if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
1435 return 0;
1436
1437 BEAST_EXPECTS(
1438 lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeqs, maxSeq),
1439 failureMessage(
1440 "Complete ledgers",
1441 expectedRange(minSeq, deleteSeqs, maxSeq),
1442 lm.getCompleteLedgers()));
1443 BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == deleteSeqs.size());
1444
1445 if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
1446 return 0;
1447 // Close another ledger, which will trigger a rotation, but the
1448 // rotation will be stuck until the missing ledger is filled in.
1449 env.close();
1450 // Do not call rendezvous() here without a timeout; it will block until the missing
1451 // ledger is backfilled. That will not happen automatically. It's a manual step that
1452 // is done later in this test.
1453 ++maxSeq;
1454
1455 if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
1456 return 0;
1457 netOPs.setMode(OperatingMode::FULL);
1458
1459 if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
1460 return 0;
1461 BEAST_EXPECT(!store.rendezvous(10ms));
1462 BEAST_EXPECT(netOPs.getOperatingMode() == OperatingMode::FULL);
1463
1464 // Nothing has changed
1465 BEAST_EXPECTS(
1466 store.getLastRotated() == lastRotated,
1467 failureMessage("lastRotated", lastRotated, store.getLastRotated()));
1468 BEAST_EXPECTS(
1469 lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeqs, maxSeq),
1470 failureMessage(
1471 "Complete ledgers",
1472 expectedRange(minSeq, deleteSeqs, maxSeq),
1473 lm.getCompleteLedgers()));
1474
1475 return deleteSeq;
1476 };
1477
1478 std::vector<LedgerIndex> deleteSeqs;
1479
1480 // Close enough ledgers to rotate a few times
1481 while (maxSeq < 40)
1482 {
1483 for (int t = 0; t < 3; ++t)
1484 {
1485 env(noop(alice));
1486 }
1487 env.close();
1488 BEAST_EXPECT(syncStore(env));
1489
1490 ++maxSeq;
1491
1492 if (maxSeq + 1 == lastRotated + kDeleteInterval)
1493 {
1494 using namespace std::chrono_literals;
1495
1496 {
1497 // Trigger the circuit breaker in SHAMapStoreImp::healthWait() to ensure it
1498 // doesn't block forever.
1499 LedgerIndex const deleteSeq = deleteLedgerSeq(env, maxSeq, deleteSeqs);
1500 if (!BEAST_EXPECT(deleteSeq > 0))
1501 return;
1502 if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
1503 return;
1504
1505 // Close 7 more ledgers, waiting a little bit in between to
1506 // simulate the ledger making progress while online delete waits
1507 // for the missing ledger to be filled in.
1508 // After the 7th ledger, the circuit breaker will trigger and abort the attempt.
1509 while (maxSeq < lastRotated + (kDeleteInterval * 2) - 2)
1510 {
1511 env.close();
1512 ++maxSeq;
1513 // Nothing has changed
1514 BEAST_EXPECTS(
1515 store.getLastRotated() == lastRotated,
1516 failureMessage("lastRotated", lastRotated, store.getLastRotated()));
1517 BEAST_EXPECTS(
1518 lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeqs, maxSeq),
1519 failureMessage(
1520 "Complete Ledgers",
1521 expectedRange(minSeq, deleteSeqs, maxSeq),
1522 lm.getCompleteLedgers()));
1523 // The Store is "stuck" in healthWait() and won't finish the run() loop
1524 // until it's backfilled
1525 if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
1526 return;
1527 }
1528
1529 // Close one more ledger, which will NOT trigger the circuit breaker. Wait for
1530 // the full 1 second recovery wait timeout to ensure the circuit breaker is not
1531 // triggered.
1532 env.close();
1533 ++maxSeq;
1534 // The Store is "stuck" in healthWait() and won't finish the run() loop
1535 // until it's backfilled
1536 BEAST_EXPECT(!store.rendezvous(1s));
1537
1538 // Close one more ledger, which will trigger the circuit breaker and abort the
1539 // attempt to rotate.
1540 env.close();
1541 ++maxSeq;
1542 // Nothing has changed
1543 BEAST_EXPECTS(
1544 store.getLastRotated() == lastRotated,
1545 failureMessage("lastRotated", lastRotated, store.getLastRotated()));
1546 BEAST_EXPECTS(
1547 lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeqs, maxSeq),
1548 failureMessage(
1549 "Complete Ledgers",
1550 expectedRange(minSeq, deleteSeqs, maxSeq),
1551 lm.getCompleteLedgers()));
1552
1553 // The circuit breaker has been triggered.
1554 BEAST_EXPECT(syncStore(env));
1555 }
1556 {
1557 // Recover before the circuit breaker triggers, so the test can continue.
1558 LedgerIndex const deleteSeq = deleteLedgerSeq(env, maxSeq, deleteSeqs);
1559 if (!BEAST_EXPECT(deleteSeq > 0))
1560 return;
1561 if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
1562 return;
1563
1564 // Close 5 more ledgers, waiting a little bit in between to
1565 // simulate the ledger making progress while online delete waits
1566 // for the missing ledger to be filled in.
1567 // This ensures the healthWait check has time to run and
1568 // detect the gap.
1569 for (int l = 0; l < 5; ++l)
1570 {
1571 env.close();
1572 ++maxSeq;
1573 // Nothing has changed
1574 BEAST_EXPECTS(
1575 store.getLastRotated() == lastRotated,
1576 failureMessage("lastRotated", lastRotated, store.getLastRotated()));
1577 BEAST_EXPECTS(
1578 lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeqs, maxSeq),
1579 failureMessage(
1580 "Complete Ledgers",
1581 expectedRange(minSeq, deleteSeqs, maxSeq),
1582 lm.getCompleteLedgers()));
1583 if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
1584 return;
1585 }
1586
1587 // The Store is "stuck" in healthWait() and won't finish the run() loop
1588 // until it's backfilled
1589 // Wait for the full 1 second recovery wait timeout to ensure the circuit
1590 // breaker is not triggered, and this isn't some other timing fluke.
1591 BEAST_EXPECT(!store.rendezvous(1s));
1592
1593 // Put the missing ledger back in LedgerMaster
1594 lm.setLedgerRangePresent(deleteSeq, deleteSeq);
1595 BEAST_EXPECT(deleteSeqs.back() == deleteSeq);
1596 deleteSeqs.pop_back();
1597
1598 // Wait for the rotation to finish
1599 BEAST_EXPECT(syncStore(env));
1600
1601 minSeq = lastRotated;
1602 while (deleteSeqs.front() < minSeq)
1603 {
1604 deleteSeqs.erase(deleteSeqs.begin());
1605 }
1606 lastRotated = deleteSeq + 1;
1607 }
1608 }
1609 BEAST_EXPECT(maxSeq != lastRotated + kDeleteInterval);
1610 BEAST_EXPECTS(
1611 env.closed()->header().seq == maxSeq,
1612 failureMessage("maxSeq", maxSeq, env.closed()->header().seq));
1613 BEAST_EXPECTS(
1614 store.getLastRotated() == lastRotated,
1615 failureMessage("lastRotated", lastRotated, store.getLastRotated()));
1616 {
1617 auto const expected = expectedRange(minSeq, deleteSeqs, maxSeq);
1618 BEAST_EXPECTS(
1619 lm.getCompleteLedgers() == expected,
1620 failureMessage("CompleteLedgers", expected, lm.getCompleteLedgers()));
1621 }
1622 BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == deleteSeqs.size());
1623 // missingFromCompleteLedgerRange() treats first > last as a
1624 // precondition violation and aborts a Debug build via UNREACHABLE.
1625 // The range can only collapse if this test's model of minSeq /
1626 // maxSeq has desynced from the store, so report that as a failure
1627 // instead of taking down the whole unit test job.
1628 if (minSeq + 1 <= maxSeq - 1)
1629 {
1630 BEAST_EXPECT(
1631 lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == deleteSeqs.size());
1632 }
1633 else
1634 {
1635 BEAST_EXPECTS(false, failureMessage("range collapsed", minSeq, maxSeq));
1636 }
1637 BEAST_EXPECT(
1638 lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == deleteSeqs.size() + 2);
1639 BEAST_EXPECT(
1640 lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == deleteSeqs.size() + 2);
1641 BEAST_EXPECT(
1642 lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == deleteSeqs.size() + 2);
1643 }
1644 }
1645
1646 // Cover the branches of SHAMapStoreImp::healthWait() that decide whether
1647 // the server is healthy enough to rotate, and how loudly to complain while
1648 // it is not. testLedgerGaps() covers the case where a gap holds the
1649 // rotation back until the circuit breaker trips; these cover the rest of
1650 // the decision table.
1651 void
1653 {
1654 testcase("healthWait server state");
1655
1656 using namespace std::chrono_literals;
1657 using namespace test::jtx;
1658
1659 auto logs = std::make_unique<StoreLogs>();
1660 // Not `auto const*`: waitFor() blocks, so it is not const.
1661 auto* const log = logs.get();
1662 Env env{*this, envconfig(onlineDelete), std::move(logs), beast::Severity::Trace};
1663
1664 auto& store = env.app().getSHAMapStore();
1665 auto& netOPs = env.app().getOPs();
1666
1667 // No gap: the only thing holding the store back is the operating mode.
1668 auto const parked = parkInHealthWait(env, false, OperatingMode::CONNECTED);
1669 if (!parked)
1670 return;
1671
1672 // Hold the non-FULL mode until the store has logged that it is waiting
1673 // on it. With no gap, a fresh validated ledger and a mode that is not
1674 // DISCONNECTED, the only check left that can report unhealthy is
1675 // "mode != FULL", and a mode that is not FULL is not expected to fix
1676 // itself, so the wait is logged at warn, for the full duration.
1677 //
1678 // Waiting for the message rather than sleeping past it is what keeps
1679 // this from depending on how quickly the store gets around to sampling.
1680 // The store cannot leave the wait loop while the mode stays put -- the
1681 // validated ledger index does not advance, so the circuit breaker is
1682 // never reached -- so the rendezvous() below is not racing it.
1683 BEAST_EXPECT(log->waitFor(beast::Severity::Warning, kFullWait, 10s));
1684 BEAST_EXPECT(!store.rendezvous(10ms));
1685 BEAST_EXPECT(netOPs.getOperatingMode() != OperatingMode::FULL);
1686 BEAST_EXPECT(netOPs.getOperatingMode() != OperatingMode::DISCONNECTED);
1687 BEAST_EXPECTS(
1688 store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
1689
1690 // Now make the mode FULL but the validated ledger stale. Advancing the
1691 // clock without closing a ledger ages the validated ledger past
1692 // age_threshold_seconds, which defaults to 60. The mode check can no
1693 // longer be the reason the store is unhealthy, so the age check is.
1694 //
1695 // This one does sleep: what is being asserted is that the store did not
1696 // rotate, and 1500ms is long enough for it to have re-sampled the server
1697 // at least once -- the full wait is 1000ms -- so the age check, not a
1698 // stale sample of the old mode, is what held it back.
1699 auto const closeTime = env.now();
1700 env.timeKeeper().set(closeTime + 2min);
1701 netOPs.setMode(OperatingMode::FULL);
1702 BEAST_EXPECT(netOPs.getOperatingMode() == OperatingMode::FULL);
1703 BEAST_EXPECT(!store.rendezvous(1500ms));
1704 BEAST_EXPECTS(
1705 store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
1706
1707 // Restore the clock. Nothing is wrong any more, so the rotation that
1708 // has been waiting all along runs to completion.
1709 env.timeKeeper().set(closeTime);
1710 BEAST_EXPECT(syncStore(env));
1711 BEAST_EXPECTS(
1712 store.getLastRotated() == parked->validated, std::to_string(store.getLastRotated()));
1713 }
1714
1715 void
1717 {
1718 testcase("healthWait gap wait levels");
1719
1720 using namespace std::chrono_literals;
1721 using namespace test::jtx;
1722
1723 auto logs = std::make_unique<StoreLogs>();
1724 // Not `auto const*`: waitFor() blocks, so it is not const.
1725 auto* const log = logs.get();
1726 Env env{*this, envconfig(onlineDelete), std::move(logs), beast::Severity::Trace};
1727
1728 auto& lm = env.app().getLedgerMaster();
1729 auto& store = env.app().getSHAMapStore();
1730
1731 auto const parked = parkInHealthWait(env, true, OperatingMode::FULL);
1732 if (!parked)
1733 return;
1734
1735 // The missing ledger is an older one; the validated ledger itself is
1736 // present. The store has no reason to think the gap will close on its
1737 // own, so it waits the full duration and says so at info -- not warn,
1738 // because the server is otherwise healthy and has not been waiting long
1739 // enough to have fallen behind.
1740 BEAST_EXPECT(lm.haveLedger(parked->validated));
1741 BEAST_EXPECT(!lm.haveLedger(parked->gap));
1742 BEAST_EXPECT(log->waitFor(beast::Severity::Info, kFullWait, 10s));
1743 // Nothing so far should have looked like a ledger being built.
1744 BEAST_EXPECT(log->count(beast::Severity::Trace, kShortWait) == 0);
1745 // Nothing fills the gap in, so the store is still in the wait loop.
1746 BEAST_EXPECT(!store.rendezvous(10ms));
1747
1748 // Move the gap onto the validated ledger itself. That is the one case
1749 // the store treats as transient -- the ledger is expected to be built
1750 // shortly -- so it drops to trace and waits a tenth as long. Asserting
1751 // that the shortened wait appears only after this swap is what pins the
1752 // branch to the buildingIndex condition, rather than to anything
1753 // incidental about a store that happens to be waiting.
1754 lm.setLedgerRangePresent(parked->gap, parked->gap);
1755 lm.clearLedger(parked->validated);
1756 BEAST_EXPECT(lm.haveLedger(parked->gap));
1757 BEAST_EXPECT(!lm.haveLedger(parked->validated));
1758 BEAST_EXPECT(log->waitFor(beast::Severity::Trace, kShortWait, 10s));
1759 BEAST_EXPECT(!store.rendezvous(10ms));
1760 BEAST_EXPECTS(
1761 store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
1762
1763 // Fill it in and the rotation completes.
1764 lm.setLedgerRangePresent(parked->validated, parked->validated);
1765 BEAST_EXPECT(syncStore(env));
1766 BEAST_EXPECTS(
1767 store.getLastRotated() == parked->validated, std::to_string(store.getLastRotated()));
1768 }
1769
1770 void
1772 {
1773 testcase("healthWait disconnected");
1774
1775 using namespace std::chrono_literals;
1776 using namespace test::jtx;
1777
1778 Env env{*this, envconfig(onlineDelete)};
1779
1780 auto& lm = env.app().getLedgerMaster();
1781 auto& store = env.app().getSHAMapStore();
1782 auto& netOPs = env.app().getOPs();
1783
1784 auto const parked = parkInHealthWait(env, true, OperatingMode::FULL);
1785 if (!parked)
1786 return;
1787
1788 // While the server is FULL, the gap holds the rotation back.
1789 BEAST_EXPECT(!store.rendezvous(1500ms));
1790 BEAST_EXPECTS(
1791 store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
1792
1793 // A disconnected server is not doing any ledger I/O, so the gap cannot
1794 // have been caused by its own activity and will not close until it has
1795 // peers again. The store deliberately takes advantage of that to get as
1796 // much rotation done as possible: this is the one case where a gap does
1797 // not hold online delete back at all.
1798 netOPs.setMode(OperatingMode::DISCONNECTED);
1799 BEAST_EXPECT(netOPs.getOperatingMode() == OperatingMode::DISCONNECTED);
1800 BEAST_EXPECT(syncStore(env));
1801 BEAST_EXPECTS(
1802 store.getLastRotated() == parked->validated, std::to_string(store.getLastRotated()));
1803 // The rotation ran with the gap still present -- nothing filled it in.
1804 BEAST_EXPECT(!lm.haveLedger(parked->gap));
1805 }
1806
1807 void
1809 {
1810 testcase("healthWait stop");
1811
1812 using namespace test::jtx;
1813
1814 Env env{*this, envconfig(onlineDelete)};
1815
1816 auto& store = env.app().getSHAMapStore();
1817
1818 auto const parked = parkInHealthWait(env, true, OperatingMode::FULL);
1819 if (!parked)
1820 return;
1821
1822 // Stopping the store has to break it out of the wait loop, which it
1823 // would otherwise never leave: the gap is never filled in and the
1824 // validated ledger index never advances to reach the circuit breaker.
1825 //
1826 // stop() joins the store's thread, so its return is the
1827 // synchronisation point here. rendezvous() afterwards is only a
1828 // cross-check, and cannot block: the store is parked in the health
1829 // check that gates a rotation, so Stopping there merely leaves
1830 // readyToRotate false, and run() falls through to the top of its loop,
1831 // where it clears working_ and notifies before returning on stop_.
1832 store.stop();
1833 BEAST_EXPECT(store.rendezvous());
1834 BEAST_EXPECTS(
1835 store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
1836 }
1837
1838 // The two tests below cover the health check that run() performs between the
1839 // stages of a rotation it has already committed to, which is a different
1840 // decision from the one that gates the rotation in the first place: giving
1841 // up here means abandoning work in progress. run() makes it at four points
1842 // -- after clearing prior ledgers, after copying the validated ledger, after
1843 // freshening the caches, and after clearing them -- with the same three-way
1844 // switch each time, and parkMidRotation() parks the store at the first of
1845 // them.
1846 void
1848 {
1849 testcase("healthWait circuit breaker mid-rotation");
1850
1851 using namespace test::jtx;
1852
1853 auto logs = std::make_unique<StoreLogs>();
1854 auto* const log = logs.get();
1855 Env env{*this, envconfig(slowOnlineDelete), std::move(logs), beast::Severity::Trace};
1856
1857 auto& lm = env.app().getLedgerMaster();
1858 auto& store = env.app().getSHAMapStore();
1859
1860 auto const parked = parkMidRotation(env, *log);
1861 if (!parked)
1862 return;
1863
1864 // Advance the validated ledger index past the circuit breaker. The store
1865 // has had no successful health check since the gap appeared, so once the
1866 // index has moved max_waiting_ledgers on from the last one that did
1867 // succeed, it abandons the rotation instead of waiting for the gap
1868 // forever. Nothing here fills the gap in.
1869 for (int i = 0; i < kMinWaitingLedgers; ++i)
1870 {
1871 env.close();
1872 BEAST_EXPECT(!lm.haveLedger(parked->gap));
1873 }
1874
1875 // Abandoning the rotation returns the store to waiting for work, so it
1876 // reports itself idle -- but with lastRotated left where it started,
1877 // unlike the completed rotation parkMidRotation() drove first.
1878 BEAST_EXPECT(syncStore(env));
1879 BEAST_EXPECTS(
1880 store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
1881 BEAST_EXPECT(log->count(kExpired) > 0);
1882 BEAST_EXPECTS(log->count(kFinished) == 1, std::to_string(log->count(kFinished)));
1883 BEAST_EXPECT(!lm.haveLedger(parked->gap));
1884 }
1885
1886 void
1888 {
1889 testcase("healthWait stop mid-rotation");
1890
1891 using namespace test::jtx;
1892
1893 auto logs = std::make_unique<StoreLogs>();
1894 auto* const log = logs.get();
1895 Env env{*this, envconfig(slowOnlineDelete), std::move(logs), beast::Severity::Trace};
1896
1897 auto& store = env.app().getSHAMapStore();
1898
1899 auto const parked = parkMidRotation(env, *log);
1900 if (!parked)
1901 return;
1902
1903 // Stopping has to break the store out of the rotation, which it would
1904 // otherwise never leave: the gap is never filled in and the validated
1905 // ledger index never advances to reach the circuit breaker. Note that
1906 // being stopped outranks being healthy -- the health check reports it
1907 // even when nothing is wrong with the server -- so this does not depend
1908 // on the store still being parked when stop() lands.
1909 //
1910 // stop() joins the store's thread, so its return is the synchronisation
1911 // point. Deliberately do not call the untimed rendezvous() afterwards:
1912 // run() returns without clearing working_, so it would block forever.
1913 store.stop();
1914 BEAST_EXPECTS(
1915 store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
1916 // The rotation was abandoned rather than completed, and the circuit
1917 // breaker was not what abandoned it.
1918 BEAST_EXPECTS(log->count(kFinished) == 1, std::to_string(log->count(kFinished)));
1919 BEAST_EXPECTS(log->count(kExpired) == 0, std::to_string(log->count(kExpired)));
1920 }
1921
1922 void
1938};
1939
1940// VFALCO This test fails because of thread asynchronous issues
1942
1943} // namespace xrpl::test
Abstraction for the underlying message destination.
Definition Journal.h:59
A testsuite class.
Definition suite.h:52
LogOs< char > log
Logging output stream.
Definition suite.h:150
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
virtual Config & config()=0
Section & section(std::string const &name)
Returns the section with the given name.
std::uint32_t ledgerHistory
int getValueFor(SizedItem item, std::optional< std::size_t > node=std::nullopt) const
Retrieve the default value for the item at the specified node size.
void rendezvous()
Block until no jobs running.
Definition JobQueue.cpp:243
Logs(beast::Severity level)
Definition Log.cpp:112
A node_store::Scheduler which uses the JobQueue.
virtual std::size_t getAccountTransactionCount()=0
getAccountTransactionCount Returns the number of account transactions.
virtual std::size_t getTransactionCount()=0
getTransactionCount Returns the number of transactions.
virtual CountMinMax getLedgerCountMinMax()=0
getLedgerCountMinMax Returns the minimum ledger sequence, maximum ledger sequence and total number of...
virtual std::optional< LedgerHeader > getLedgerInfoByIndex(LedgerIndex ledgerSeq)=0
getLedgerInfoByIndex Returns a ledger by its sequence.
class to create database, launch online delete thread, and related SQLite database
Definition SHAMapStore.h:25
virtual bool rendezvous(std::optional< std::chrono::milliseconds > const &timeout={}) const =0
Holds a collection of configuration values.
Definition BasicConfig.h:28
void set(std::string const &key, std::string const &value)
Set a key/value pair.
virtual JobQueue & getJobQueue()=0
virtual RelationalDatabase & getRelationalDatabase()=0
virtual beast::Journal getJournal(std::string const &name)=0
virtual NetworkOPs & getOPs()=0
virtual SHAMapStore & getSHAMapStore()=0
virtual LedgerMaster & getLedgerMaster()=0
virtual std::unique_ptr< Backend > makeBackend(Section const &parameters, std::size_t burstSize, Scheduler &scheduler, beast::Journal journal)=0
Create a backend.
static Manager & instance()
Returns the instance of the manager singleton.
void write(beast::Severity level, std::string const &text) override
Write text to the sink at the specified severity.
Sink(beast::Severity threshold, StoreLogs &owner)
void threshold(beast::Severity) override
Set the minimum severity this sink will report.
void writeAlways(beast::Severity level, std::string const &text) override
Bypass filter and write text to the sink at the specified severity.
std::size_t countLocked(std::optional< beast::Severity > severity, std::string const &text) const
bool waitFor(std::string const &text, std::chrono::milliseconds timeout, std::size_t expected=1)
std::unique_ptr< beast::Journal::Sink > makeSink(std::string const &partition, beast::Severity) override
bool waitFor(std::optional< beast::Severity > severity, std::string const &text, std::chrono::milliseconds timeout, std::size_t expected=1)
std::vector< std::pair< beast::Severity, std::string > > messages_
std::size_t count(std::string const &text) const
std::size_t count(beast::Severity severity, std::string const &text) const
static auto onlineDelete(std::unique_ptr< Config > cfg)
static auto advisoryDelete(std::unique_ptr< Config > cfg)
std::optional< Parked > parkInHealthWait(jtx::Env &env, bool createGap, OperatingMode modeAfterClose)
std::optional< int > initializeStore(jtx::Env &env, int const maxExtraCloses=3)
static constexpr char const * kShortWait
static constexpr int kMinDeleteInterval
static constexpr int kDeleteInterval
std::unique_ptr< node_store::Backend > makeBackendRotating(jtx::Env &env, NodeStoreScheduler &scheduler, std::string path)
static constexpr char const * kExpired
static bool bad(json::Value const &json, ErrorCodeI error=RpcLgrNotFound)
void ledgerCheck(jtx::Env &env, int const rows, int const first)
void expectConfigAccepted(std::function< void(Config &)> const &tweak)
void accountTransactionCheck(jtx::Env &env, int const rows)
void run() override
Runs the suite.
void expectConfigRejected(std::string const &expected, std::function< void(Config &)> const &tweak)
static constexpr int kMinWaitingLedgers
static auto slowOnlineDelete(std::unique_ptr< Config > cfg)
static bool syncStore(jtx::Env &env)
std::optional< std::string > storeConfigResult(std::function< void(Config &)> const &tweak)
void transactionCheck(jtx::Env &env, int const rows)
static constexpr char const * kFullWait
std::string getHash(json::Value const &json)
std::optional< Parked > parkMidRotation(jtx::Env &env, StoreLogs &log)
static constexpr char const * kRotating
static constexpr char const * kFinished
static bool goodLedger(jtx::Env &env, json::Value const &json, std::string ledgerID, bool checkDB=false)
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition Env.cpp:127
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:1058
ManualTimeKeeper & timeKeeper()
Definition Env.h:313
NetClock::time_point now()
Returns the current network time.
Definition Env.h:326
T count_if(T... args)
T emplace(T... args)
T endl(T... args)
T make_unique(T... args)
T max(T... args)
Severity
Severity level / threshold of a Journal message.
Definition Journal.h:16
JSON (JavaScript Object Notation).
Definition json_errors.h:5
bool containsError(json::Value const &json)
Returns true if the json contains an rpc error specification.
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
json::Value noop(Account const &account)
The null transaction.
Definition noop.h:14
std::array< Account, 1+sizeof...(Args)> noripple(Account const &account, Args const &... args)
Designate accounts as no-ripple in Env::fund.
Definition Env.h:86
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:38
std::unique_ptr< Config > onlineDelete(std::unique_ptr< Config > cfg, std::uint32_t deleteInterval=8)
adjust config to enable online_delete
Definition envconfig.cpp:66
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
ErrorCodeI
Definition ErrorCodes.h:23
@ RpcLgrNotFound
Definition ErrorCodes.h:55
@ RpcNotEnabled
Definition ErrorCodes.h:42
std::uint32_t LedgerIndex
A ledger index.
Definition Protocol.h:382
std::unique_ptr< beast::Journal::Sink > setDebugLogSink(std::unique_ptr< beast::Journal::Sink > sink)
Set the sink for the debug journal.
Definition Log.cpp:393
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
constexpr auto megabytes(T value) noexcept
OperatingMode
Specifies the mode under which the server believes it's operating.
Definition NetworkOPs.h:60
@ DISCONNECTED
not ready to process requests
Definition NetworkOPs.h:61
@ CONNECTED
convinced we are talking to the network
Definition NetworkOPs.h:62
@ FULL
we have the ledger and can even validate
Definition NetworkOPs.h:65
T sleep_for(T... args)
T str(T... args)
static constexpr auto kMaxWaitingLedgers
Definition Constants.h:128
static constexpr auto kBackOffMilliseconds
Definition Constants.h:92
static constexpr auto kAdvisoryDelete
Definition Constants.h:89
static constexpr auto kCacheSize
Definition Constants.h:99
static constexpr auto kDeleteBatch
Definition Constants.h:104
static constexpr auto kRecoveryWaitSeconds
Definition Constants.h:149
static constexpr auto kPath
Definition Constants.h:144
static constexpr auto kCacheAge
Definition Constants.h:97
static constexpr auto kOnlineDelete
Definition Constants.h:137
Information about the notional ledger backing the view.
NetClock::time_point parentCloseTime
NetClock::duration closeTimeResolution
NetClock::time_point closeTime
static constexpr auto kNodeDatabase
Definition Constants.h:32
T time_since_epoch(T... args)
T to_string(T... args)
T value(T... args)
T what(T... args)