Compare commits

...

286 Commits

Author SHA1 Message Date
Alex Kremer
435d56e7c5 Fix empty log lines (#825) 2023-08-16 22:57:52 +01:00
Alex Kremer
bf3b24867c Implement sanitizer support via CMake (#822)
Fixes #302
2023-08-15 15:20:50 +01:00
Alex Kremer
ec70127050 Add LOG macro to prevent unnecessary evaluations (#823)
Fixes #824
2023-08-15 14:36:11 +01:00
Alex Kremer
547cb340bd Update doxygen comments (#818)
Fixes #421
2023-08-11 21:32:32 +01:00
Arihant Kothari
c20b14494a Add note on code coverage report generation (#821) 2023-08-11 18:38:35 +01:00
Peter Chen
696b1a585c Refactor namespaces part 2 (#820)
Part 2 of refactoring effort
2023-08-11 17:00:31 +01:00
Peter Chen
23442ff1a7 Refactor namespaces part 1 (#817)
Part 1 of refactoring effort
2023-08-10 18:05:13 +01:00
Alex Kremer
db4046e02a Move connection state logic to an earlier point (#816) 2023-08-09 16:42:33 +01:00
Peter Chen
fc1b5ae4da Support whitelisting for IPV4/IPV6 with CIDR (#796)
Fixes #244
2023-08-08 16:04:16 +01:00
Alex Kremer
5411fd7497 Update readme and compiler requirements (#815) 2023-08-08 11:32:18 +01:00
Michael Legleux
f6488f7024 Fix Linux/gcc build on CI (#813) 2023-08-07 20:53:20 +01:00
cyan317
e3ada6c5da Remove deprecated fields (#814)
Fix #801
2023-08-07 18:23:13 +01:00
cyan317
d61d702ccd Account_info add flags (#812)
Fixes #768
2023-08-04 16:22:39 +01:00
Alex Kremer
4d42cb3cdb Expose advanced options from cassandra-cpp-driver thru the config (#808)
Fixes #810
2023-08-03 15:49:56 +01:00
cyan317
111b55b397 Add cache test (#807)
Fixes #809
2023-08-03 15:03:17 +01:00
cyan317
c90bc15959 Return error when limit<=0 (#804)
Fix #806
2023-08-02 15:34:42 +01:00
Shawn Xie
1804e3e9c0 Update tmp build instructions in README (#802) 2023-08-02 13:45:04 +01:00
Alex Kremer
24f69acd9e Fix Linux/gcc compilation (#795)
Fixes #803
2023-08-02 13:44:03 +01:00
Alex Kremer
98d0a963dc Fix backend factory test and remove cout from base tests (#792)
Fixes #793
2023-07-27 15:40:50 +01:00
cyan317
665890d410 Fix connect_timeout request_timeout not work + tsan in RPCServerTestSuite (#790)
Fixes #791
2023-07-27 13:35:52 +01:00
John Freeman
545886561f Fix link to clio branch of rippled (#789) 2023-07-26 21:44:12 +01:00
Alex Kremer
68eec01dbc Fix TSAN issues part1 (#788)
Fixes a few issues from boost 1.82 migration and some Conan misconfigurations
2023-07-26 21:39:39 +01:00
Peter Chen
02621fe02e Add new RPC Handler "version" (#782)
Fixes #726
2023-07-26 20:02:11 +01:00
cyan317
6ad72446d1 Disable xrpl tests (#785) 2023-07-26 19:31:12 +01:00
Arihant Kothari
1d0a43669b Fix noRippleCheck fee field (#786)
Fixes #709
2023-07-26 19:22:17 +01:00
cyan317
71aabc8c29 Nftids (#780)
Fixes #769
2023-07-26 17:12:20 +01:00
cyan317
6b98579bfb Remove try catch for server_info(#781)
Avoid tsan false alert
2023-07-26 12:44:56 +01:00
cyan317
375ac2ffa6 Enable CI MACOS node (#783)
Fixes  #784
2023-07-26 11:39:13 +01:00
Alex Kremer
c6ca650767 Add initial Conan integration (#712)
Fixes #645
2023-07-24 18:43:02 +01:00
cyan317
2336148d0d Fix missing "validated" (#778)
Fixes #779
2023-07-18 10:52:22 +01:00
Shawn Xie
12178abf4d Use mismatch in getNFTokenMintData (#774) 2023-07-17 22:09:15 +01:00
Alex Kremer
b8705ae086 Add time/uptime/amendment_blocked to server_info (#775) 2023-07-14 16:46:10 +01:00
cyan317
b83d7478ef Unsupported Error when request server stream (#772)
Fixes #773
2023-07-14 14:44:40 +01:00
cyan317
4fd6d51d21 Rename WsSession to WsBase (#770) 2023-07-14 13:28:15 +01:00
cyan317
d195bdb66d Change limit tests (#766)
Fixes #771
2023-07-14 13:08:08 +01:00
Alex Kremer
50dbb51627 Implement configuration options for useful cassandra driver opts (#765)
Fixes #764
2023-07-12 15:59:06 +01:00
cyan317
2f369e175c Add "network_id" to server_info (#761)
Fixes #763
2023-07-12 12:09:08 +01:00
cyan317
47e03a7da3 Forward not supported fields (#757)
Fixes #760
2023-07-11 16:49:16 +01:00
cyan317
d7b84a2e7a Missing "tx_hash" for transaction_entry (#758)
Fixes #759
2023-07-11 16:47:47 +01:00
cyan317
e79425bc21 Remove "strict" (#755)
Fixes #756
2023-07-11 13:21:56 +01:00
cyan317
7710468f37 Ledger owner fund (#753)
Fixes #754
2023-07-11 12:36:48 +01:00
Alex Kremer
210d7fdbc8 Use clamp modifier on limit field instead of between validator (#752)
Fixes #751
2023-07-10 17:57:26 +01:00
Alex Kremer
ba8e7188ca Implement the Clamp modifier (#740)
Fixes #750
2023-07-10 16:09:20 +01:00
cyan317
271323b0f4 account_object supports nft page (#736)
Fix #696
2023-07-10 13:42:57 +01:00
cyan317
7b306f3ba0 version2's account_info (#747)
Fixes #743
2023-07-10 13:42:09 +01:00
cyan317
73805d44ad account_tx rpcLGR_IDXS_INVALID adapt to v2 (#749)
Fixes #748
2023-07-10 13:41:11 +01:00
cyan317
f19772907d account_flags (#745)
Fixes #746
2023-07-10 13:01:33 +01:00
Alex Kremer
616f0176c9 Change an error code in account_lines to match rippled (#742)
Fixes #741
2023-07-07 16:50:32 +01:00
Alex Kremer
9f4f5d319e Fix discrepancies in ledger_entry (#739)
Fixes #738
2023-07-07 12:04:59 +01:00
cyan317
dcbc4577c2 Use "invalidParam" when "book_offers" taker format is wrong (#734)
Fix #735
2023-07-05 17:25:17 +01:00
Alex Kremer
f4d8e18bf7 Add deletion_blockers_only support (#737)
Fixes #730
2023-07-05 17:04:08 +01:00
cyan317
b3e001ebfb Remove date from account_tx (#732)
Fixes #733
2023-07-04 15:40:54 +01:00
Alex Kremer
524821c0b0 Add strict field support (#731)
Fixes #729
2023-07-04 15:39:34 +01:00
cyan317
a292a607c2 Implement 'type' for 'ledger_data' (#705)
Fixes #703
2023-07-04 15:26:21 +01:00
Alex Kremer
81894c0a90 Implement deposit_authorized RPC and tests (#728)
Fixes #727
2023-07-04 11:21:41 +01:00
Alex Kremer
0a7def18cd Implement custom HTTP errors (#720)
Fixes #697
2023-07-04 11:02:32 +01:00
cyan317
1e969ba13b Unknown Option (#710)
Fixes #711
2023-07-03 11:02:14 +01:00
cyan317
ef62718a27 Fix max limit for account tx (#723)
Fixes #724
2023-07-03 10:56:43 +01:00
Alex Kremer
aadd9e50f0 Forward api_version 1 requests to rippled (#716)
Fixes #698
2023-06-26 09:52:57 +01:00
cyan317
d9e89746a4 Stop etl when crash (#708)
Fixes #706
2023-06-21 13:10:24 +01:00
cyan317
557ea5d7f6 Remove sensitive info from log (#701)
Fixes #702
2023-06-16 16:50:54 +01:00
cyan317
4cc3b3ec0f Fix account_tx marker issue (#699)
Fixes #700
2023-06-16 12:29:34 +01:00
Alex Kremer
a960471ef4 Support api_version (#695)
Fixes #64
2023-06-16 12:14:30 +01:00
cyan317
871d43c85f Account_tx bug fix (#694)
Fixes #693
2023-06-14 14:57:42 +01:00
cyan317
5ce3fff788 Read only for backend (#691)
Fixes #663
2023-06-14 13:05:37 +01:00
cyan317
a76194d299 Fix clang crash (#690)
Fixes #692
2023-06-13 11:35:16 +01:00
Alex Kremer
14f9f98cf2 Implement basic transformer tests (#689) 2023-06-13 11:16:52 +01:00
cyan317
01e4eed130 Fix marker issue (#687)
Fixes #688
2023-06-12 10:44:42 +01:00
cyan317
893315c50d Fix mismatch when subscribe book offers when both is true
Fixes #677
2023-06-12 10:23:19 +01:00
Alex Kremer
b83d206ced Add more fine grained counters (#683)
Fixes #665
2023-06-09 16:48:55 +01:00
cyan317
9d28e64383 report not support (#682)
Fixes #678 #679
2023-06-09 16:14:19 +01:00
cyan317
b873af2d43 Move the json check to RPC executor (#676)
Fixes #681
2023-06-09 11:35:15 +01:00
cyan317
435db339df Refactor web server (#667)
Fixs #674
2023-06-08 13:25:49 +01:00
cyan317
9836e4ceaf fix (#672)
Fix #673
2023-06-08 13:12:58 +01:00
Alex Kremer
5d2c079f1a Implement extractor tests (#671) 2023-06-07 12:33:46 +01:00
cyan317
244337c5b6 Fix (#669)
Fixes #670
2023-06-05 15:18:36 +01:00
Alex Kremer
b07fbb14dc Refactor ETL into smaller components (#649)
Fixes #666 and #664
2023-06-02 16:12:06 +01:00
Alex Kremer
7e8569b03a Remove old backend implementation (#662)
Fixes #661
2023-06-01 13:54:49 +01:00
cyan317
fc0c93b2ee hot wallet (#656)
Fix #654
2023-05-31 13:09:27 +01:00
cyan317
8ba7388d58 keep same with rippled (#655)
Fix #652
2023-05-31 13:08:31 +01:00
cyan317
0bbb539d0b update the default limit (#646)
Fixes #648
2023-05-25 11:39:08 +01:00
cyan317
c50174235f Fix (#647)
Fixs #609 #610 #611
2023-05-24 15:38:27 +01:00
cyan317
aace437245 Add the missing input check for "diff" field of "ledger" (#643)
Fixes #644
2023-05-17 19:02:55 +01:00
cyan317
2e3e8cd779 Fix mismatch error when owner is in wrong format (#641)
Fixes #642
2023-05-16 14:26:08 +01:00
Alex Kremer
6f93e1003e Address cppcheck issues (#640)
Fixes #639
2023-05-15 17:57:21 +01:00
cyan317
14978ca91d add nft_page (#637)
Fixes #638
2023-05-15 13:00:35 +01:00
Alex Kremer
9adcaeb21b Rename functions to camelCase (#636) 2023-05-15 11:38:48 +01:00
Alex Kremer
d94fe4e7ab Fix nft_history error to match rippled (#635)
Fixes #633
2023-05-15 11:28:24 +01:00
ledhed2222
c8029255ba fix nft uri (#634)
* fix nft uri

* remove some comments too
2023-05-14 04:15:45 -04:00
cyan317
d548d44a61 Fix mismatches (#630)
Fix #632
2023-05-11 17:48:27 +01:00
Alex Kremer
4cae248b5c Fix race condition and ub (#631) 2023-05-10 18:35:04 +01:00
Alex Kremer
b3db4cadab Improve performance of Counters and add unit-test (#629)
Fixes #478
2023-05-09 14:24:12 +01:00
cyan317
02c0a1f11d Add handlers comments (#627)
Fixes #628
2023-05-09 14:02:01 +01:00
cyan317
6bc2ec745f fix bugs (#625)
Fixes #626
2023-05-05 12:55:14 +01:00
Alex Kremer
d7d5d61747 Integrate nextgen RPC into clio (#572)
Fixes #592
2023-05-04 16:15:36 +01:00
cyan317
f1b3a6b511 Use self-hosted mac for CI (#619) 2023-05-04 15:21:46 +01:00
cyan317
f52f36ecbc Add missing expectation in unit test (#622)
Fixes #623
2023-05-03 11:06:37 +01:00
Alex Kremer
860d10cddc Fix issue with retry policy that lead to crashes (#620)
Fixes #621
2023-05-03 11:04:30 +01:00
cyan317
36ac3215e2 Ledger (#604)
Fixes #618
2023-05-02 14:07:26 +01:00
cyan317
7776a5ffb6 Init (#614)
Fixes #617
2023-05-02 13:24:23 +01:00
cyan317
4b2d53fc2f account_objects of new RPC framework (#599)
Fixes #602
2023-04-25 09:14:20 +01:00
cyan317
9a19519550 Account_nfts (#598)
Fixes #601
2023-04-24 14:17:28 +01:00
cyan317
88e25687dc Ledgerdata (#596)
Fixes #600
2023-04-24 09:00:10 +01:00
cyan317
93e2ac529d Unsubscribe (#595)
Fixes #597
2023-04-20 08:54:20 +01:00
cyan317
0bc84fefbf Subscribe handler (#591)
Fixes #593
2023-04-13 14:14:11 +01:00
Alex Kremer
36bb20806e Implement server_info nextgen RPC (#590)
Fixes #587
2023-04-13 11:51:54 +01:00
ledhed2222
dfe974d5ab Add default ordering to issuer_nf_tokens_v2 (#588)
Fixes #589
2023-04-07 23:15:39 +01:00
Alex Kremer
bf65cfabae Fix backend error handling (#586)
Fixes #585
2023-04-06 14:21:08 +01:00
Alex Kremer
f42e024f38 Update git blame ignore file 2023-04-06 11:32:16 +01:00
Alex Kremer
d816ef54ab Reformat codebase with 120 char limit (#583) 2023-04-06 11:24:36 +01:00
Alex Kremer
e60fd3e58e Implement nft_history nextgen handler (#581)
Fixes #580
2023-04-05 14:06:26 +01:00
cyan317
654168efec Create ngContext (#579)
Fixes #582
2023-04-05 12:46:59 +01:00
Alex Kremer
5d06a79f13 Implement nextgen random handler and tests (#576)
Fixes #575
2023-04-04 15:16:02 +01:00
cyan317
3320125d8f Fix compile error on clang14.0.3 (#577)
Fixes #578
2023-04-04 12:55:43 +01:00
cyan317
a1f93b09f7 account_info implementation in new RPC framework (#573)
Fixes #574
2023-04-03 17:03:48 +01:00
Alex Kremer
232acaeff2 Implement nextgen nft_sell_offers handler (#571)
Fixes #570
2023-03-30 12:46:54 +01:00
Alex Kremer
d86104577b Implement new experimental cassandra backend (#537) 2023-03-29 19:38:38 +01:00
cyan317
e9937fab76 account_offer in new RPC framework (#567)
Fixes #569
2023-03-29 16:40:51 +01:00
Alex Kremer
75c2011845 Implement nextgen handler for nft_buy_offers (#568)
Fixes #564
2023-03-29 16:33:48 +01:00
cyan317
5604b37c02 account_tx of new RPC framework (#562)
Fixes #566
2023-03-28 13:21:51 +01:00
cyan317
f604856eab Use JSS string (#563)
#565
2023-03-28 09:07:10 +01:00
cyan317
b69e4350a1 noripple_check implementation of new RPC system (#554)
Fixes #561
2023-03-27 14:17:51 +01:00
Alex Kremer
95da706fed Implement nextgen nft_info handler (#558)
Fixing #557
2023-03-27 11:50:13 +01:00
Alex Kremer
1bb67217e5 Add codecov.io steps (#546)
Fixing #
2023-03-27 10:58:30 +01:00
cyan317
21f1b70daf Fix spawn (#556)
Fixes #559
2023-03-24 14:10:20 +00:00
cyan317
430812abf5 Transaction entry with new RPC framework (#553)
Fixes #555
2023-03-24 12:57:54 +00:00
Alex Kremer
8d5e28ef30 Implement nextgen account_lines handler (#551)
Fixing #550
2023-03-24 12:00:00 +00:00
cyan317
4180d81819 Fix subscription forward issue (#544)
Fixes #552
2023-03-23 13:54:55 +00:00
Alex Kremer
21eeb9ae02 Implement ledger_range rpc handler (#548)
Fixes #549
2023-03-21 14:14:43 +00:00
cyan317
edd2e9dd4b Implement book_offers in new RPC framework (#542)
Fixes #547
2023-03-21 09:12:25 +00:00
ledhed2222
b25ac5d707 Write NFT URIs to nf_token_uris table and pull from it for nft_info API (#313)
Fixes #308
2023-03-20 17:43:31 +00:00
cyan317
9d10cff873 Custom error validator (#540)
Fixes #541
2023-03-15 17:19:57 +00:00
cyan317
bc438ce58a Ledger entry in new RPC framework (#534)
Fixes #539
2023-03-15 13:01:40 +00:00
cyan317
b99a68e55f Gateway balance (#536)
Fixes #538
2023-03-14 14:21:28 +00:00
cyan317
7a819f4955 Gateway balance fix (#535)
Fixes #464
2023-03-08 15:08:20 +00:00
cyan317
6b78b1ad8b Fix ledger_entry bug (#532)
Fixes #533
2023-03-07 09:32:39 +00:00
cyan317
488e28e874 Add IfType requirement to RPC framework (#530)
Fixes #531
2023-03-03 12:25:53 +00:00
cyan317
d26dd5a8cf Fix (#528)
Fixes #529
2023-02-28 15:29:12 +00:00
cyan317
67f0fa26ae Tx handler in new RPC framework (#526)
Fixes #527
2023-02-28 09:35:13 +00:00
cyan317
a3211f4458 Handler account_currencies (#524)
Fixes #525
2023-02-27 09:17:51 +00:00
cyan317
7d4e5ff0bd Account channel (#519)
Fixes #523
2023-02-24 09:34:29 +00:00
cyan317
f6c2008540 Provide coroutine process interface for handler (#521)
Fixes #522
2023-02-23 16:35:01 +00:00
Elliot Lee
d74ca4940b Update CONTRIBUTING.md (#520) 2023-02-22 23:21:39 +00:00
cyan317
739807a7d7 Fix marker issue (#518)
* Fixes #515
2023-02-21 13:48:52 +00:00
Alex Kremer
9fa26be13a Change few loglines severity and channel (#517)
Fix #516
2023-02-20 11:14:05 +00:00
Alex Kremer
f0555af284 Add libfmt (#514)
Fix #513
2023-02-16 15:15:12 +00:00
cyan317
b7fa9b09fe Add common validator (#510)
Fixes #512
2023-02-15 13:54:53 +00:00
Michael Legleux
08f7a7a476 Exit 1 on failed experimental builds to fail build step (#507) 2023-02-14 11:48:13 -08:00
cyan317
703196b013 Fix mac build failure (#509)
Fixes #511
2023-02-14 16:55:42 +00:00
Elliot Lee
284986e7b7 Update CONTRIBUTING.md (#504) 2023-02-10 13:57:47 +00:00
cyan317
09ac1b866e Add ping handler (#503)
Fix #506
2023-02-08 16:20:24 +00:00
cyan317
4112cc42df Fix backend test fail (#502)
Fix #505
2023-02-08 10:21:19 +00:00
Alex Kremer
c07e04ce84 Document RPC framework (#501)
Fixes #500
2023-02-03 12:07:51 +00:00
cyan317
19455b4d6c Add Unittests for subscription module (#488)
Fix #492
2023-02-03 09:07:02 +00:00
Alex Kremer
1186622e58 Improve sweephandler test flakiness (#499)
Fixes #498
2023-02-02 15:56:00 +00:00
Alex Kremer
023e02da15 Implement base for nextgen rpc subsystem (#487)
Fixes #494
2023-02-02 13:16:01 +00:00
cyan317
8dbf049a71 Adjust DosGuard default cfg (#496)
Fix #497
2023-02-02 09:04:00 +00:00
cyan317
fe5150dba4 Run test on mac (#490)
Fixes #490
2023-01-31 17:22:28 +00:00
Francis Mendoza
992d5a7a70 [FOLDED] Eliminate remaining bypass and add comment on rare edge case where it's necessary (#298) (#484) 2023-01-25 13:49:28 -08:00
cyan317
b702b6e14e Fix clio-server link issue (#485)
Fix #486
2023-01-25 17:57:18 +00:00
cyan317
557c76233a update gitignore and readme (#481)
Fixes #482
2023-01-19 15:22:29 +00:00
cyan317
6ba9903a37 add code coverage job (#477)
install gcovr

exclude src file and adjust folder

revert other jobs

fix format issue

add cov info to lib

update lib

add lib
2023-01-19 13:15:18 +00:00
Alex Kremer
81bf9894e4 Fix bug with ClioVersion to prevent crash at runtime (#473)
Fixes #474
2023-01-16 16:42:57 +00:00
cyan317
047d64983c add coverage for clang (#472)
add report target for CI to parse

Extract the coverage function

format

format

add last line
2023-01-16 15:38:26 +00:00
Alex Kremer
1708b929b8 Demote couple errors to warning/info in ETLSource (#471)
Fixes #468
2023-01-16 11:16:20 +00:00
cyan317
a377514287 Replace unique_lock with scoped_lock (#467)
Fixes #466
2023-01-10 17:50:53 +00:00
Michael Legleux
c51d696181 Write Clio version file from template (#457)
* Set build version from git

* disallow untagged commits to master

* remove clang-format ingore around versionString
2023-01-09 09:36:33 -08:00
cyan317
1a9d328f94 Add requests limit to DosGuard (#462)
Fixing #448
2023-01-06 19:06:33 +00:00
Francis Mendoza
3b1dc60f63 Change error message to match rippled (#463)
Fixes #263
2023-01-06 17:38:47 +00:00
Francis Mendoza
0c2ca1737e Match format to rippled error code (#461)
Fixes #263
2023-01-04 20:53:37 +00:00
cyan317
2f65a26dc7 Add time measurement profiler (#458)
Rebase
2022-12-20 18:57:47 +00:00
Michael Legleux
37c765a072 Build macOS and Ubuntu 22.04 (#456)
build release/x.y.z branches
2022-12-19 17:37:38 -08:00
Alex Kremer
29f1f860d8 Add unit tests for DOSGuard (#453)
Fixes #452
2022-12-19 17:24:02 +00:00
CJ Cobb
414a416938 Document dos_guard in example config. Log when client surpasses rate limit (#451) 2022-12-16 12:53:28 -05:00
Alex Kremer
1a4180f678 Update readme with more log configurations (#447)
Fixes #446
2022-12-13 19:17:41 +00:00
Michael Legleux
bca086d776 Increase file descriptor limit (#449) 2022-12-13 19:17:10 +00:00
Alex Kremer
f81086f40c Add copyright to top of each source file (#444)
Fixes #411
2022-12-12 21:11:01 +00:00
Francis Mendoza
962fb12410 Update README and example config to describe start_sequence (#438)
Fixes #250
2022-12-12 19:16:46 +00:00
Alex Kremer
10af787324 Fix gateway balances to match rippled output (#441)
Fixes #271
2022-12-12 19:03:51 +00:00
Francis Mendoza
5f32bbbd81 Update documentation and config with ssl_cert_file and ssl_key_file (#443)
Fixes #424
2022-12-12 19:01:43 +00:00
Alex Kremer
fa78d4e783 Implement cli parsing using boost::po (#436)
Fixes #367
2022-12-09 21:21:19 +00:00
Michael Legleux
05b03b2086 Remove branch name from version string (#437)
Fixes a bug from #430
2022-12-07 20:57:49 +00:00
Alex Kremer
866b1d32b3 Fix malformed output format over ws rpc (#426)
Fixes #405
2022-12-07 19:20:21 +00:00
CJ Cobb
8a1f00debb add connection counting (#433) 2022-12-07 13:12:38 -05:00
Michael Legleux
3ec5755930 Implement always adding git ref to version string (#430)
Fixes #427
2022-12-06 16:23:33 +00:00
Alex Kremer
a0d173feb8 Fix source_location issue on MacOSX and Debug build (#431)
Fixes #428
2022-12-06 15:53:14 +00:00
Alex Kremer
b0f678411c Return srcCurMalformed on invalid taker_pays in book_offers (#413)
Fixes #267
2022-11-30 15:51:09 +00:00
Alex Kremer
1369eaeef6 Add custom error for malformed request (#414)
Fixes #276
2022-11-30 15:02:43 +00:00
Alexander Kremer
bf217345ae Update headers to use #pragma once 2022-11-23 14:26:16 -08:00
Francis Mendoza
7bb567761c Return lgrIdxsInvalid error for ledger_max_index less than ledger_min_index (#339)
Fixes #263
2022-11-23 21:38:13 +00:00
Alex Kremer
4b94ed3e55 Use custom malformedAddress error in ledger_entry (#419)
Fixes #272
2022-11-22 22:18:10 +00:00
Alex Kremer
75c0d22f87 Add custom error for malformed owner and request (#417)
Fixes #274
2022-11-22 22:05:03 +00:00
Alex Kremer
9803e86158 Add closed to header for all paths of ledger_data (#416)
Fixes #219
2022-11-22 22:03:49 +00:00
CJ Cobb
0f7e1d5517 helper function for subscribe to ensure cleanup (#402) 2022-11-22 13:39:14 -05:00
CJ Cobb
cf7a6ecc89 include searched_all in error response of tx (#407) 2022-11-21 15:52:59 -06:00
Michael Legleux
5c9dce0f8a Remove the github action package signing step
This will be done elsewhere.
2022-11-20 22:46:40 -08:00
Alex Kremer
041aba9a0b Implement account ownership check and fix paging (#383)
Fixes #222
2022-11-18 17:51:18 +00:00
Alexander Kremer
b13c44eb12 Fix pre-commit to only check staged files 2022-11-18 09:24:43 -08:00
Alex Kremer
a47bf2e8fe Implement logging abstraction (#371)
Fixes #290
2022-11-17 22:02:16 +00:00
manojsdoshi
4b8dd7b981 Merging 1.0.3 to develop 2022-11-17 12:08:09 -08:00
Michael Legleux
d2c870db92 Set version to 1.0.3 2022-11-17 11:06:35 -08:00
Michael Legleux
8e17039586 Build Clio with CentOS 7 2022-11-17 11:02:54 -08:00
Francis Mendoza
25067c97ed Add rpcNOT_SUPPORTED due to Clio disparity with Rippled (#360)
Mitigates #280
2022-11-17 17:02:49 +00:00
manojsdoshi
1310e5dde9 Set version to 1.0.3-rc1 2022-11-16 11:54:09 -05:00
Alex Kremer
0a5bf911c1 Add error code extension mechanism and use malformed currency code (#396)
Fixes #275
2022-11-15 17:08:09 +00:00
Francis Mendoza
6015faa0d3 Return srcIsrMalformed for taker_gets issuer in book_offers (#266)
Fixes #266
2022-11-14 21:05:26 +00:00
Francis Mendoza
e68fd3251a Use rpcBAD_ISSUER for empty taker in subscribe cmd (#352)
Fixes #352
2022-11-14 20:50:42 +00:00
Francis Mendoza
c13ac79552 Use rpcINVALID_PARAMETERS for invalid ledger_index in ledger command (#279)
Fixes #279
2022-11-14 20:42:50 +00:00
CJ Cobb
b1299792a6 Better handle markers in nft_buy_offers and nft_sell_offers (#400) 2022-11-14 14:59:12 -05:00
CJ Cobb
2cbf09d6ae handle invalidHotWallet in gateway_balances (#384) 2022-11-14 13:21:18 -05:00
CJ Cobb
42cf55fd0e remove accountFromSeed (#399) 2022-11-14 13:20:42 -05:00
Francis Mendoza
e825be24cc Add Doxyfile and comments for BackendInterface.h (#307)
Fixes #285
2022-11-11 12:26:44 +00:00
Francis Mendoza
997742b555 Add limit in four additional files (#328)
Fixes #221
2022-11-10 13:47:13 +00:00
Alex Kremer
031ad411a6 Add clang-format git hook (#395)
Fixes #392
2022-11-09 21:31:49 +00:00
CJ Cobb
486f1f2fd2 return error on negative limit (#394) 2022-11-08 14:41:18 -05:00
Alex Kremer
739dd81981 Return correct error on subscription to non-existing stream (#390)
Fixes #353
2022-11-08 14:42:02 +00:00
CJ Cobb
1f900fcf7f Return account malformed error from account_tx when account is malformed (#319) 2022-11-08 09:29:28 -05:00
CJ Cobb
fc68664b02 put peers in correct spot in example config (#376) 2022-11-08 09:29:04 -05:00
CJ Cobb
ffa5c58b32 Return actNotFound for non-existent account in traverseOwnedNodes (#382) 2022-11-08 09:28:43 -05:00
Alex Kremer
4bf3a228dc Port ignore_default support for account_lines rpc (#391)
Fixes #375
2022-11-08 14:13:13 +00:00
Alex Kremer
9091bb06f4 Remove checks for a valid subscription in subscribe/unsubscribe rpc to match rippled (#386)
Fixes #348
2022-11-08 14:11:41 +00:00
Alex Kremer
41e3176c56 Implement a simple check to suppress 'validated' flag output (#393)
Fixes #354
2022-11-08 14:11:05 +00:00
Alex Kremer
bedca85c78 Add checks for empty array in accounts/accounts_proposed subscriptions (#387)
Fixes #347
2022-11-08 14:10:07 +00:00
Alex Kremer
39157f8be4 Return account malformed error for invalid accounts (#388)
Fixes #349
2022-11-08 14:09:29 +00:00
Alex Kremer
3affda8b13 Add offers to the response regardless of it being empty (#389)
Fixes #351
2022-11-08 14:08:52 +00:00
Alex Kremer
8cc2de5643 Fix nft_sell_offers/nft_buy_offers limit and marker correctness (#342)
Fixes #335
2022-11-02 19:20:48 +00:00
Shawn
9b74b3f898 Rename NFT offer index to "nft_offer_index" (#377)
Fixes #380
2022-11-01 17:27:58 +00:00
Alex Kremer
ea2837749a Implement an abstraction for the config (#358)
Fixes #321
2022-11-01 16:59:23 +00:00
CJ Cobb
8bd8ab9b8a Fix bug on cache download from peer when ledger not found (#370) 2022-10-26 22:30:39 -04:00
Francis Mendoza
dc89d23e5a Return badMarket for same currency in taker_gets and taker_pays in book_offers (#357)
Fixes #269
2022-10-26 19:15:38 +01:00
Francis Mendoza
734c7a5c36 Return malformedOwner for deposit_preauth.owner in ledger_entry (#345)
Fixes #273
2022-10-26 19:14:40 +01:00
Francis Mendoza
b17ef28f55 Return malformedOwner in ticket.owner for ledger_entry (#344)
Fixes #274
2022-10-26 19:14:02 +01:00
Francis Mendoza
e56bd7b29e Add malformedAddress in conditionals (#343)
Fixes #272
2022-10-26 19:13:00 +01:00
Alex Kremer
5bf334e5f7 Remove postgres support from clio (#327)
Fixes #310
2022-10-04 18:00:37 +01:00
CJ Cobb
97ef66d130 Allow server to download cache from another clio server (#246)
* Allow server to download cache from another clio server

* Config takes an array of clio peers. If any of these peers have a
  full cache, clio picks a peer at random to download the cache from.
  Otherwise, fall back to downloading cache from the database.
2022-10-04 12:29:29 -04:00
Francis Mendoza
4c9c606202 Don't return marker in account_tx when past user specified window (#282) 2022-10-04 10:35:38 -04:00
Francis Mendoza
a885551006 Add rpcDST_ISR_MALFORMED to taker_gets conditionals (#341) 2022-10-04 10:30:49 -04:00
Francis Mendoza
fae1ec0c8d Add LimitRange to noripple_check (#324) 2022-10-04 10:04:06 -04:00
Michael Legleux
de23f015d6 Mark package release's version string (#317) 2022-10-04 10:02:32 -04:00
Francis Mendoza
37f9493d15 Add rpcSRC_CUR_MALFORMED to badTakerPaysCurrency and rpcDST_AMT_MALFORMED to badTakerGetsCurrency (#268) (#333) 2022-10-03 15:29:28 -04:00
Francis Mendoza
49387059ef Add rpcLGR_IDX_MALFORMED error messages to ledger sequence min and max out of range conditionals (#336) 2022-10-03 15:28:21 -04:00
Alex Kremer
744af4b639 Implement unique taging of incoming requests (#311)
Fixes #212
2022-09-29 21:56:29 +01:00
CJ Cobb
db2b9dac3b Throw error if server bind or listen fails (#309)
* Throw error if server bind or listen fails
2022-09-29 16:07:33 -04:00
Alex Kremer
ccf73dc68c Fix ProbingETL toJson to serialize underlying source states (#325)
Fixes #323
2022-09-28 00:30:56 +01:00
Alex Kremer
3de421c390 Remove useless mutex from BackendInterface and its usage from CassandraBackend (#326)
Fixes #304
2022-09-28 00:28:18 +01:00
Alex Kremer
d4a9560c3f Implement subscription for book_changes (#315)
Fixes #315
2022-09-27 00:20:53 +01:00
Michael Legleux
983aa29271 Build Clio with CentOS 7 2022-09-26 15:46:43 -07:00
CJ Cobb
0ebe92de68 add work queue output to server_info (#322) 2022-09-26 14:51:39 -05:00
CJ Cobb
eb1ea28e27 Database read throttle (#242)
Track current outstanding read requests to the database. When the configured limit is exceeded, reject new RPCs and return rpcTOO_BUSY
2022-09-23 15:43:03 -05:00
ledhed2222
1764f3524e add nft_history and mark certain APIs as clio-only to improve error (#255) 2022-09-15 21:11:29 -04:00
ledhed2222
777ae24f62 Fix issue with assigning values to NFT offers API responses (#301) 2022-09-13 15:54:55 -04:00
Alex Kremer
1ada879072 Probing ETL Source (#292)
* Implement a probing ETL source and do not require SSL certs for SslETLSource (#251)

Fixes #251
2022-09-12 23:32:13 +01:00
Alex Kremer
e2792f5a0c Fix compiler warnings (#306) 2022-09-12 21:35:30 +01:00
Alex Kremer
97c431680a Add 20 second timeout for ETLSource websocket (#297)
Fixes #289
2022-09-12 16:09:46 +01:00
Alex Kremer
0b454a2316 Implement book_changes RPC (#300)
* Port book_changes RPC call from rippled
* Refactor for readability and modern cpp
2022-09-09 18:08:11 +01:00
CJ Cobb
b7cae53fcd cleanup README and example config (#247)
* Indicate defaults for logging parameters
* Remove log_to_file from example config
* Remove online_delete from example config
2022-09-07 18:28:32 -04:00
CJ Cobb
ac45cce5bd insert delivered_amount based on close time (#252) 2022-09-07 18:28:07 -04:00
Michael Legleux
ef39c04e1e timeout for tests (#257) 2022-09-07 18:27:45 -04:00
CJ Cobb
83a099a547 Fix bug where some ledgers are not being published (#281)
* The ledger close time can occasionally be a few seconds in the future,
  which causes ETL to not publish the ledger, because the age
  calculation wraps around and the age is computed as a very large
  unsigned integer. This fix rounds to zero when the age would be
  negative
2022-09-07 16:17:42 -04:00
Alex Kremer
73337d0819 Add CONTRIBUTING documentation (#296)
Fixes #293
2022-09-06 22:30:12 +01:00
CJ Cobb
816625c44e set grpc max message size to unlimited (#249) 2022-08-23 09:30:18 -04:00
ethanlabelle
48e87d7c07 added cache hit rate to server info (#220) 2022-08-15 10:20:45 -05:00
CJ Cobb
dfe18ed682 Update version to 1.0.2 (#245) 2022-08-11 14:35:49 -04:00
Mwni
92a072d7a8 Add README section for database administration
Add remark about Scyllas default memory reservation behavior.
2022-08-11 13:10:23 -04:00
CJ Cobb
24fca61b56 update rippled to 1.9.2 (#228)
* patch rippled to build with c++20
2022-08-10 17:09:56 -04:00
Michael Legleux
ae8303fdc8 Guard for GCC < 11 and update readme (#243) 2022-08-10 15:02:44 -04:00
CJ Cobb
709a8463b8 server_info improvements (#240)
* only return counters and etl info if client is localhost
* move cache and etl info inside info
2022-08-10 15:02:31 -04:00
CJ Cobb
84d31986d1 config file improvements (#241)
* remove log_to_file param
* change the place of workers
2022-08-10 11:30:43 -04:00
Brandon Kong
d50f229631 Fixed warning message to be XRPL standard compliant (#229)
All warnings now contain Warning Objects, which have ID, Message, and Details as fields
2022-08-04 13:21:55 -04:00
Michael Legleux
379c89fb02 Change branches jobs run on
Run gha on "release" branch also
Restrict signing to release branches
2022-07-29 13:36:20 -07:00
CJ Cobb
81f7171368 wrap atomics in shared_ptr for cache download (#230) 2022-07-29 10:56:08 -04:00
Michael Legleux
629b35d1dd Sign clio packages 2022-07-28 23:02:11 -07:00
Brandon Kong
6fc4cee195 Updated backend README.md with the latest Cassandra schemas (#170)
* Updated backend README.md with the latest Cassandra schemas
2022-07-27 12:31:51 -04:00
CJ Cobb
b01813ac3d change id to object_id in diff response to ledger command (#218) 2022-07-26 14:08:54 -05:00
ledhed2222
6bf8c5bc4e Add NFT-specific data stores and add nft_info API (#98) 2022-07-26 15:01:14 -04:00
CJ Cobb
2ffd98f895 Fine tune cache download (#215)
* Fine tune cache download

* Allow operators to specify the max number of concurrent markers. The
  software generates possible markers from ledger diffs, as before, but
  only processes a specified number at one time, which caps database
  reads and distributes the load more evenly over the entire download.
* Allow operators to specify the page fetch size during the cache
  download, which is the number of ledger objects to fetch per marker at
  one time.

* Refactor full ledger dump in test.py
2022-07-26 15:00:27 -04:00
CJ Cobb
3edead32ba remove assert in fetchLedgerPage (#227) 2022-07-26 14:35:59 -04:00
Nathan Nichols
28980734ae ensure lgrInfo is in context.range (#226) 2022-07-26 14:35:48 -04:00
ethanlabelle
ce60c8f64d moved warnings array out of result JSON (#208) 2022-07-26 13:39:27 -04:00
Brandon Kong
39ef2ae33c Fixed 503 response code (#214)
The rate limiting warning response of Clio now follows the XRPL standard.
2022-07-26 13:39:09 -04:00
Nathan Nichols
d83975e750 report ledger when no marker exists in ledger_data (#203) 2022-07-15 13:25:46 -05:00
CJ Cobb
4468302852 Set version to 1.0.1 (#216) 2022-07-13 19:33:03 -04:00
Nathan Nichols
a704cf7cfe remove "this software is in a beta version" from readme (#204)
* remove "this software is in a beta version" from readme

Co-authored-by: Michael Legleux <legleux@users.noreply.github.com>
2022-07-10 20:05:13 -05:00
CJ Cobb
05d09cc352 Only fetch validated ledgers 2022-07-08 12:10:12 -04:00
ethanlabelle
ae96ac7baf removed unused LayeredCache (#199) 2022-06-29 16:10:15 -07:00
ethanlabelle
4579fa2f26 Use ledger close times for stale data warning (#194) 2022-06-29 16:10:03 -07:00
363 changed files with 60322 additions and 20798 deletions

View File

@@ -34,7 +34,7 @@ BreakBeforeBinaryOperators: false
BreakBeforeBraces: Custom
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: true
ColumnLimit: 80
ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:'
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
build/

View File

@@ -7,3 +7,4 @@
# clang-format
e41150248a97e4bdc1cf21b54650c4bb7c63928e
2e542e7b0d94451a933c88778461cc8d3d7e6417
d816ef54abd8e8e979b9c795bdb657a8d18f5e95

20
.githooks/ensure_release_tag Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
# Pushing a release branch requires an annotated tag at the released commit
branch=$(git rev-parse --abbrev-ref HEAD)
if [[ $branch =~ master ]]; then
# check if HEAD commit is tagged
if ! git describe --exact-match HEAD; then
echo "Commits to master must be tagged"
exit 1
fi
elif [[ $branch =~ release/* ]]; then
IFS=/ read -r branch rel_ver <<< ${branch}
tag=$(git describe --tags --abbrev=0)
if [[ "${rel_ver}" != "${tag}" ]]; then
echo "release/${rel_ver} branches must have annotated tag ${rel_ver}"
echo "git tag -am\"${rel_ver}\" ${rel_ver}"
exit 1
fi
fi

27
.githooks/pre-commit Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
exec 1>&2
# paths to check and re-format
sources="src unittests"
formatter="clang-format-11 -i"
first=$(git diff $sources)
find $sources -type f \( -name '*.cpp' -o -name '*.h' -o -name '*.ipp' \) -print0 | xargs -0 $formatter
second=$(git diff $sources)
changes=$(diff <(echo "$first") <(echo "$second") | wc -l | sed -e 's/^[[:space:]]*//')
if [ "$changes" != "0" ]; then
cat <<\EOF
WARNING
-----------------------------------------------------------------------------
Automatically re-formatted code with `clang-format` - commit was aborted.
Please manually add any updated files and commit again.
-----------------------------------------------------------------------------
EOF
exit 1
fi
.githooks/ensure_release_tag

13
.github/actions/lint/action.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
runs:
using: composite
steps:
# Github's ubuntu-20.04 image already has clang-format-11 installed
- run: |
find src unittests -type f \( -name '*.cpp' -o -name '*.h' -o -name '*.ipp' \) -print0 | xargs -0 clang-format-11 -i
shell: bash
- name: Check for differences
id: assert
shell: bash
run: |
git diff --color --exit-code | tee "clang-format.patch"

20
.github/actions/linux_build/build.sh vendored Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
conan profile new default --detect
conan profile update settings.compiler.cppstd=20 default
conan profile update settings.compiler.libcxx=libstdc++11 default
conan remote add --insert 0 conan-non-prod http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod
cd rippled
conan export external/snappy snappy/1.1.10@
conan export external/soci soci/4.0.3@
conan export .
conan install --output-folder build_rippled -install-folder build_rippled --build missing --settings build_type=Release
cmake -B build_rippled -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
cmake --build build_rippled --target xrpl_core --parallel $(($(nproc) - 2))
cd ..
conan export external/cassandra
conan install . -if build_clio -of build_clio --build missing --settings build_type=Release -o tests=True
cmake -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release -B build_clio
cmake --build build_clio --parallel $(($(nproc) - 2))

6
.github/actions/test/Dockerfile vendored Normal file
View File

@@ -0,0 +1,6 @@
FROM cassandra:4.0.4
RUN apt-get update && apt-get install -y postgresql
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

8
.github/actions/test/entrypoint.sh vendored Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
pg_ctlcluster 12 main start
su postgres -c"psql -c\"alter user postgres with password 'postgres'\""
su cassandra -c "/opt/cassandra/bin/cassandra -R"
sleep 90
chmod +x ./clio_tests
./clio_tests

View File

@@ -1,9 +1,9 @@
name: Build Clio
on:
push:
branches: [master, develop, develop-next]
branches: [master, release/*, develop]
pull_request:
branches: [master, develop, develop-next]
branches: [master, release/*, develop]
workflow_dispatch:
jobs:
@@ -11,59 +11,105 @@ jobs:
name: Lint
runs-on: ubuntu-20.04
steps:
- name: Get source
uses: actions/checkout@v3
- uses: actions/checkout@v3
- name: Run clang-format
uses: XRPLF/clio-gha/lint@main
uses: ./.github/actions/lint
build_clio:
name: Build
runs-on: [self-hosted, Linux]
build_mac:
name: Build macOS
needs: lint
continue-on-error: true
runs-on: [self-hosted, macOS]
steps:
- name: Clone Clio repo
uses: actions/checkout@v3
- uses: actions/checkout@v3
with:
path: clio_src
path: clio
- name: Clone Clio CI repo
uses: actions/checkout@v3
- name: List conan artifactory
run: |
conan search
conan remote list
if [[ $(conan remote list |grep conan-non-prod| wc -c) -ne 0 ]]; then
echo "conan-non-prod is available"
else
echo "adding conan-non-prod"
conan remote add conan-non-prod http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod
fi
- name: Install dependencies
run: |
brew install llvm@14 pkg-config ninja bison cmake
- name: Setup environment for llvm-14
run: |
export PATH="/usr/local/opt/llvm@14/bin:$PATH"
export LDFLAGS="-L/usr/local/opt/llvm@14/lib -L/usr/local/opt/llvm@14/lib/c++ -Wl,-rpath,/usr/local/opt/llvm@14/lib/c++"
export CPPFLAGS="-I/usr/local/opt/llvm@14/include"
- name: Build Clio
run: |
cd clio
mkdir -p build
cd build
conan install .. -of . -b missing -s build_type=Release -o clio:tests=True
cmake -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build . --parallel $(($(sysctl -n hw.logicalcpu) - 2))
- name: Upload clio_tests
uses: actions/upload-artifact@v3
with:
path: clio_ci
repository: 'XRPLF/clio-ci'
name: clio_tests_mac
path: ./clio/build/clio_tests
- name: Clone GitHub actions repo
uses: actions/checkout@v3
with:
repository: XRPLF/clio-gha
path: gha # must be the same as defined in XRPLF/clio-gha
- name: Build
uses: XRPLF/clio-gha/build@main
- name: Artifact clio_tests
uses: actions/upload-artifact@v2
with:
name: clio_tests
path: clio_tests
- name: Artifact Debian package
uses: actions/upload-artifact@v2
with:
name: deb_package-${{ github.sha }}
path: clio_ci/build/*.deb
test_clio:
name: Test Clio
build_linux:
name: Build linux
needs: lint
continue-on-error: true
runs-on: [self-hosted, Linux]
needs: build_clio
container:
image: conanio/gcc11:1.60.2
options: --user root
steps:
- name: Get clio_tests artifact
uses: actions/download-artifact@v3
with:
name: clio_tests
- name: Get Clio
uses: actions/checkout@v3
- name: Run tests
uses: XRPLF/clio-gha/test@main
- name: Get rippled
uses: actions/checkout@v3
with:
repository: thejohnfreeman/rippled
ref: clio
path: rippled
- name: Build Clio
run: |
./.github/actions/linux_build/build.sh
- name: Upload clio_tests
uses: actions/upload-artifact@v3
with:
name: clio_tests_linux
path: ./build_clio/clio_tests
test_mac:
needs: build_mac
runs-on: [self-hosted, macOS]
steps:
- uses: actions/download-artifact@v3
with:
name: clio_tests_mac
- name: Run clio_tests
run: |
chmod +x ./clio_tests
./clio_tests --gtest_filter="-BackendCassandraBaseTest*:BackendCassandraTest*:BackendCassandraFactoryTestWithDB*"
test_linux:
needs: build_linux
runs-on: [self-hosted, x-heavy]
steps:
- uses: actions/download-artifact@v3
with:
name: clio_tests_linux
- name: Run clio_tests
run: |
chmod +x ./clio_tests
./clio_tests --gtest_filter="-BackendCassandraBaseTest*:BackendCassandraTest*:BackendCassandraFactoryTestWithDB*"

7
.gitignore vendored
View File

@@ -1,3 +1,8 @@
*clio*.log
build/
build*/
.build
.vscode
.python-version
CMakeUserPresets.json
config.json
src/main/impl/Build.cpp

39
CMake/Build.cpp.in Normal file
View File

@@ -0,0 +1,39 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2022, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <main/Build.h>
namespace Build {
static constexpr char versionString[] = "@VERSION@";
std::string const&
getClioVersionString()
{
static std::string const value = versionString;
return value;
}
std::string const&
getClioFullVersionString()
{
static std::string const value = "clio-" + getClioVersionString();
return value;
}
} // namespace Build

42
CMake/CheckCompiler.cmake Normal file
View File

@@ -0,0 +1,42 @@
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 14)
message (FATAL_ERROR "Clang 14+ required for building clio")
endif ()
set (is_clang TRUE)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 14)
message (FATAL_ERROR "AppleClang 14+ required for building clio")
endif ()
set (is_appleclang TRUE)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS 11)
message (FATAL_ERROR "GCC 11+ required for building clio")
endif ()
set (is_gcc TRUE)
else ()
message (FATAL_ERROR "Supported compilers: AppleClang 14+, Clang 14+, GCC 11+")
endif ()
if (san)
string (TOLOWER ${san} san)
set (SAN_FLAG "-fsanitize=${san}")
set (SAN_LIB "")
if (is_gcc)
if (san STREQUAL "address")
set (SAN_LIB "asan")
elseif (san STREQUAL "thread")
set (SAN_LIB "tsan")
elseif (san STREQUAL "memory")
set (SAN_LIB "msan")
elseif (san STREQUAL "undefined")
set (SAN_LIB "ubsan")
endif ()
endif ()
set (_saved_CRL ${CMAKE_REQUIRED_LIBRARIES})
set (CMAKE_REQUIRED_LIBRARIES "${SAN_FLAG};${SAN_LIB}")
CHECK_CXX_COMPILER_FLAG (${SAN_FLAG} COMPILER_SUPPORTS_SAN)
set (CMAKE_REQUIRED_LIBRARIES ${_saved_CRL})
if (NOT COMPILER_SUPPORTS_SAN)
message (FATAL_ERROR "${san} sanitizer does not seem to be supported by your compiler")
endif ()
endif ()

View File

@@ -1,15 +1,33 @@
#[===================================================================[
read version from source
write version to source
#]===================================================================]
file (STRINGS src/main/impl/Build.cpp BUILD_INFO)
foreach (line_ ${BUILD_INFO})
if (line_ MATCHES "versionString[ ]*=[ ]*\"(.+)\"")
set (clio_version ${CMAKE_MATCH_1})
endif ()
endforeach ()
if (clio_version)
message (STATUS "clio version: ${clio_version}")
else ()
message (FATAL_ERROR "unable to determine clio version")
find_package (Git REQUIRED)
set (GIT_COMMAND rev-parse --short HEAD)
execute_process (COMMAND ${GIT_EXECUTABLE} ${GIT_COMMAND} OUTPUT_VARIABLE REV OUTPUT_STRIP_TRAILING_WHITESPACE)
set (GIT_COMMAND branch --show-current)
execute_process (COMMAND ${GIT_EXECUTABLE} ${GIT_COMMAND} OUTPUT_VARIABLE BRANCH OUTPUT_STRIP_TRAILING_WHITESPACE)
if (BRANCH STREQUAL "")
set (BRANCH "dev")
endif ()
if (NOT (BRANCH MATCHES master OR BRANCH MATCHES release/*)) # for develop and any other branch name YYYYMMDDHMS-<branch>-<git-ref>
execute_process (COMMAND date +%Y%m%d%H%M%S OUTPUT_VARIABLE DATE OUTPUT_STRIP_TRAILING_WHITESPACE)
set (VERSION "${DATE}-${BRANCH}-${REV}")
else ()
set (GIT_COMMAND describe --tags)
execute_process (COMMAND ${GIT_EXECUTABLE} ${GIT_COMMAND} OUTPUT_VARIABLE TAG_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE)
set (VERSION "${TAG_VERSION}-${REV}")
endif ()
if (CMAKE_BUILD_TYPE MATCHES Debug)
set (VERSION "${VERSION}+DEBUG")
endif ()
message (STATUS "Build version: ${VERSION}")
set (clio_version "${VERSION}")
configure_file (CMake/Build.cpp.in ${CMAKE_SOURCE_DIR}/src/main/impl/Build.cpp)

125
CMake/Coverage.cmake Normal file
View File

@@ -0,0 +1,125 @@
# call add_coverage(module_name) to add coverage targets for the given module
function (add_coverage module)
if ("${CMAKE_C_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang"
OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang")
message ("[Coverage] Building with llvm Code Coverage Tools")
# Using llvm gcov ; llvm install by xcode
set (LLVM_COV_PATH /Library/Developer/CommandLineTools/usr/bin)
if (NOT EXISTS ${LLVM_COV_PATH}/llvm-cov)
message (FATAL_ERROR "llvm-cov not found! Aborting.")
endif ()
# set Flags
target_compile_options (${module} PRIVATE
-fprofile-instr-generate
-fcoverage-mapping)
target_link_options (${module} PUBLIC
-fprofile-instr-generate
-fcoverage-mapping)
target_compile_options (clio PRIVATE
-fprofile-instr-generate
-fcoverage-mapping)
target_link_options (clio PUBLIC
-fprofile-instr-generate
-fcoverage-mapping)
# llvm-cov
add_custom_target (${module}-ccov-preprocessing
COMMAND LLVM_PROFILE_FILE=${module}.profraw $<TARGET_FILE:${module}>
COMMAND ${LLVM_COV_PATH}/llvm-profdata merge -sparse ${module}.profraw -o
${module}.profdata
DEPENDS ${module})
add_custom_target (${module}-ccov-show
COMMAND ${LLVM_COV_PATH}/llvm-cov show $<TARGET_FILE:${module}>
-instr-profile=${module}.profdata -show-line-counts-or-regions
DEPENDS ${module}-ccov-preprocessing)
# add summary for CI parse
add_custom_target (${module}-ccov-report
COMMAND
${LLVM_COV_PATH}/llvm-cov report $<TARGET_FILE:${module}>
-instr-profile=${module}.profdata
-ignore-filename-regex=".*_makefiles|.*unittests|.*_deps"
-show-region-summary=false
DEPENDS ${module}-ccov-preprocessing)
# exclude libs and unittests self
add_custom_target (${module}-ccov
COMMAND
${LLVM_COV_PATH}/llvm-cov show $<TARGET_FILE:${module}>
-instr-profile=${module}.profdata -show-line-counts-or-regions
-output-dir=${module}-llvm-cov -format="html"
-ignore-filename-regex=".*_makefiles|.*unittests|.*_deps" > /dev/null 2>&1
DEPENDS ${module}-ccov-preprocessing)
add_custom_command (
TARGET ${module}-ccov
POST_BUILD
COMMENT
"Open ${module}-llvm-cov/index.html in your browser to view the coverage report."
)
elseif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU")
message ("[Coverage] Building with Gcc Code Coverage Tools")
find_program (GCOV_PATH gcov)
if (NOT GCOV_PATH)
message (FATAL_ERROR "gcov not found! Aborting...")
endif () # NOT GCOV_PATH
find_program (GCOVR_PATH gcovr)
if (NOT GCOVR_PATH)
message (FATAL_ERROR "gcovr not found! Aborting...")
endif () # NOT GCOVR_PATH
set (COV_OUTPUT_PATH ${module}-gcc-cov)
target_compile_options (${module} PRIVATE -fprofile-arcs -ftest-coverage
-fPIC)
target_link_libraries (${module} PRIVATE gcov)
target_compile_options (clio PRIVATE -fprofile-arcs -ftest-coverage
-fPIC)
target_link_libraries (clio PRIVATE gcov)
# this target is used for CI as well generate the summary out.xml will send
# to github action to generate markdown, we can paste it to comments or
# readme
add_custom_target (${module}-ccov
COMMAND ${module} ${TEST_PARAMETER}
COMMAND rm -rf ${COV_OUTPUT_PATH}
COMMAND mkdir ${COV_OUTPUT_PATH}
COMMAND
gcovr -r ${CMAKE_SOURCE_DIR} --object-directory=${PROJECT_BINARY_DIR} -x
${COV_OUTPUT_PATH}/out.xml --exclude='${CMAKE_SOURCE_DIR}/unittests/'
--exclude='${PROJECT_BINARY_DIR}/'
COMMAND
gcovr -r ${CMAKE_SOURCE_DIR} --object-directory=${PROJECT_BINARY_DIR}
--html ${COV_OUTPUT_PATH}/report.html
--exclude='${CMAKE_SOURCE_DIR}/unittests/'
--exclude='${PROJECT_BINARY_DIR}/'
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
COMMENT "Running gcovr to produce Cobertura code coverage report.")
# generate the detail report
add_custom_target (${module}-ccov-report
COMMAND ${module} ${TEST_PARAMETER}
COMMAND rm -rf ${COV_OUTPUT_PATH}
COMMAND mkdir ${COV_OUTPUT_PATH}
COMMAND
gcovr -r ${CMAKE_SOURCE_DIR} --object-directory=${PROJECT_BINARY_DIR}
--html-details ${COV_OUTPUT_PATH}/index.html
--exclude='${CMAKE_SOURCE_DIR}/unittests/'
--exclude='${PROJECT_BINARY_DIR}/'
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
COMMENT "Running gcovr to produce Cobertura code coverage report.")
add_custom_command (
TARGET ${module}-ccov-report
POST_BUILD
COMMENT
"Open ${COV_OUTPUT_PATH}/index.html in your browser to view the coverage report."
)
else ()
message (FATAL_ERROR "Complier not support yet")
endif ()
endfunction ()

11
CMake/Docs.cmake Normal file
View File

@@ -0,0 +1,11 @@
find_package (Doxygen REQUIRED)
set (DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile)
set (DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
configure_file (${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY)
add_custom_target (docs
COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Generating API documentation with Doxygen"
VERBATIM)

7
CMake/Settings.cmake Normal file
View File

@@ -0,0 +1,7 @@
target_compile_options (clio PUBLIC
-Wall
-Werror
-Wno-narrowing
-Wno-deprecated-declarations
-Wno-dangling-else
-Wno-unused-but-set-variable)

View File

@@ -0,0 +1,11 @@
include (CheckIncludeFileCXX)
check_include_file_cxx ("source_location" SOURCE_LOCATION_AVAILABLE)
if (SOURCE_LOCATION_AVAILABLE)
target_compile_definitions (clio PUBLIC "HAS_SOURCE_LOCATION")
endif ()
check_include_file_cxx ("experimental/source_location" EXPERIMENTAL_SOURCE_LOCATION_AVAILABLE)
if (EXPERIMENTAL_SOURCE_LOCATION_AVAILABLE)
target_compile_definitions (clio PUBLIC "HAS_EXPERIMENTAL_SOURCE_LOCATION")
endif ()

View File

@@ -1,6 +1,11 @@
set(Boost_USE_STATIC_LIBS ON)
set(Boost_USE_STATIC_RUNTIME ON)
set (Boost_USE_STATIC_LIBS ON)
set (Boost_USE_STATIC_RUNTIME ON)
find_package(Boost 1.75 COMPONENTS filesystem log_setup log thread system REQUIRED)
target_link_libraries(clio PUBLIC ${Boost_LIBRARIES})
find_package (Boost 1.82 REQUIRED
COMPONENTS
program_options
coroutine
system
log
log_setup
)

5
CMake/deps/OpenSSL.cmake Normal file
View File

@@ -0,0 +1,5 @@
find_package (OpenSSL 1.1.1 REQUIRED)
set_target_properties (OpenSSL::SSL PROPERTIES
INTERFACE_COMPILE_DEFINITIONS OPENSSL_NO_SSL2
)

View File

@@ -1,31 +0,0 @@
set(POSTGRES_INSTALL_DIR ${CMAKE_BINARY_DIR}/postgres)
set(POSTGRES_LIBS pq pgcommon pgport)
ExternalProject_Add(postgres
GIT_REPOSITORY https://github.com/postgres/postgres.git
GIT_TAG REL_14_1
GIT_SHALLOW 1
LOG_CONFIGURE 1
LOG_BUILD 1
CONFIGURE_COMMAND ./configure --prefix ${POSTGRES_INSTALL_DIR} --without-readline --verbose
BUILD_COMMAND ${CMAKE_COMMAND} -E env --unset=MAKELEVEL make VERBOSE=${CMAKE_VERBOSE_MAKEFILE} -j32
BUILD_IN_SOURCE 1
INSTALL_COMMAND ${CMAKE_COMMAND} -E env make -s --no-print-directory install
UPDATE_COMMAND ""
BUILD_BYPRODUCTS
${POSTGRES_INSTALL_DIR}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}pq${CMAKE_STATIC_LIBRARY_SUFFIX}}
${POSTGRES_INSTALL_DIR}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}pgcommon${CMAKE_STATIC_LIBRARY_SUFFIX}}
${POSTGRES_INSTALL_DIR}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}pgport${CMAKE_STATIC_LIBRARY_SUFFIX}}
)
ExternalProject_Get_Property (postgres BINARY_DIR)
foreach(_lib ${POSTGRES_LIBS})
add_library(${_lib} STATIC IMPORTED GLOBAL)
add_dependencies(${_lib} postgres)
set_target_properties(${_lib} PROPERTIES
IMPORTED_LOCATION ${POSTGRES_INSTALL_DIR}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}${_lib}.a)
set_target_properties(${_lib} PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${POSTGRES_INSTALL_DIR}/include)
target_link_libraries(clio PUBLIC ${POSTGRES_INSTALL_DIR}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}${_lib}${CMAKE_STATIC_LIBRARY_SUFFIX})
endforeach()
add_dependencies(clio postgres)
target_include_directories(clio PUBLIC ${POSTGRES_INSTALL_DIR}/include)

2
CMake/deps/Threads.cmake Normal file
View File

@@ -0,0 +1,2 @@
set (THREADS_PREFER_PTHREAD_FLAG ON)
find_package (Threads)

View File

@@ -1,151 +1 @@
find_package(ZLIB REQUIRED)
find_library(cassandra NAMES cassandra)
if(NOT cassandra)
message("System installed Cassandra cpp driver not found. Will build")
find_library(zlib NAMES zlib1g-dev zlib-devel zlib z)
if(NOT zlib)
message("zlib not found. will build")
add_library(zlib STATIC IMPORTED GLOBAL)
ExternalProject_Add(zlib_src
PREFIX ${nih_cache_path}
GIT_REPOSITORY https://github.com/madler/zlib.git
GIT_TAG v1.2.12
INSTALL_COMMAND ""
BUILD_BYPRODUCTS <BINARY_DIR>/${CMAKE_STATIC_LIBRARY_PREFIX}z.a
)
ExternalProject_Get_Property (zlib_src SOURCE_DIR)
ExternalProject_Get_Property (zlib_src BINARY_DIR)
set (zlib_src_SOURCE_DIR "${SOURCE_DIR}")
file (MAKE_DIRECTORY ${zlib_src_SOURCE_DIR}/include)
set_target_properties (zlib PROPERTIES
IMPORTED_LOCATION
${BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}z.a
INTERFACE_INCLUDE_DIRECTORIES
${SOURCE_DIR}/include)
add_dependencies(zlib zlib_src)
file(TO_CMAKE_PATH "${zlib_src_SOURCE_DIR}" zlib_src_SOURCE_DIR)
endif()
find_library(krb5 NAMES krb5-dev libkrb5-dev)
if(NOT krb5)
message("krb5 not found. will build")
add_library(krb5 STATIC IMPORTED GLOBAL)
ExternalProject_Add(krb5_src
PREFIX ${nih_cache_path}
GIT_REPOSITORY https://github.com/krb5/krb5.git
GIT_TAG krb5-1.20
UPDATE_COMMAND ""
CONFIGURE_COMMAND autoreconf src && CFLAGS=-fcommon ./src/configure --enable-static --disable-shared
BUILD_IN_SOURCE 1
BUILD_COMMAND make
INSTALL_COMMAND ""
BUILD_BYPRODUCTS <SOURCE_DIR>/lib/${CMAKE_STATIC_LIBRARY_PREFIX}krb5.a
)
message(${ep_lib_prefix}/krb5.a)
message(${CMAKE_STATIC_LIBRARY_PREFIX}krb5.a)
ExternalProject_Get_Property (krb5_src SOURCE_DIR)
ExternalProject_Get_Property (krb5_src BINARY_DIR)
set (krb5_src_SOURCE_DIR "${SOURCE_DIR}")
file (MAKE_DIRECTORY ${krb5_src_SOURCE_DIR}/include)
set_target_properties (krb5 PROPERTIES
IMPORTED_LOCATION
${SOURCE_DIR}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}krb5.a
INTERFACE_INCLUDE_DIRECTORIES
${SOURCE_DIR}/include)
add_dependencies(krb5 krb5_src)
file(TO_CMAKE_PATH "${krb5_src_SOURCE_DIR}" krb5_src_SOURCE_DIR)
endif()
find_library(libuv1 NAMES uv1 libuv1 liubuv1-dev libuv1:amd64)
if(NOT libuv1)
message("libuv1 not found, will build")
add_library(libuv1 STATIC IMPORTED GLOBAL)
ExternalProject_Add(libuv_src
PREFIX ${nih_cache_path}
GIT_REPOSITORY https://github.com/libuv/libuv.git
GIT_TAG v1.44.1
INSTALL_COMMAND ""
BUILD_BYPRODUCTS <BINARY_DIR>/${CMAKE_STATIC_LIBRARY_PREFIX}uv_a.a
)
ExternalProject_Get_Property (libuv_src SOURCE_DIR)
ExternalProject_Get_Property (libuv_src BINARY_DIR)
set (libuv_src_SOURCE_DIR "${SOURCE_DIR}")
file (MAKE_DIRECTORY ${libuv_src_SOURCE_DIR}/include)
set_target_properties (libuv1 PROPERTIES
IMPORTED_LOCATION
${BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}uv_a.a
INTERFACE_INCLUDE_DIRECTORIES
${SOURCE_DIR}/include)
add_dependencies(libuv1 libuv_src)
file(TO_CMAKE_PATH "${libuv_src_SOURCE_DIR}" libuv_src_SOURCE_DIR)
endif()
add_library (cassandra STATIC IMPORTED GLOBAL)
ExternalProject_Add(cassandra_src
PREFIX ${nih_cache_path}
GIT_REPOSITORY https://github.com/datastax/cpp-driver.git
GIT_TAG 2.16.2
CMAKE_ARGS
-DLIBUV_ROOT_DIR=${BINARY_DIR}
-DLIBUV_INCLUDE_DIR=${SOURCE_DIR}/include
-DCASS_BUILD_STATIC=ON
-DCASS_BUILD_SHARED=OFF
INSTALL_COMMAND ""
BUILD_BYPRODUCTS <BINARY_DIR>/${CMAKE_STATIC_LIBRARY_PREFIX}cassandra_static.a
)
ExternalProject_Get_Property (cassandra_src SOURCE_DIR)
ExternalProject_Get_Property (cassandra_src BINARY_DIR)
set (cassandra_src_SOURCE_DIR "${SOURCE_DIR}")
file (MAKE_DIRECTORY ${cassandra_src_SOURCE_DIR}/include)
set_target_properties (cassandra PROPERTIES
IMPORTED_LOCATION
${BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}cassandra_static.a
INTERFACE_INCLUDE_DIRECTORIES
${SOURCE_DIR}/include)
message("cass dirs")
message(${BINARY_DIR})
message(${SOURCE_DIR})
message(${BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}cassandra_static.a)
add_dependencies(cassandra cassandra_src)
if(NOT libuv1)
ExternalProject_Add_StepDependencies(cassandra_src build libuv1)
target_link_libraries(cassandra INTERFACE libuv1)
else()
target_link_libraries(cassandra INTERFACE ${libuv1})
endif()
if(NOT krb5)
ExternalProject_Add_StepDependencies(cassandra_src build krb5)
target_link_libraries(cassandra INTERFACE krb5)
else()
target_link_libraries(cassandra INTERFACE ${krb5})
endif()
if(NOT zlib)
ExternalProject_Add_StepDependencies(cassandra_src build zlib)
target_link_libraries(cassandra INTERFACE zlib)
else()
target_link_libraries(cassandra INTERFACE ${zlib})
endif()
set(OPENSSL_USE_STATIC_LIBS TRUE)
find_package(OpenSSL REQUIRED)
target_link_libraries(cassandra INTERFACE OpenSSL::SSL)
file(TO_CMAKE_PATH "${cassandra_src_SOURCE_DIR}" cassandra_src_SOURCE_DIR)
target_link_libraries(clio PUBLIC cassandra)
else()
message("Found system installed cassandra cpp driver")
message(${cassandra})
find_path(cassandra_includes NAMES cassandra.h REQUIRED)
target_link_libraries (clio PUBLIC ${cassandra})
target_include_directories(clio INTERFACE ${cassandra_includes})
endif()
find_package (cassandra-cpp-driver REQUIRED)

View File

@@ -1,19 +1,4 @@
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip
)
find_package (GTest REQUIRED)
FetchContent_GetProperties(googletest)
if(NOT googletest_POPULATED)
FetchContent_Populate(googletest)
add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR} EXCLUDE_FROM_ALL)
endif()
target_link_libraries(clio_tests PUBLIC clio gtest_main)
enable_testing()
include(GoogleTest)
gtest_discover_tests(clio_tests)
enable_testing ()
include (GoogleTest)

1
CMake/deps/libfmt.cmake Normal file
View File

@@ -0,0 +1 @@
find_package (fmt REQUIRED)

1
CMake/deps/libxrpl.cmake Normal file
View File

@@ -0,0 +1 @@
find_package (xrpl REQUIRED)

View File

@@ -1,18 +0,0 @@
set(RIPPLED_REPO "https://github.com/ripple/rippled.git")
set(RIPPLED_BRANCH "1.9.0")
set(NIH_CACHE_ROOT "${CMAKE_CURRENT_BINARY_DIR}" CACHE INTERNAL "")
message(STATUS "Cloning ${RIPPLED_REPO} branch ${RIPPLED_BRANCH}")
FetchContent_Declare(rippled
GIT_REPOSITORY "${RIPPLED_REPO}"
GIT_TAG "${RIPPLED_BRANCH}"
GIT_SHALLOW ON
)
FetchContent_GetProperties(rippled)
if(NOT rippled_POPULATED)
FetchContent_Populate(rippled)
add_subdirectory(${rippled_SOURCE_DIR} ${rippled_BINARY_DIR} EXCLUDE_FROM_ALL)
endif()
target_link_libraries(clio PUBLIC xrpl_core grpc_pbufs)
target_include_directories(clio PUBLIC ${rippled_SOURCE_DIR}/src ) # TODO: Seems like this shouldn't be needed?

View File

@@ -11,6 +11,7 @@ ExecStart=@CLIO_INSTALL_DIR@/bin/clio_server @CLIO_INSTALL_DIR@/etc/config.json
Restart=on-failure
User=clio
Group=clio
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target

View File

@@ -1,16 +1,14 @@
set(CLIO_INSTALL_DIR "/opt/clio")
set(CMAKE_INSTALL_PREFIX ${CLIO_INSTALL_DIR})
set (CLIO_INSTALL_DIR "/opt/clio")
set (CMAKE_INSTALL_PREFIX ${CLIO_INSTALL_DIR})
install(TARGETS clio_server DESTINATION bin)
# install(TARGETS clio_tests DESTINATION bin) # NOTE: Do we want to install the tests?
install (TARGETS clio_server DESTINATION bin)
#install(FILES example-config.json DESTINATION etc RENAME config.json)
file(READ example-config.json config)
string(REGEX REPLACE "./clio_log" "/var/log/clio/" config "${config}")
file(WRITE ${CMAKE_BINARY_DIR}/install-config.json "${config}")
install(FILES ${CMAKE_BINARY_DIR}/install-config.json DESTINATION etc RENAME config.json)
file (READ example-config.json config)
string (REGEX REPLACE "./clio_log" "/var/log/clio/" config "${config}")
file (WRITE ${CMAKE_BINARY_DIR}/install-config.json "${config}")
install (FILES ${CMAKE_BINARY_DIR}/install-config.json DESTINATION etc RENAME config.json)
configure_file("${CMAKE_SOURCE_DIR}/CMake/install/clio.service.in" "${CMAKE_BINARY_DIR}/clio.service")
configure_file ("${CMAKE_SOURCE_DIR}/CMake/install/clio.service.in" "${CMAKE_BINARY_DIR}/clio.service")
install(FILES "${CMAKE_BINARY_DIR}/clio.service" DESTINATION /lib/systemd/system)
install (FILES "${CMAKE_BINARY_DIR}/clio.service" DESTINATION /lib/systemd/system)

View File

@@ -1 +0,0 @@
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-narrowing -Wall -Werror -Wno-dangling-else")

View File

@@ -1,106 +1,263 @@
cmake_minimum_required(VERSION 3.16.3)
project(clio)
option(BUILD_TESTS "Build tests" TRUE)
# ========================================================================== #
# Options #
# ========================================================================== #
option (verbose "Verbose build" FALSE)
option (tests "Build tests" FALSE)
option (docs "Generate doxygen docs" FALSE)
option (coverage "Build test coverage report" FALSE)
option (packaging "Create distribution packages" FALSE)
# ========================================================================== #
set (san "" CACHE STRING "Add sanitizer instrumentation")
set_property (CACHE san PROPERTY STRINGS ";undefined;memory;address;thread")
# ========================================================================== #
option(VERBOSE "Verbose build" TRUE)
if(VERBOSE)
set(CMAKE_VERBOSE_MAKEFILE TRUE)
set(FETCHCONTENT_QUIET FALSE CACHE STRING "Verbose FetchContent()")
endif()
# Include required modules
include (CheckCXXCompilerFlag)
if(NOT GIT_COMMIT_HASH)
if(VERBOSE)
message(WARNING "GIT_COMMIT_HASH not provided...looking for git")
endif()
find_package(Git)
if(Git_FOUND)
execute_process(COMMAND ${GIT_EXECUTABLE} describe --always --abbrev=8
OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE gch)
if(gch)
set(GIT_COMMIT_HASH "${gch}")
message(STATUS "Git commit: ${GIT_COMMIT_HASH}")
add_definitions(-DCLIO_GIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
endif()
endif()
endif() #git
if (verbose)
set (CMAKE_VERBOSE_MAKEFILE TRUE)
endif ()
add_library(clio)
target_compile_features(clio PUBLIC cxx_std_20)
target_include_directories(clio PUBLIC src)
if (packaging)
add_definitions (-DPKG=1)
endif ()
include(FetchContent)
include(ExternalProject)
include(CMake/settings.cmake)
include(CMake/ClioVersion.cmake)
include(CMake/deps/rippled.cmake)
include(CMake/deps/Boost.cmake)
include(CMake/deps/cassandra.cmake)
include(CMake/deps/Postgres.cmake)
add_library (clio)
target_sources(clio PRIVATE
# Clio tweaks and checks
include (CMake/CheckCompiler.cmake)
include (CMake/Settings.cmake)
include (CMake/ClioVersion.cmake)
include (CMake/SourceLocation.cmake)
# Clio deps
include (CMake/deps/libxrpl.cmake)
include (CMake/deps/Boost.cmake)
include (CMake/deps/OpenSSL.cmake)
include (CMake/deps/Threads.cmake)
include (CMake/deps/libfmt.cmake)
include (CMake/deps/cassandra.cmake)
# TODO: Include directory will be wrong when installed.
target_include_directories (clio PUBLIC src)
target_compile_features (clio PUBLIC cxx_std_20)
target_link_libraries (clio
PUBLIC Boost::boost
PUBLIC Boost::coroutine
PUBLIC Boost::program_options
PUBLIC Boost::system
PUBLIC Boost::log
PUBLIC Boost::log_setup
PUBLIC cassandra-cpp-driver::cassandra-cpp-driver
PUBLIC fmt::fmt
PUBLIC OpenSSL::Crypto
PUBLIC OpenSSL::SSL
PUBLIC xrpl::libxrpl
INTERFACE Threads::Threads
)
if (is_gcc)
# FIXME: needed on gcc for now
target_compile_definitions (clio PUBLIC BOOST_ASIO_DISABLE_CONCEPTS)
endif ()
target_sources (clio PRIVATE
## Main
src/main/impl/Build.cpp
## Backend
src/backend/BackendInterface.cpp
src/backend/CassandraBackend.cpp
src/backend/LayeredCache.cpp
src/backend/Pg.cpp
src/backend/PostgresBackend.cpp
src/backend/SimpleCache.cpp
src/data/BackendInterface.cpp
src/data/LedgerCache.cpp
src/data/cassandra/impl/Future.cpp
src/data/cassandra/impl/Cluster.cpp
src/data/cassandra/impl/Batch.cpp
src/data/cassandra/impl/Result.cpp
src/data/cassandra/impl/Tuple.cpp
src/data/cassandra/impl/SslContext.cpp
src/data/cassandra/Handle.cpp
src/data/cassandra/SettingsProvider.cpp
## ETL
src/etl/ETLSource.cpp
src/etl/ReportingETL.cpp
## Subscriptions
src/subscriptions/SubscriptionManager.cpp
src/etl/Source.cpp
src/etl/ProbingSource.cpp
src/etl/NFTHelpers.cpp
src/etl/ETLService.cpp
src/etl/LoadBalancer.cpp
src/etl/impl/ForwardCache.cpp
## Feed
src/feed/SubscriptionManager.cpp
## Web
src/web/IntervalSweepHandler.cpp
## RPC
src/rpc/RPC.cpp
src/rpc/Errors.cpp
src/rpc/Factories.cpp
src/rpc/RPCHelpers.cpp
src/rpc/Counters.cpp
src/rpc/WorkQueue.cpp
## RPC Methods
# Account
src/rpc/common/Specs.cpp
src/rpc/common/Validators.cpp
src/rpc/common/MetaProcessors.cpp
src/rpc/common/impl/APIVersionParser.cpp
src/rpc/common/impl/HandlerProvider.cpp
## RPC handlers
src/rpc/handlers/AccountChannels.cpp
src/rpc/handlers/AccountCurrencies.cpp
src/rpc/handlers/AccountInfo.cpp
src/rpc/handlers/AccountLines.cpp
src/rpc/handlers/AccountOffers.cpp
src/rpc/handlers/AccountNFTs.cpp
src/rpc/handlers/AccountObjects.cpp
src/rpc/handlers/AccountOffers.cpp
src/rpc/handlers/AccountTx.cpp
src/rpc/handlers/BookChanges.cpp
src/rpc/handlers/BookOffers.cpp
src/rpc/handlers/DepositAuthorized.cpp
src/rpc/handlers/GatewayBalances.cpp
src/rpc/handlers/NoRippleCheck.cpp
# Ledger
src/rpc/handlers/Ledger.cpp
src/rpc/handlers/LedgerData.cpp
src/rpc/handlers/LedgerEntry.cpp
src/rpc/handlers/LedgerRange.cpp
# Transaction
src/rpc/handlers/Tx.cpp
src/rpc/handlers/NFTBuyOffers.cpp
src/rpc/handlers/NFTHistory.cpp
src/rpc/handlers/NFTInfo.cpp
src/rpc/handlers/NFTOffersCommon.cpp
src/rpc/handlers/NFTSellOffers.cpp
src/rpc/handlers/NoRippleCheck.cpp
src/rpc/handlers/Random.cpp
src/rpc/handlers/TransactionEntry.cpp
src/rpc/handlers/AccountTx.cpp
# Dex
src/rpc/handlers/BookOffers.cpp
# NFT
src/rpc/handlers/NFTOffers.cpp
# Payment Channel
src/rpc/handlers/ChannelAuthorize.cpp
src/rpc/handlers/ChannelVerify.cpp
# Subscribe
src/rpc/handlers/Subscribe.cpp
# Server
src/rpc/handlers/ServerInfo.cpp
# Utility
src/rpc/handlers/Random.cpp)
src/rpc/handlers/Tx.cpp
## Util
src/util/config/Config.cpp
src/util/log/Logger.cpp
src/util/Taggable.cpp)
add_executable(clio_server src/main/main.cpp)
target_link_libraries(clio_server PUBLIC clio)
# Clio server
add_executable (clio_server src/main/Main.cpp)
target_link_libraries (clio_server PUBLIC clio)
if(BUILD_TESTS)
add_executable(clio_tests unittests/main.cpp)
include(CMake/deps/gtest.cmake)
endif()
# Unittesting
if (tests)
set (TEST_TARGET clio_tests)
add_executable (${TEST_TARGET}
# Common
unittests/Main.cpp
unittests/Playground.cpp
unittests/LoggerTests.cpp
unittests/ConfigTests.cpp
unittests/ProfilerTests.cpp
unittests/JsonUtilTests.cpp
unittests/DOSGuardTests.cpp
unittests/SubscriptionTests.cpp
unittests/SubscriptionManagerTests.cpp
unittests/util/TestObject.cpp
unittests/util/StringUtils.cpp
# ETL
unittests/etl/ExtractionDataPipeTests.cpp
unittests/etl/ExtractorTests.cpp
unittests/etl/TransformerTests.cpp
unittests/etl/CacheLoaderTests.cpp
# RPC
unittests/rpc/ErrorTests.cpp
unittests/rpc/BaseTests.cpp
unittests/rpc/RPCHelpersTests.cpp
unittests/rpc/CountersTests.cpp
unittests/rpc/AdminVerificationTests.cpp
unittests/rpc/APIVersionTests.cpp
unittests/rpc/ForwardingProxyTests.cpp
unittests/rpc/WorkQueueTests.cpp
unittests/rpc/AmendmentsTests.cpp
## RPC handlers
unittests/rpc/handlers/DefaultProcessorTests.cpp
unittests/rpc/handlers/TestHandlerTests.cpp
unittests/rpc/handlers/AccountCurrenciesTests.cpp
unittests/rpc/handlers/AccountLinesTests.cpp
unittests/rpc/handlers/AccountTxTests.cpp
unittests/rpc/handlers/AccountOffersTests.cpp
unittests/rpc/handlers/AccountInfoTests.cpp
unittests/rpc/handlers/AccountChannelsTests.cpp
unittests/rpc/handlers/AccountNFTsTests.cpp
unittests/rpc/handlers/BookOffersTests.cpp
unittests/rpc/handlers/DepositAuthorizedTests.cpp
unittests/rpc/handlers/GatewayBalancesTests.cpp
unittests/rpc/handlers/TxTests.cpp
unittests/rpc/handlers/TransactionEntryTests.cpp
unittests/rpc/handlers/LedgerEntryTests.cpp
unittests/rpc/handlers/LedgerRangeTests.cpp
unittests/rpc/handlers/NoRippleCheckTests.cpp
unittests/rpc/handlers/ServerInfoTests.cpp
unittests/rpc/handlers/PingTests.cpp
unittests/rpc/handlers/RandomTests.cpp
unittests/rpc/handlers/NFTInfoTests.cpp
unittests/rpc/handlers/NFTBuyOffersTests.cpp
unittests/rpc/handlers/NFTSellOffersTests.cpp
unittests/rpc/handlers/NFTHistoryTests.cpp
unittests/rpc/handlers/SubscribeTests.cpp
unittests/rpc/handlers/UnsubscribeTests.cpp
unittests/rpc/handlers/LedgerDataTests.cpp
unittests/rpc/handlers/AccountObjectsTests.cpp
unittests/rpc/handlers/BookChangesTests.cpp
unittests/rpc/handlers/LedgerTests.cpp
unittests/rpc/handlers/VersionHandlerTests.cpp
# Backend
unittests/data/BackendFactoryTests.cpp
unittests/data/cassandra/BaseTests.cpp
unittests/data/cassandra/BackendTests.cpp
unittests/data/cassandra/RetryPolicyTests.cpp
unittests/data/cassandra/SettingsProviderTests.cpp
unittests/data/cassandra/ExecutionStrategyTests.cpp
unittests/data/cassandra/AsyncExecutorTests.cpp
# Webserver
unittests/web/ServerTests.cpp
unittests/web/RPCServerHandlerTests.cpp
unittests/web/WhitelistHandlerTests.cpp
unittests/web/SweepHandlerTests.cpp)
include(CMake/install/install.cmake)
if(PACKAGING)
include(CMake/packaging.cmake)
endif()
include (CMake/deps/gtest.cmake)
# See https://github.com/google/googletest/issues/3475
gtest_discover_tests (clio_tests DISCOVERY_TIMEOUT 10)
# Fix for dwarf5 bug on ci
target_compile_options (clio PUBLIC -gdwarf-4)
target_compile_definitions (${TEST_TARGET} PUBLIC UNITTEST_BUILD)
target_include_directories (${TEST_TARGET} PRIVATE unittests)
target_link_libraries (${TEST_TARGET} PUBLIC clio gtest::gtest)
# Generate `clio_tests-ccov` if coverage is enabled
# Note: use `make clio_tests-ccov` to generate report
if (coverage)
include (CMake/Coverage.cmake)
add_coverage (${TEST_TARGET})
endif ()
endif ()
# Enable selected sanitizer if enabled via `san`
if (san)
target_compile_options (clio
PUBLIC
# Sanitizers recommend minimum of -O1 for reasonable performance
$<$<CONFIG:Debug>:-O1>
${SAN_FLAG}
-fno-omit-frame-pointer)
target_compile_definitions (clio
PUBLIC
$<$<STREQUAL:${san},address>:SANITIZER=ASAN>
$<$<STREQUAL:${san},thread>:SANITIZER=TSAN>
$<$<STREQUAL:${san},memory>:SANITIZER=MSAN>
$<$<STREQUAL:${san},undefined>:SANITIZER=UBSAN>)
target_link_libraries (clio INTERFACE ${SAN_FLAG} ${SAN_LIB})
endif ()
# Generate `docs` target for doxygen documentation if enabled
# Note: use `make docs` to generate the documentation
if (docs)
include (CMake/Docs.cmake)
endif ()
include (CMake/install/install.cmake)
if (packaging)
include (CMake/packaging.cmake) # This file exists only in build runner
endif ()

134
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,134 @@
# Contributing
Thank you for your interest in contributing to the `clio` project 🙏
To contribute, please:
1. Fork the repository under your own user.
2. Create a new branch on which to commit/push your changes.
3. Write and test your code.
4. Ensure that your code compiles with the provided build engine and update the provided build engine as part of your PR where needed and where appropriate.
5. Where applicable, write test cases for your code and include those in `unittests`.
6. Ensure your code passes automated checks (e.g. clang-format)
7. Squash your commits (i.e. rebase) into as few commits as is reasonable to describe your changes at a high level (typically a single commit for a small change). See below for more details.
8. Open a PR to the main repository onto the _develop_ branch, and follow the provided template.
> **Note:** Please read the [Style guide](#style-guide).
## Install git hooks
Please run the following command in order to use git hooks that are helpful for `clio` development.
``` bash
git config --local core.hooksPath .githooks
```
## Git commands
This sections offers a detailed look at the git commands you will need to use to get your PR submitted.
Please note that there are more than one way to do this and these commands are provided for your convenience.
At this point it's assumed that you have already finished working on your feature/bug.
> **Important:** Before you issue any of the commands below, please hit the `Sync fork` button and make sure your fork's `develop` branch is up-to-date with the main `clio` repository.
``` bash
# Create a backup of your branch
git branch <your feature branch>_bk
# Rebase and squash commits into one
git checkout develop
git pull origin develop
git checkout <your feature branch>
git rebase -i develop
```
For each commit in the list other than the first one, enter `s` to squash.
After this is done, you will have the opportunity to write a message for the squashed commit.
> **Hint:** Please use **imperative mood** in the commit message, and capitalize the first word.
``` bash
# You should now have a single commit on top of a commit in `develop`
git log
```
> **Note:** If there are merge conflicts, please resolve them now.
``` bash
# Use the same commit message as you did above
git commit -m 'Your message'
git rebase --continue
```
> **Important:** If you have no GPG keys set up, please follow [this tutorial](https://docs.github.com/en/authentication/managing-commit-signature-verification/adding-a-gpg-key-to-your-github-account)
``` bash
# Sign the commit with your GPG key, and push your changes
git commit --amend -S
git push --force
```
## Fixing issues found during code review
While your code is in review, it's possible that some changes will be requested by reviewer(s).
This section describes the process of adding your fixes.
We assume that you already made the required changes on your feature branch.
``` bash
# Add the changed code
git add <paths to add>
# Add a [FOLD] commit message (so you remember to squash it later)
# while also signing it with your GPG key
git commit -S -m "[FOLD] Your commit message"
# And finally push your changes
git push
```
## After code review
When your PR is approved and ready to merge, use `Squash and merge`.
The button for that is near the bottom of the PR's page on GitHub.
> **Important:** Please leave the automatically-generated mention/link to the PR in the subject line **and** in the description field add `"Fix #ISSUE_ID"` (replacing `ISSUE_ID` with yours) if the PR fixes an issue.
> **Note:** See [issues](https://github.com/XRPLF/clio/issues) to find the `ISSUE_ID` for the feature/bug you were working on.
# Style guide
This is a non-exhaustive list of recommended style guidelines. These are not always strictly enforced and serve as a way to keep the codebase coherent.
## Formatting
Code must conform to `clang-format` version 10, unless the result would be unreasonably difficult to read or maintain.
To change your code to conform use `clang-format -i <your changed files>`.
## Avoid
* Proliferation of nearly identical code.
* Proliferation of new files and classes unless it improves readability or/and compilation time.
* Unmanaged memory allocation and raw pointers.
* Macros (unless they add significant value.)
* Lambda patterns (unless these add significant value.)
* CPU or architecture-specific code unless there is a good reason to include it, and where it is used guard it with macros and provide explanatory comments.
* Importing new libraries unless there is a very good reason to do so.
## Seek to
* Extend functionality of existing code rather than creating new code.
* Prefer readability over terseness where important logic is concerned.
* Inline functions that are not used or are not likely to be used elsewhere in the codebase.
* Use clear and self-explanatory names for functions, variables, structs and classes.
* Use TitleCase for classes, structs and filenames, camelCase for function and variable names, lower case for namespaces and folders.
* Provide as many comments as you feel that a competent programmer would need to understand what your code does.
# Maintainers
Maintainers are ecosystem participants with elevated access to the repository. They are able to push new code, make decisions on when a release should be made, etc.
## Code Review
A PR must be reviewed and approved by at least one of the maintainers before it can be merged.
## Adding and Removing
New maintainers can be proposed by two existing maintainers, subject to a vote by a quorum of the existing maintainers. A minimum of 50% support and a 50% participation is required. In the event of a tie vote, the addition of the new maintainer will be rejected.
Existing maintainers can resign, or be subject to a vote for removal at the behest of two existing maintainers. A minimum of 60% agreement and 50% participation are required. The XRP Ledger Foundation will have the ability, for cause, to remove an existing maintainer without a vote.
## Existing Maintainers
* [cindyyan317](https://github.com/cindyyan317) (Ripple)
* [godexsoft](https://github.com/godexsoft) (Ripple)
* [legleux](https://github.com/legleux) (Ripple)
## Honorable ex-Maintainers
* [cjcobb23](https://github.com/cjcobb23) (ex-Ripple)
* [natenichols](https://github.com/natenichols) (ex-Ripple)

16
Doxyfile Normal file
View File

@@ -0,0 +1,16 @@
PROJECT_NAME = "Clio"
INPUT = ../src ../unittests
EXCLUDE_PATTERNS = *Test*.cpp *Test*.h
RECURSIVE = YES
HAVE_DOT = YES
QUIET = YES
WARNINGS = NO
WARN_NO_PARAMDOC = NO
WARN_IF_INCOMPLETE_DOC = NO
WARN_IF_UNDOCUMENTED = NO
GENERATE_LATEX = NO
GENERATE_HTML = YES
SORT_MEMBERS_CTORS_1ST = YES

215
README.md
View File

@@ -1,55 +1,102 @@
[![Build Clio](https://github.com/legleux/clio/actions/workflows/build.yml/badge.svg?branch=run-tests)](https://github.com/legleux/clio/actions/workflows/build.yml)
**Status:** This software is in beta mode. We encourage anyone to try it out and
report any issues they discover. Version 1.0 coming soon.
# Clio
Clio is an XRP Ledger API server. Clio is optimized for RPC calls, over WebSocket or JSON-RPC. Validated
historical ledger and transaction data are stored in a more space-efficient format,
Clio is an XRP Ledger API server. Clio is optimized for RPC calls, over WebSocket or JSON-RPC.
Validated historical ledger and transaction data are stored in a more space-efficient format,
using up to 4 times less space than rippled. Clio can be configured to store data in Apache Cassandra or ScyllaDB,
allowing for scalable read throughput. Multiple Clio nodes can share
access to the same dataset, allowing for a highly available cluster of Clio nodes,
without the need for redundant data storage or computation.
allowing for scalable read throughput. Multiple Clio nodes can share access to the same dataset,
allowing for a highly available cluster of Clio nodes, without the need for redundant data storage or computation.
Clio offers the full rippled API, with the caveat that Clio by default only returns validated data.
This means that `ledger_index` defaults to `validated` instead of `current` for all requests.
Other non-validated data is also not returned, such as information about queued transactions.
For requests that require access to the p2p network, such as `fee` or `submit`, Clio automatically forwards the request to a rippled node and propagates the response back to the client. To access non-validated data for *any* request, simply add `ledger_index: "current"` to the request, and Clio will forward the request to rippled.
For requests that require access to the p2p network, such as `fee` or `submit`, Clio automatically forwards the request to a rippled node and propagates the response back to the client.
To access non-validated data for *any* request, simply add `ledger_index: "current"` to the request, and Clio will forward the request to rippled.
Clio does not connect to the peer-to-peer network. Instead, Clio extracts data from a group of specified rippled nodes. Running Clio requires access to at least one rippled node
from which data can be extracted. The rippled node does not need to be running on the same machine as Clio.
## Requirements
1. Access to a Cassandra cluster or ScyllaDB cluster. Can be local or remote.
2. Access to one or more rippled nodes. Can be local or remote.
## Building
Clio is built with CMake. Clio requires c++20, and boost 1.75.0 or later.
Clio is built with CMake and uses Conan for managing dependencies.
It is written in C++20 and therefore requires a modern compiler.
Use these instructions to build a Clio executable from the source. These instructions were tested on Ubuntu 20.04 LTS.
## Prerequisites
```sh
# Install dependencies
sudo apt-get -y install git pkg-config protobuf-compiler libprotobuf-dev libssl-dev wget build-essential bison flex autoconf cmake
### Minimum Requirements
# Compile Boost
wget -O $HOME/boost_1_75_0.tar.gz https://boostorg.jfrog.io/artifactory/main/release/1.75.0/source/boost_1_75_0.tar.gz
tar xvzf $HOME/boost_1_75_0.tar.gz
cd $HOME/boost_1_75_0
./bootstrap.sh
./b2 -j$(nproc)
echo "export BOOST_ROOT=$HOME/boost_1_75_0" >> $HOME/.profile && source $HOME/.profile
- [Python 3.7](https://www.python.org/downloads/)
- [Conan 1.55](https://conan.io/downloads.html)
- [CMake 3.16](https://cmake.org/download/)
- [**Optional**] [GCovr](https://gcc.gnu.org/onlinedocs/gcc/Gcov.html) (needed for code coverage generation)
# Clone the Clio Git repository & build Clio
cd $HOME
git clone https://github.com/XRPLF/clio.git
cd $HOME/clio
cmake -B build && cmake --build build --parallel $(nproc)
| Compiler | Version |
|-------------|---------|
| GCC | 11 |
| Clang | 14 |
| Apple Clang | 14.0.3 |
### Conan configuration
Clio does not require anything but default settings in your (`~/.conan/profiles/default`) Conan profile. It's best to have no extra flags specified.
> Mac example:
```
[settings]
os=Macos
os_build=Macos
arch=armv8
arch_build=armv8
compiler=apple-clang
compiler.version=14
compiler.libcxx=libc++
build_type=Release
compiler.cppstd=20
```
> Linux example:
```
[settings]
os=Linux
os_build=Linux
arch=x86_64
arch_build=x86_64
compiler=gcc
compiler.version=11
compiler.libcxx=libstdc++11
build_type=Release
compiler.cppstd=20
```
### Artifactory
1. Make sure artifactory is setup with Conan
```sh
conan remote add --insert 0 conan-non-prod http://18.143.149.228:8081/artifactory/api/conan/conan-non-prod
```
Now you should be able to download prebuilt `xrpl` package on some platforms.
2. Remove old packages you may have cached:
```sh
conan remove -f xrpl
conan remove -f cassandra-cpp-driver
```
## Building Clio
Navigate to Clio's root directory and perform
```sh
mkdir build && cd build
conan install .. --output-folder . --build missing --settings build_type=Release -o tests=True
cmake -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build . --parallel 8 # or without the number if you feel extra adventurous
```
If all goes well, `conan install` will find required packages and `cmake` will do the rest. you should end up with `clio_server` and `clio_tests` in the `build` directory (the current directory).
> **Tip:** You can omit the `-o tests=True` in `conan install` command above if you don't want to build `clio_tests`.
> **Tip:** To generate a Code Coverage report, include `-o coverage=True` in the `conan install` command above, along with `-o tests=True` to enable tests. After running the `cmake` commands, execute `make clio_tests-ccov`. The coverage report will be found at `clio_tests-llvm-cov/index.html`.
## Running
```sh
@@ -78,6 +125,36 @@ server is running
to the IP of your Clio server. This entry can take the form of a comma-separated list if
you are running multiple Clio nodes.
In addition, the parameter `start_sequence` can be included and configured within the top level of the config file. This parameter specifies the sequence of first ledger to extract if the database is empty. Note that ETL extracts ledgers in order and that no backfilling functionality currently exists, meaning Clio will not retroactively learn ledgers older than the one you specify. Choosing to specify this or not will yield the following behavior:
- If this setting is absent and the database is empty, ETL will start with the next ledger validated by the network.
- If this setting is present and the database is not empty, an exception is thrown.
In addition, the optional parameter `finish_sequence` can be added to the json file as well, specifying where the ledger can stop.
To add `start_sequence` and/or `finish_sequence` to the config.json file appropriately, they will be on the same top level of precedence as other parameters (such as `database`, `etl_sources`, `read_only`, etc.) and be specified with an integer. Here is an example snippet from the config file:
```json
"start_sequence": 12345,
"finish_sequence": 54321
```
The parameters `ssl_cert_file` and `ssl_key_file` can also be added to the top level of precedence of our Clio config. `ssl_cert_file` specifies the filepath for your SSL cert while `ssl_key_file` specifies the filepath for your SSL key. It is up to you how to change ownership of these folders for your designated Clio user. Your options include:
- Copying the two files as root somewhere that's accessible by the Clio user, then running `sudo chown` to your user
- Changing the permissions directly so it's readable by your Clio user
- Running Clio as root (strongly discouraged)
An example of how to specify `ssl_cert_file` and `ssl_key_file` in the config:
```json
"server": {
"ip": "0.0.0.0",
"port": 51233
},
"ssl_cert_file": "/full/path/to/cert.file",
"ssl_key_file": "/full/path/to/key.file"
```
Once your config files are ready, start rippled and Clio. It doesn't matter which you
start first, and it's fine to stop one or the other and restart at any given time.
@@ -146,25 +223,81 @@ which can cause high latencies. A possible alternative to this is to just deploy
a database in each region, and the Clio nodes in each region use their region's database.
This is effectively two systems.
Clio supports API versioning as [described here](https://xrpl.org/request-formatting.html#api-versioning).
It's possible to configure `minimum`, `maximum` and `default` version like so:
```json
"api_version": {
"min": 1,
"max": 2,
"default": 2
}
```
All of the above are optional.
Clio will fallback to hardcoded defaults when not specified in the config file or configured values are outside
of the minimum and maximum supported versions hardcoded in `src/rpc/common/APIVersion.h`.
> **Note:** See `example-config.json` for more details.
## Developing against `rippled` in standalone mode
If you wish you develop against a `rippled` instance running in standalone
mode there are a few quirks of both clio and rippled you need to keep in mind.
You must:
1. Advance the `rippled` ledger to at least ledger 256
2. Wait 10 minutes before first starting clio against this standalone node.
## Logging
Clio provides several logging options, all are configurable via the config file and are detailed below.
`log_level`: The minimum level of severity at which the log message will be outputted.
Severity options are `trace`, `debug`, `info`, `warning`, `error`, `fatal`.
`log_level`: The minimum level of severity at which the log message will be outputted by default.
Severity options are `trace`, `debug`, `info`, `warning`, `error`, `fatal`. Defaults to `info`.
`log_to_console`: Enable/disable log output to console. Options are `true`/`false`.
`log_format`: The format of log lines produced by clio. Defaults to `"%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%"`.
Each of the variables expands like so
- `TimeStamp`: The full date and time of the log entry
- `SourceLocation`: A partial path to the c++ file and the line number in said file (`source/file/path:linenumber`)
- `ThreadID`: The ID of the thread the log entry is written from
- `Channel`: The channel that this log entry was sent to
- `Severity`: The severity (aka log level) the entry was sent at
- `Message`: The actual log message
`log_to_file`: Enable/disable log saving to files in persistent local storage. Options are `true`/`false`.
`log_channels`: An array of json objects, each overriding properties for a logging `channel`.
At the moment of writing, only `log_level` can be overriden using this mechanism.
`log_directory`: Path to the directory where log files are stored. If such directory doesn't exist, Clio will create it.
Each object is of this format:
```json
{
"channel": "Backend",
"log_level": "fatal"
}
```
If no override is present for a given channel, that channel will log at the severity specified by the global `log_level`.
Overridable log channels: `Backend`, `WebServer`, `Subscriptions`, `RPC`, `ETL` and `Performance`.
`log_rotation_size`: The max size of the log file in **megabytes** before it will rotate into a smaller file.
> **Note:** See `example-config.json` for more details.
`log_directory_max_size`: The max size of the log directory in **megabytes** before old log files will be
deleted to free up space.
`log_to_console`: Enable/disable log output to console. Options are `true`/`false`. Defaults to true.
`log_directory`: Path to the directory where log files are stored. If such directory doesn't exist, Clio will create it. If not specified, logs are not written to a file.
`log_rotation_size`: The max size of the log file in **megabytes** before it will rotate into a smaller file. Defaults to 2GB.
`log_directory_max_size`: The max size of the log directory in **megabytes** before old log files will be
deleted to free up space. Defaults to 50GB.
`log_rotation_hour_interval`: The time interval in **hours** after the last log rotation to automatically
rotate the current log file.
rotate the current log file. Defaults to 12 hours.
Note, time-based log rotation occurs dependently on size-based log rotation, where if a
size-based log rotation occurs, the timer for the time-based rotation will reset.
size-based log rotation occurs, the timer for the time-based rotation will reset.
`log_tag_style`: Tag implementation to use. Must be one of:
- `uint`: Lock free and threadsafe but outputs just a simple unsigned integer
- `uuid`: Threadsafe and outputs a UUID tag
- `none`: Don't use tagging at all
## Cassandra / Scylla Administration
Since Clio relies on either Cassandra or Scylla for its database backend, here are some important considerations:
- Scylla, by default, will reserve all free RAM on a machine for itself. If you are running `rippled` or other services on the same machine, restrict its memory usage using the `--memory` argument: https://docs.scylladb.com/getting-started/scylla-in-a-shared-environment/

121
REVIEW.md
View File

@@ -1,121 +0,0 @@
# How to review clio
Clio is a massive project, and thus I don't expect the code to be reviewed the
way a normal PR would. So I put this guide together to help reviewers look at
the relevant pieces of code without getting lost in the weeds.
One thing reviewers should keep in mind is that most of clio is designed to be
lightweight and simple. We try not to introduce any uneccessary complexity and
keep the code as simple and straightforward as possible. Sometimes complexity is
unavoidable, but simplicity is the goal.
## Order of review
The code is organized into 4 main components, each with their own folder. The
code in each folder is as self contained as possible. A good way to approach
the review would be to review one folder at a time.
### backend
The code in the backend folder is the heart of the project, and reviewers should
start here. This is the most complex part of the code, as well as the most
performance sensitive. clio does not keep any data in memory, so performance
generally depends on the data model and the way we talk to the database.
Reviewers should start with the README in this folder to get a high level idea
of the data model and to review the data model itself. Then, reviewers should
dive into the implementation. The table schemas and queries for Cassandra are
defined in `CassandraBackend::open()`. The table schemas for Postgres are defined
in Pg.cpp. The queries for Postgres are defined in each of the functions of `PostgresBackend`.
A good way to approach the implementation would be to look at the table schemas,
and then go through the functions declared in `BackendInterface`. Reviewers could
also branch out to the rest of the code by looking at where these functions are
called from.
### webserver
The code in the webserver folder implements the web server for handling RPC requests.
This code was mostly copied and pasted from boost beast example code, so I would
really appreciate review here.
### rpc
The rpc folder contains all of the handlers and any helper functions they need.
This code is not too complicated, so reviewers don't need to dwell long here.
### etl
The etl folder contains all of the code for extracting data from rippled. This
code is complex and important, but most of this code was just copied from rippled
reporting mode, and thus has already been reviewed and is being used in prod.
## Design decisions that should be reviewed
### Data model
Reviewers should review the general data model. The data model itself is described
at a high level in the README in the backend folder. The table schemas and queries
for Cassandra are defined in the `open()` function of `CassandraBackend`. The table
schemas for Postgres are defined in Pg.cpp.
Particular attention should be paid to the keys table, and the problem that solves
(successor/upper bound). I originally was going to have a special table for book_offers,
but then I decided that we could use the keys table itself for that and save space.
This makes book_offers somewhat slow compared to rippled, though still very usable.
### Large rows
I did some tricks with Cassandra to deal with very large rows in the keys and account_tx
tables. For each of these, the partition key (the first component of the primary
key) is a compound key. This is meant to break large rows into smaller rows. This
is done to avoid hotspots. Data is sharded in Cassandra, and if some rows get very
large, some nodes can have a lot more data than others.
For account_tx, this has performance implications when iterating very far back
in time. Refer to the `fetchAccountTransactions()` function in `CassandraBackend`.
It is unclear if this needs to be done for other tables.
### Postgres table partitioning
Originally, Postgres exhibited performance problems when the dataset approach 1
TB. This was solved by table partitioning.
### Threading
I used asio for multithreading. There are a lot of different io_contexts lying
around the code. This needs to be cleaned up a bit. Most of these are really
just ways to submit an async job to a single thread. I don't think it makes
sense to have one io_context for the whole application, but some of the threading
is a bit opaque and could be cleaned up.
### Boost Json
I used boost json for serializing data to json.
### No cache
As of now, there is no cache. I am not sure if a cache is even worth it. A
transaction cache would not be hard, but a cache for ledger data will be hard.
While a cache would improve performance, it would increase memory usage. clio
is designed to be lightweight. Also, I've reached thousands of requests per
second with a single clio node, so I'm not sure performance is even an issue.
## Things I'm less than happy about
#### BackendIndexer
This is a particularly hairy piece of code that handles writing to the keys table.
I am not too happy with this code. Parts of it need to execute in real time as
part of ETL, and other parts are allowed to run in the background. There is also
code that detects if a previous background job failed to complete before the
server shutdown, and thus tries to rerun that job. The code feels tacked on, and
I would like it to be more cleanly integrated with the rest of the code.
#### Shifting
There is some bit shifting going on with the keys table and the account_tx table.
The keys table is written to every 2^20 ledgers. Maybe it would be better to just
write every 1 million ledgers.
#### performance of book_offers
book_offers is a bit slow. It could be sped up in a variety of ways. One is to
keep a separate book_offers table. However, this is not straightforward and will
use more space. Another is to keep a cache of book_offers for the most recent ledger
(or few ledgers). I am not sure if this is worth it
#### account_tx in Cassandra
After the fix to deal with large rows, account_tx can be slow at times when using
Cassandra. Specifically, if there are large gaps in time where the account was
not affected by any transactions, the code will be reading empty records. I would
like to sidestep this issue if possible.
#### Implementation of fetchLedgerPage
`fetchLedgerPage()` is rather complex. Part of this seems unavoidable, since this
code is dealing with the keys table.

View File

@@ -1,38 +1,43 @@
/*
* This is an example configuration file. Please do not use without modifying to suit your needs.
*/
{
"database":
{
"type":"cassandra",
"cassandra":
{
"secure_connect_bundle":"[path/to/zip. ignore if using contact_points]",
"contact_points":"[ip. ignore if using secure_connect_bundle]",
"port":"[port. ignore if using_secure_connect_bundle]",
"keyspace":"clio",
"username":"[username, if any]",
"password":"[password, if any]",
"max_requests_outstanding":25000,
"threads":8
"database": {
"type": "cassandra",
"cassandra": {
// This option can be used to setup a secure connect bundle connection
"secure_connect_bundle": "[path/to/zip. ignore if using contact_points]",
// The following options are used only if using contact_points
"contact_points": "[ip. ignore if using secure_connect_bundle]",
"port": "[port. ignore if using_secure_connect_bundle]",
// Authentication settings
"username": "[username, if any]",
"password": "[password, if any]",
// Other common settings
"keyspace": "clio",
"max_write_requests_outstanding": 25000,
"max_read_requests_outstanding": 30000,
"threads": 8
}
},
"etl_sources":
[
"etl_sources": [
{
"ip":"[rippled ip]",
"ws_port":"6006",
"grpc_port":"50051"
"ip": "[rippled ip]",
"ws_port": "6006",
"grpc_port": "50051"
}
],
"dos_guard":
{
"whitelist":["127.0.0.1"]
"dos_guard": {
"whitelist": [
"127.0.0.1"
]
},
"server":{
"ip":"0.0.0.0",
"port":8080
"server": {
"ip": "0.0.0.0",
"port": 8080
},
"log_level":"debug",
"log_file":"./clio.log",
"online_delete":0,
"extractor_threads":8,
"read_only":false
"log_level": "debug",
"log_file": "./clio.log",
"extractor_threads": 8,
"read_only": false
}

87
conanfile.py Normal file
View File

@@ -0,0 +1,87 @@
from conan import ConanFile
from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
import re
class Clio(ConanFile):
name = 'clio'
license = 'ISC'
author = 'Alex Kremer <akremer@ripple.com>, John Freeman <jfreeman@ripple.com>'
url = 'https://github.com/xrplf/clio'
description = 'Clio RPC server'
settings = 'os', 'compiler', 'build_type', 'arch'
options = {
'fPIC': [True, False],
'verbose': [True, False],
'tests': [True, False], # build unit tests; create `clio_tests` binary
'docs': [True, False], # doxygen API docs; create custom target 'docs'
'packaging': [True, False], # create distribution packages
'coverage': [True, False], # build for test coverage report; create custom target `clio_tests-ccov`
}
requires = [
'boost/1.82.0',
'cassandra-cpp-driver/2.16.2',
'fmt/10.0.0',
'grpc/1.50.1',
'openssl/1.1.1u',
'xrpl/1.12.0-b2',
]
default_options = {
'fPIC': True,
'verbose': False,
'tests': False,
'packaging': False,
'coverage': False,
'docs': False,
'xrpl/*:tests': False,
'cassandra-cpp-driver/*:shared': False,
'date/*:header_only': True,
'grpc/*:shared': False,
'grpc/*:secure': True,
'libpq/*:shared': False,
'lz4/*:shared': False,
'openssl/*:shared': False,
'protobuf/*:shared': False,
'protobuf/*:with_zlib': True,
'snappy/*:shared': False,
'gtest/*:no_main': True,
}
exports_sources = (
'CMakeLists.txt', 'CMake/*', 'src/*'
)
def requirements(self):
if self.options.tests:
self.requires('gtest/1.13.0')
def configure(self):
if self.settings.compiler == 'apple-clang':
self.options['boost'].visibility = 'global'
def layout(self):
cmake_layout(self)
# Fix this setting to follow the default introduced in Conan 1.48
# to align with our build instructions.
self.folders.generators = 'build/generators'
generators = 'CMakeDeps'
def generate(self):
tc = CMakeToolchain(self)
tc.variables['verbose'] = self.options.verbose
tc.variables['tests'] = self.options.tests
tc.variables['coverage'] = self.options.coverage
tc.variables['docs'] = self.options.docs
tc.variables['packaging'] = self.options.packaging
tc.generate()
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
def package(self):
cmake = CMake(self)
cmake.install()

49
docker/centos/Dockerfile Normal file
View File

@@ -0,0 +1,49 @@
# FROM centos:7 as deps
FROM centos:7 as build
ENV CLIO_DIR=/opt/clio/
# ENV OPENSSL_DIR=/opt/openssl
RUN yum -y install git epel-release centos-release-scl perl-IPC-Cmd openssl
RUN yum install -y devtoolset-11
ENV version=3.16
ENV build=3
# RUN curl -OJL https://cmake.org/files/v$version/cmake-$version.$build.tar.gz
COPY docker/shared/install_cmake.sh /install_cmake.sh
RUN /install_cmake.sh 3.16.3 /usr/local
RUN source /opt/rh/devtoolset-11/enable
WORKDIR /tmp
# RUN mkdir $OPENSSL_DIR && cd $OPENSSL_DIR
COPY docker/centos/build_git_centos7.sh build_git_centos7.sh
RUN ./build_git_centos7.sh
RUN git clone https://github.com/openssl/openssl
WORKDIR /tmp/openssl
RUN git checkout OpenSSL_1_1_1q
#--prefix=/usr --openssldir=/etc/ssl --libdir=lib no-shared zlib-dynamic
RUN SSLDIR=$(openssl version -d | cut -d: -f2 | tr -d [:space:]\") && ./config -fPIC --prefix=/usr --openssldir=${SSLDIR} zlib shared && \
make -j $(nproc) && \
make install_sw
WORKDIR /tmp
# FROM centos:7 as build
RUN git clone https://github.com/xrplf/clio.git
COPY docker/shared/build_boost.sh build_boost.sh
ENV OPENSSL_ROOT=/opt/local/openssl
ENV BOOST_ROOT=/boost
RUN source scl_source enable devtoolset-11 && /tmp/build_boost.sh 1.75.0
RUN yum install -y bison flex
RUN yum install -y rpmdevtools rpmlint
RUN source /opt/rh/devtoolset-11/enable && cd /tmp/clio && \
cmake -B build -DBUILD_TESTS=1 && \
cmake --build build --parallel $(nproc)
RUN mkdir output
RUN strip clio/build/clio_server && strip clio/build/clio_tests
RUN cp clio/build/clio_tests output/ && cp clio/build/clio_server output/
RUN cp clio/example-config.json output/example-config.json
FROM centos:7
COPY --from=build /tmp/output /clio
RUN mkdir -p /opt/clio/etc && mv /clio/example-config.json /opt/clio/etc/config.json
CMD ["/clio/clio_server", "/opt/clio/etc/config.json"]

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -ex
GIT_VERSION="2.37.1"
curl -OJL https://github.com/git/git/archive/refs/tags/v${GIT_VERSION}.tar.gz
tar zxvf git-${GIT_VERSION}.tar.gz
cd git-${GIT_VERSION}
yum install -y centos-release-scl epel-release
yum update -y
yum install -y devtoolset-11 autoconf gnu-getopt gettext zlib-devel libcurl-devel
source /opt/rh/devtoolset-11/enable
make configure
./configure
make git -j$(nproc)
make install git
git --version | cut -d ' ' -f3

11
docker/centos/install_cmake.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
set -eo pipefail
CMAKE_VERSION=${1:-"3.16.3"}
cd /tmp
URL="https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-x86_64.tar.gz"
curl -OJLs $URL
tar xzvf cmake-${CMAKE_VERSION}-Linux-x86_64.tar.gz
mv cmake-${CMAKE_VERSION}-Linux-x86_64 /opt/
ln -s /opt/cmake-${CMAKE_VERSION}-Linux-x86_64/bin/cmake /usr/local/bin/cmake

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -exu
#yum install wget lz4 lz4-devel git llvm13-static.x86_64 llvm13-devel.x86_64 devtoolset-11-binutils zlib-static
# it's either those or link=static that halves the failures. probably link=static
BOOST_VERSION=$1
BOOST_VERSION_=$(echo ${BOOST_VERSION} | tr . _)
echo "BOOST_VERSION: ${BOOST_VERSION}"
echo "BOOST_VERSION_: ${BOOST_VERSION_}"
curl -OJLs "https://boostorg.jfrog.io/artifactory/main/release/${BOOST_VERSION}/source/boost_${BOOST_VERSION_}.tar.gz"
tar zxf "boost_${BOOST_VERSION_}.tar.gz"
cd boost_${BOOST_VERSION_} && ./bootstrap.sh && ./b2 --without-python link=static -j$(nproc)
mkdir -p /boost && mv boost /boost && mv stage /boost

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -ex
GIT_VERSION="2.37.1"
curl -OJL https://github.com/git/git/archive/refs/tags/v${GIT_VERSION}.tar.gz
tar zxvf git-${GIT_VERSION}.tar.gz
cd git-${GIT_VERSION}
yum install -y centos-release-scl epel-release
yum update -y
yum install -y devtoolset-11 autoconf gnu-getopt gettext zlib-devel libcurl-devel
source /opt/rh/devtoolset-11/enable
make configure
./configure
make git -j$(nproc)
make install git
git --version | cut -d ' ' -f3

View File

@@ -0,0 +1,34 @@
FROM centos:7
ENV CLIO_DIR=/opt/clio/
# ENV OPENSSL_DIR=/opt/openssl
RUN yum -y install git epel-release centos-release-scl perl-IPC-Cmd openssl
RUN yum install -y devtoolset-11
ENV version=3.16
ENV build=3
# RUN curl -OJL https://cmake.org/files/v$version/cmake-$version.$build.tar.gz
COPY install_cmake.sh /install_cmake.sh
RUN /install_cmake.sh 3.16.3 /usr/local
RUN source /opt/rh/devtoolset-11/enable
WORKDIR /tmp
# RUN mkdir $OPENSSL_DIR && cd $OPENSSL_DIR
COPY build_git_centos7.sh build_git_centos7.sh
RUN ./build_git_centos7.sh
RUN git clone https://github.com/openssl/openssl
WORKDIR /tmp/openssl
RUN git checkout OpenSSL_1_1_1q
#--prefix=/usr --openssldir=/etc/ssl --libdir=lib no-shared zlib-dynamic
RUN SSLDIR=$(openssl version -d | cut -d: -f2 | tr -d [:space:]\") && ./config -fPIC --prefix=/usr --openssldir=${SSLDIR} zlib shared && \
make -j $(nproc) && \
make install_sw
WORKDIR /tmp
RUN git clone https://github.com/xrplf/clio.git
COPY build_boost.sh build_boost.sh
ENV OPENSSL_ROOT=/opt/local/openssl
ENV BOOST_ROOT=/boost
RUN source scl_source enable devtoolset-11 && /tmp/build_boost.sh 1.75.0
RUN yum install -y bison flex
RUN source /opt/rh/devtoolset-11/enable && \
cd /tmp/clio && cmake -B build -Dtests=0 -Dlocal_libarchive=1 -Dunity=0 -DBUILD_TESTS=0 && cmake --build build --parallel $(nproc)

View File

@@ -0,0 +1,11 @@
#!/bin/bash
set -eo pipefail
CMAKE_VERSION=${1:-"3.16.3"}
cd /tmp
URL="https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-x86_64.tar.gz"
curl -OJLs $URL
tar xzvf cmake-${CMAKE_VERSION}-Linux-x86_64.tar.gz
mv cmake-${CMAKE_VERSION}-Linux-x86_64 /opt/
ln -s /opt/cmake-${CMAKE_VERSION}-Linux-x86_64/bin/cmake /usr/local/bin/cmake

13
docker/shared/build_boost.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -exu
#yum install wget lz4 lz4-devel git llvm13-static.x86_64 llvm13-devel.x86_64 devtoolset-11-binutils zlib-static
# it's either those or link=static that halves the failures. probably link=static
BOOST_VERSION=$1
BOOST_VERSION_=$(echo ${BOOST_VERSION} | tr . _)
echo "BOOST_VERSION: ${BOOST_VERSION}"
echo "BOOST_VERSION_: ${BOOST_VERSION_}"
curl -OJLs "https://boostorg.jfrog.io/artifactory/main/release/${BOOST_VERSION}/source/boost_${BOOST_VERSION_}.tar.gz"
tar zxf "boost_${BOOST_VERSION_}.tar.gz"
cd boost_${BOOST_VERSION_} && ./bootstrap.sh && ./b2 --without-python link=static -j$(nproc)
mkdir -p /boost && mv boost /boost && mv stage /boost

11
docker/shared/install_cmake.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
set -eo pipefail
CMAKE_VERSION=${1:-"3.16.3"}
cd /tmp
URL="https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-x86_64.tar.gz"
curl -OJLs $URL
tar xzvf cmake-${CMAKE_VERSION}-Linux-x86_64.tar.gz
mv cmake-${CMAKE_VERSION}-Linux-x86_64 /opt/
ln -s /opt/cmake-${CMAKE_VERSION}-Linux-x86_64/bin/cmake /usr/local/bin/cmake

View File

@@ -0,0 +1,3 @@
#!/bin/bash
set -e

24
docker/ubuntu/Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM ubuntu:20.04 AS boost
RUN apt-get update && apt-get install -y build-essential
ARG BOOST_VERSION_=1_75_0
ARG BOOST_VERSION=1.75.0
COPY docker/shared/build_boost.sh .
RUN apt install -y curl
RUN ./build_boost.sh ${BOOST_VERSION}
ENV BOOST_ROOT=/boost
FROM ubuntu:20.04 AS build
ENV BOOST_ROOT=/boost
COPY --from=boost /boost /boost
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install --no-install-recommends -y build-essential software-properties-common pkg-config libssl-dev wget curl gpg git zlib1g-dev bison flex autoconf lsb-release
RUN apt install -y gpg-agent
RUN wget https://apt.llvm.org/llvm.sh
RUN chmod +x llvm.sh && ./llvm.sh 14 && ./llvm.sh 15
# COPY . /clio
## Install cmake
ARG CMAKE_VERSION=3.16.3
COPY docker/shared/install_cmake.sh .
RUN ./install_cmake.sh ${CMAKE_VERSION}
ENV PATH="/opt/local/cmake/bin:$PATH"

View File

@@ -1,42 +1,127 @@
/*
* This is an example configuration file. Please do not use without modifying to suit your needs.
*/
{
"database":
{
"type":"cassandra",
"cassandra":
{
"contact_points":"127.0.0.1",
"port":9042,
"keyspace":"clio",
"replication_factor":1,
"table_prefix":"",
"max_requests_outstanding":25000,
"threads":8
"database": {
"type": "cassandra",
"cassandra": {
"contact_points": "127.0.0.1",
"port": 9042,
"keyspace": "clio",
"replication_factor": 1,
"table_prefix": "",
"max_write_requests_outstanding": 25000,
"max_read_requests_outstanding": 30000,
"threads": 8,
//
// Advanced options. USE AT OWN RISK:
// ---
"max_connections_per_host": 1, // Defaults to 2
"core_connections_per_host": 1, // Defaults to 2
"max_concurrent_requests_threshold": 55000 // Defaults to ((max_read + max_write) / core_connections_per_host)
//
// Below options will use defaults from cassandra driver if left unspecified.
// See https://docs.datastax.com/en/developer/cpp-driver/2.0/api/struct.CassCluster/ for details.
//
// "queue_size_event": 1,
// "queue_size_io": 2,
// "write_bytes_high_water_mark": 3,
// "write_bytes_low_water_mark": 4,
// "pending_requests_high_water_mark": 5,
// "pending_requests_low_water_mark": 6,
// "max_requests_per_flush": 7,
// "max_concurrent_creation": 8
//
// ---
}
},
"etl_sources":
[
"etl_sources": [
{
"ip":"127.0.0.1",
"ws_port":"6006",
"grpc_port":"50051"
"ip": "127.0.0.1",
"ws_port": "6006",
"grpc_port": "50051"
}
],
"dos_guard":
{
"whitelist":["127.0.0.1"]
"dos_guard": {
// Comma-separated list of IPs to exclude from rate limiting
"whitelist": [
"127.0.0.1"
],
//
// The below values are the default values and are only specified here
// for documentation purposes. The rate limiter currently limits
// connections and bandwidth per IP. The rate limiter looks at the raw
// IP of a client connection, and so requests routed through a load
// balancer will all have the same IP and be treated as a single client.
//
"max_fetches": 1000000, // Max bytes per IP per sweep interval
"max_connections": 20, // Max connections per IP
"max_requests": 20, // Max connections per IP per sweep interval
"sweep_interval": 1 // Time in seconds before resetting max_fetches and max_requests
},
"server":{
"ip":"0.0.0.0",
"port":51233
"cache": {
// Comma-separated list of peer nodes that Clio can use to download cache from at startup
"peers": [
{
"ip": "127.0.0.1",
"port": 51234
}
]
},
"log_level":"debug",
"server": {
"ip": "0.0.0.0",
"port": 51233,
// Max number of requests to queue up before rejecting further requests.
// Defaults to 0, which disables the limit.
"max_queue_size": 500
},
// Overrides log level on a per logging channel.
// Defaults to global "log_level" for each unspecified channel.
"log_channels": [
{
"channel": "Backend",
"log_level": "fatal"
},
{
"channel": "WebServer",
"log_level": "info"
},
{
"channel": "Subscriptions",
"log_level": "info"
},
{
"channel": "RPC",
"log_level": "error"
},
{
"channel": "ETL",
"log_level": "debug"
},
{
"channel": "Performance",
"log_level": "trace"
}
],
"log_level": "info",
// Log format (this is the default format)
"log_format": "%TimeStamp% (%SourceLocation%) [%ThreadID%] %Channel%:%Severity% %Message%",
"log_to_console": true,
"log_to_file": true,
"log_directory":"./clio_log",
// Clio logs to file in the specified directory only if "log_directory" is set
// "log_directory": "./clio_log",
"log_rotation_size": 2048,
"log_directory_max_size": 51200,
"log_rotation_hour_interval": 12,
"online_delete":0,
"extractor_threads":8,
"read_only":false
"log_tag_style": "uint",
"extractor_threads": 8,
"read_only": false,
// "start_sequence": [integer] the ledger index to start from,
// "finish_sequence": [integer] the ledger index to finish at,
// "ssl_cert_file" : "/full/path/to/cert.file",
// "ssl_key_file" : "/full/path/to/key.file"
"api_version": {
"min": 2,
"max": 2,
"default": 2 // Clio only supports API v2 and newer
}
}

87
external/cassandra/conanfile.py vendored Normal file
View File

@@ -0,0 +1,87 @@
from conan import ConanFile, tools
from conan.tools.cmake import CMake, CMakeToolchain
class Cassandra(ConanFile):
name = 'cassandra-cpp-driver'
version = '2.16.2'
license = 'Apache-2.0'
url = 'https://github.com/conan-io/conan-center-index'
homepage = 'https://docs.datastax.com/en/developer/cpp-driver/'
description = 'Cassandra C++ Driver'
topics = ('conan', 'cassandra', 'driver')
settings = 'os', 'arch', 'compiler', 'build_type'
options = {
'shared': [True, False],
'fPIC': [True, False],
'install_header_in_subdir': [True, False],
'use_atomic': [None, 'boost', 'std'],
'with_openssl': [True, False],
'with_zlib': [True, False],
'with_kerberos': [True, False],
'use_timerfd': [True, False],
}
default_options = {
'shared': False,
'fPIC': True,
'install_header_in_subdir': False,
'use_atomic': None,
'with_openssl': True,
'with_zlib': True,
'with_kerberos': False,
'use_timerfd': True,
}
def requirements(self):
self.requires('libuv/1.44.1')
self.requires('http_parser/2.9.4')
if self.options.with_openssl:
self.requires('openssl/1.1.1q')
if self.options.with_zlib:
self.requires('minizip/1.2.12')
self.requires('zlib/1.2.13')
if self.options.use_atomic == 'boost':
self.requires('boost/1.79.0')
exports_sources = ['CMakeLists.txt']
def config_options(self):
if self.settings.os == 'Windows':
del self.options.fPIC
def configure(self):
self.options['libuv'].shared = self.options.shared
def generate(self):
tc = CMakeToolchain(self)
if self.settings.get_safe('compiler.cppstd') == '20':
tc.blocks['cppstd'].values['cppstd'] = 17
tc.variables['CASS_BUILD_STATIC'] = not self.options.shared
tc.variables['CASS_USE_STATIC_LIBS'] = not self.options.shared
tc.variables['CASS_BUILD_SHARED'] = self.options.shared
tc.variables['LIBUV_ROOT_DIR'] = self.deps_cpp_info['libuv'].rootpath
if self.options.with_openssl:
tc.variables['OPENSSL_ROOT_DIR'] = self.deps_cpp_info['openssl'].rootpath
tc.generate()
def source(self):
tools.files.get(self, f'https://github.com/datastax/cpp-driver/archive/refs/tags/{self.version}.tar.gz', strip_root=True)
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
def package(self):
cmake = CMake(self)
cmake.install()
def package_info(self):
if self.options.shared:
self.cpp_info.libs = ['cassandra']
else:
self.cpp_info.libs = ['cassandra_static']
if self.settings.os == 'Windows':
self.cpp_info.libs.extend(['iphlpapi', 'psapi', 'wsock32', 'crypt32', 'ws2_32', 'userenv'])
if not self.options.shared:
self.cpp_info.defines = ['CASS_STATIC']

View File

@@ -1,65 +0,0 @@
#ifndef RIPPLE_APP_REPORTING_BACKENDFACTORY_H_INCLUDED
#define RIPPLE_APP_REPORTING_BACKENDFACTORY_H_INCLUDED
#include <boost/algorithm/string.hpp>
#include <backend/BackendInterface.h>
#include <backend/CassandraBackend.h>
#include <backend/PostgresBackend.h>
namespace Backend {
std::shared_ptr<BackendInterface>
make_Backend(boost::asio::io_context& ioc, boost::json::object const& config)
{
BOOST_LOG_TRIVIAL(info) << __func__ << ": Constructing BackendInterface";
boost::json::object dbConfig = config.at("database").as_object();
bool readOnly = false;
if (config.contains("read_only"))
readOnly = config.at("read_only").as_bool();
auto type = dbConfig.at("type").as_string();
std::shared_ptr<BackendInterface> backend = nullptr;
if (boost::iequals(type, "cassandra"))
{
if (config.contains("online_delete"))
dbConfig.at(type).as_object()["ttl"] =
config.at("online_delete").as_int64() * 4;
backend = std::make_shared<CassandraBackend>(
ioc, dbConfig.at(type).as_object());
}
else if (boost::iequals(type, "postgres"))
{
if (dbConfig.contains("experimental") &&
dbConfig.at("experimental").is_bool() &&
dbConfig.at("experimental").as_bool())
backend = std::make_shared<PostgresBackend>(
ioc, dbConfig.at(type).as_object());
else
BOOST_LOG_TRIVIAL(fatal)
<< "Postgres support is experimental at this time. "
<< "If you would really like to use Postgres, add "
"\"experimental\":true to your database config";
}
if (!backend)
throw std::runtime_error("Invalid database type");
backend->open(readOnly);
auto rng = backend->hardFetchLedgerRangeNoThrow();
if (rng)
{
backend->updateRange(rng->minSequence);
backend->updateRange(rng->maxSequence);
}
BOOST_LOG_TRIVIAL(info)
<< __func__ << ": Constructed BackendInterface Successfully";
return backend;
}
} // namespace Backend
#endif // RIPPLE_REPORTING_BACKEND_FACTORY

View File

@@ -1,338 +0,0 @@
#ifndef RIPPLE_APP_REPORTING_BACKENDINTERFACE_H_INCLUDED
#define RIPPLE_APP_REPORTING_BACKENDINTERFACE_H_INCLUDED
#include <ripple/ledger/ReadView.h>
#include <boost/asio.hpp>
#include <backend/DBHelpers.h>
#include <backend/SimpleCache.h>
#include <backend/Types.h>
#include <thread>
#include <type_traits>
namespace Backend {
class DatabaseTimeout : public std::exception
{
const char*
what() const throw() override
{
return "Database read timed out. Please retry the request";
}
};
template <class F>
auto
retryOnTimeout(F func, size_t waitMs = 500)
{
while (true)
{
try
{
return func();
}
catch (DatabaseTimeout& t)
{
std::this_thread::sleep_for(std::chrono::milliseconds(waitMs));
BOOST_LOG_TRIVIAL(error)
<< __func__ << " function timed out. Retrying ... ";
}
}
}
template <class F>
auto
synchronous(F&& f)
{
boost::asio::io_context ctx;
boost::asio::io_context::strand strand(ctx);
std::optional<boost::asio::io_context::work> work;
work.emplace(ctx);
using R = typename std::result_of<F(boost::asio::yield_context&)>::type;
if constexpr (!std::is_same<R, void>::value)
{
R res;
boost::asio::spawn(
strand, [&f, &work, &res](boost::asio::yield_context yield) {
res = f(yield);
work.reset();
});
ctx.run();
return res;
}
else
{
boost::asio::spawn(
strand, [&f, &work](boost::asio::yield_context yield) {
f(yield);
work.reset();
});
ctx.run();
}
}
template <class F>
auto
synchronousAndRetryOnTimeout(F&& f)
{
return retryOnTimeout([&]() { return synchronous(f); });
}
class BackendInterface
{
protected:
mutable std::shared_mutex rngMtx_;
std::optional<LedgerRange> range;
SimpleCache cache_;
// mutex used for open() and close()
mutable std::mutex mutex_;
public:
BackendInterface(boost::json::object const& config)
{
}
virtual ~BackendInterface()
{
}
// *** public read methods ***
// All of these reads methods can throw DatabaseTimeout. When writing code
// in an RPC handler, this exception does not need to be caught: when an RPC
// results in a timeout, an error is returned to the client
public:
// *** ledger methods
//
SimpleCache const&
cache() const
{
return cache_;
}
SimpleCache&
cache()
{
return cache_;
}
virtual std::optional<ripple::LedgerInfo>
fetchLedgerBySequence(
std::uint32_t const sequence,
boost::asio::yield_context& yield) const = 0;
virtual std::optional<ripple::LedgerInfo>
fetchLedgerByHash(
ripple::uint256 const& hash,
boost::asio::yield_context& yield) const = 0;
virtual std::optional<std::uint32_t>
fetchLatestLedgerSequence(boost::asio::yield_context& yield) const = 0;
std::optional<LedgerRange>
fetchLedgerRange() const
{
std::shared_lock lck(rngMtx_);
return range;
}
void
updateRange(uint32_t newMax)
{
std::unique_lock lck(rngMtx_);
assert(!range || newMax >= range->maxSequence);
if (!range)
range = {newMax, newMax};
else
range->maxSequence = newMax;
}
std::optional<ripple::Fees>
fetchFees(std::uint32_t const seq, boost::asio::yield_context& yield) const;
// *** transaction methods
virtual std::optional<TransactionAndMetadata>
fetchTransaction(
ripple::uint256 const& hash,
boost::asio::yield_context& yield) const = 0;
virtual std::vector<TransactionAndMetadata>
fetchTransactions(
std::vector<ripple::uint256> const& hashes,
boost::asio::yield_context& yield) const = 0;
virtual AccountTransactions
fetchAccountTransactions(
ripple::AccountID const& account,
std::uint32_t const limit,
bool forward,
std::optional<AccountTransactionsCursor> const& cursor,
boost::asio::yield_context& yield) const = 0;
virtual std::vector<TransactionAndMetadata>
fetchAllTransactionsInLedger(
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const = 0;
virtual std::vector<ripple::uint256>
fetchAllTransactionHashesInLedger(
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const = 0;
// *** state data methods
std::optional<Blob>
fetchLedgerObject(
ripple::uint256 const& key,
std::uint32_t const sequence,
boost::asio::yield_context& yield) const;
std::vector<Blob>
fetchLedgerObjects(
std::vector<ripple::uint256> const& keys,
std::uint32_t const sequence,
boost::asio::yield_context& yield) const;
virtual std::optional<Blob>
doFetchLedgerObject(
ripple::uint256 const& key,
std::uint32_t const sequence,
boost::asio::yield_context& yield) const = 0;
virtual std::vector<Blob>
doFetchLedgerObjects(
std::vector<ripple::uint256> const& keys,
std::uint32_t const sequence,
boost::asio::yield_context& yield) const = 0;
virtual std::vector<LedgerObject>
fetchLedgerDiff(
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const = 0;
// Fetches a page of ledger objects, ordered by key/index.
// Used by ledger_data
LedgerPage
fetchLedgerPage(
std::optional<ripple::uint256> const& cursor,
std::uint32_t const ledgerSequence,
std::uint32_t const limit,
bool outOfOrder,
boost::asio::yield_context& yield) const;
// Fetches the successor to key/index
std::optional<LedgerObject>
fetchSuccessorObject(
ripple::uint256 key,
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const;
std::optional<ripple::uint256>
fetchSuccessorKey(
ripple::uint256 key,
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const;
// Fetches the successor to key/index
virtual std::optional<ripple::uint256>
doFetchSuccessorKey(
ripple::uint256 key,
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const = 0;
BookOffersPage
fetchBookOffers(
ripple::uint256 const& book,
std::uint32_t const ledgerSequence,
std::uint32_t const limit,
std::optional<ripple::uint256> const& cursor,
boost::asio::yield_context& yield) const;
std::optional<LedgerRange>
hardFetchLedgerRange() const
{
return synchronous([&](boost::asio::yield_context yield) {
return hardFetchLedgerRange(yield);
});
}
virtual std::optional<LedgerRange>
hardFetchLedgerRange(boost::asio::yield_context& yield) const = 0;
// Doesn't throw DatabaseTimeout. Should be used with care.
std::optional<LedgerRange>
hardFetchLedgerRangeNoThrow() const;
// Doesn't throw DatabaseTimeout. Should be used with care.
std::optional<LedgerRange>
hardFetchLedgerRangeNoThrow(boost::asio::yield_context& yield) const;
virtual void
writeLedger(
ripple::LedgerInfo const& ledgerInfo,
std::string&& ledgerHeader) = 0;
virtual void
writeLedgerObject(
std::string&& key,
std::uint32_t const seq,
std::string&& blob);
virtual void
writeTransaction(
std::string&& hash,
std::uint32_t const seq,
std::uint32_t const date,
std::string&& transaction,
std::string&& metadata) = 0;
virtual void
writeAccountTransactions(std::vector<AccountTransactionsData>&& data) = 0;
virtual void
writeSuccessor(
std::string&& key,
std::uint32_t const seq,
std::string&& successor) = 0;
// Tell the database we are about to begin writing data for a particular
// ledger.
virtual void
startWrites() const = 0;
// Tell the database we have finished writing all data for a particular
// ledger
// TODO change the return value to represent different results. committed,
// write conflict, errored, successful but not committed
bool
finishWrites(std::uint32_t const ledgerSequence);
virtual bool
doOnlineDelete(
std::uint32_t numLedgersToKeep,
boost::asio::yield_context& yield) const = 0;
// Open the database. Set up all of the necessary objects and
// datastructures. After this call completes, the database is ready for
// use.
virtual void
open(bool readOnly) = 0;
// Close the database, releasing any resources
virtual void
close(){};
// *** private helper methods
private:
virtual void
doWriteLedgerObject(
std::string&& key,
std::uint32_t const seq,
std::string&& blob) = 0;
virtual bool
doFinishWrites() = 0;
};
} // namespace Backend
using BackendInterface = Backend::BackendInterface;
#endif

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,130 +0,0 @@
#ifndef CLIO_BACKEND_DBHELPERS_H_INCLUDED
#define CLIO_BACKEND_DBHELPERS_H_INCLUDED
#include <ripple/basics/Log.h>
#include <ripple/protocol/SField.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/protocol/TxMeta.h>
#include <boost/container/flat_set.hpp>
#include <backend/Pg.h>
#include <backend/Types.h>
/// Struct used to keep track of what to write to transactions and
/// account_transactions tables in Postgres
struct AccountTransactionsData
{
boost::container::flat_set<ripple::AccountID> accounts;
std::uint32_t ledgerSequence;
std::uint32_t transactionIndex;
ripple::uint256 txHash;
AccountTransactionsData(
ripple::TxMeta& meta,
ripple::uint256 const& txHash,
beast::Journal& j)
: accounts(meta.getAffectedAccounts())
, ledgerSequence(meta.getLgrSeq())
, transactionIndex(meta.getIndex())
, txHash(txHash)
{
}
AccountTransactionsData() = default;
};
template <class T>
inline bool
isOffer(T const& object)
{
short offer_bytes = (object[1] << 8) | object[2];
return offer_bytes == 0x006f;
}
template <class T>
inline bool
isOfferHex(T const& object)
{
auto blob = ripple::strUnHex(4, object.begin(), object.begin() + 4);
if (blob)
{
short offer_bytes = ((*blob)[1] << 8) | (*blob)[2];
return offer_bytes == 0x006f;
}
return false;
}
template <class T>
inline bool
isDirNode(T const& object)
{
short spaceKey = (object.data()[1] << 8) | object.data()[2];
return spaceKey == 0x0064;
}
template <class T, class R>
inline bool
isBookDir(T const& key, R const& object)
{
if (!isDirNode(object))
return false;
ripple::STLedgerEntry const sle{
ripple::SerialIter{object.data(), object.size()}, key};
return !sle[~ripple::sfOwner].has_value();
}
template <class T>
inline ripple::uint256
getBook(T const& offer)
{
ripple::SerialIter it{offer.data(), offer.size()};
ripple::SLE sle{it, {}};
ripple::uint256 book = sle.getFieldH256(ripple::sfBookDirectory);
return book;
}
template <class T>
inline ripple::uint256
getBookBase(T const& key)
{
assert(key.size() == ripple::uint256::size());
ripple::uint256 ret;
for (size_t i = 0; i < 24; ++i)
{
ret.data()[i] = key.data()[i];
}
return ret;
}
inline ripple::LedgerInfo
deserializeHeader(ripple::Slice data)
{
ripple::SerialIter sit(data.data(), data.size());
ripple::LedgerInfo info;
info.seq = sit.get32();
info.drops = sit.get64();
info.parentHash = sit.get256();
info.txHash = sit.get256();
info.accountHash = sit.get256();
info.parentCloseTime =
ripple::NetClock::time_point{ripple::NetClock::duration{sit.get32()}};
info.closeTime =
ripple::NetClock::time_point{ripple::NetClock::duration{sit.get32()}};
info.closeTimeResolution = ripple::NetClock::duration{sit.get8()};
info.closeFlags = sit.get8();
info.hash = sit.get256();
return info;
}
inline std::string
uint256ToString(ripple::uint256 const& uint)
{
return {reinterpret_cast<const char*>(uint.data()), uint.size()};
}
static constexpr std::uint32_t rippleEpochStart = 946684800;
#endif

View File

@@ -1,110 +0,0 @@
#include <backend/LayeredCache.h>
namespace Backend {
void
LayeredCache::insert(
ripple::uint256 const& key,
Blob const& value,
uint32_t seq)
{
auto entry = map_[key];
// stale insert, do nothing
if (seq <= entry.recent.seq)
return;
entry.old = entry.recent;
entry.recent = {seq, value};
if (value.empty())
pendingDeletes_.push_back(key);
if (!entry.old.blob.empty())
pendingSweeps_.push_back(key);
}
std::optional<Blob>
LayeredCache::select(CacheEntry const& entry, uint32_t seq) const
{
if (seq < entry.old.seq)
return {};
if (seq < entry.recent.seq && !entry.old.blob.empty())
return entry.old.blob;
if (!entry.recent.blob.empty())
return entry.recent.blob;
return {};
}
void
LayeredCache::update(std::vector<LedgerObject> const& blobs, uint32_t seq)
{
std::unique_lock lck{mtx_};
if (seq > mostRecentSequence_)
mostRecentSequence_ = seq;
for (auto const& k : pendingSweeps_)
{
auto e = map_[k];
e.old = {};
}
for (auto const& k : pendingDeletes_)
{
map_.erase(k);
}
for (auto const& b : blobs)
{
insert(b.key, b.blob, seq);
}
}
std::optional<LedgerObject>
LayeredCache::getSuccessor(ripple::uint256 const& key, uint32_t seq) const
{
ripple::uint256 curKey = key;
while (true)
{
std::shared_lock lck{mtx_};
if (seq < mostRecentSequence_ - 1)
return {};
auto e = map_.upper_bound(curKey);
if (e == map_.end())
return {};
auto const& entry = e->second;
auto blob = select(entry, seq);
if (!blob)
{
curKey = e->first;
continue;
}
else
return {{e->first, *blob}};
}
}
std::optional<LedgerObject>
LayeredCache::getPredecessor(ripple::uint256 const& key, uint32_t seq) const
{
ripple::uint256 curKey = key;
std::shared_lock lck{mtx_};
while (true)
{
if (seq < mostRecentSequence_ - 1)
return {};
auto e = map_.lower_bound(curKey);
--e;
if (e == map_.begin())
return {};
auto const& entry = e->second;
auto blob = select(entry, seq);
if (!blob)
{
curKey = e->first;
continue;
}
else
return {{e->first, *blob}};
}
}
std::optional<Blob>
LayeredCache::get(ripple::uint256 const& key, uint32_t seq) const
{
std::shared_lock lck{mtx_};
auto e = map_.find(key);
if (e == map_.end())
return {};
auto const& entry = e->second;
return select(entry, seq);
}
} // namespace Backend

View File

@@ -1,73 +0,0 @@
#ifndef CLIO_LAYEREDCACHE_H_INCLUDED
#define CLIO_LAYEREDCACHE_H_INCLUDED
#include <ripple/basics/base_uint.h>
#include <backend/Types.h>
#include <map>
#include <mutex>
#include <shared_mutex>
#include <utility>
#include <vector>
namespace Backend {
class LayeredCache
{
struct SeqBlobPair
{
uint32_t seq;
Blob blob;
};
struct CacheEntry
{
SeqBlobPair recent;
SeqBlobPair old;
};
std::map<ripple::uint256, CacheEntry> map_;
std::vector<ripple::uint256> pendingDeletes_;
std::vector<ripple::uint256> pendingSweeps_;
mutable std::shared_mutex mtx_;
uint32_t mostRecentSequence_;
void
insert(ripple::uint256 const& key, Blob const& value, uint32_t seq);
/*
void
insert(ripple::uint256 const& key, Blob const& value, uint32_t seq)
{
map_.emplace(key,{{seq,value,{}});
}
void
update(ripple::uint256 const& key, Blob const& value, uint32_t seq)
{
auto& entry = map_.find(key);
entry.old = entry.recent;
entry.recent = {seq, value};
pendingSweeps_.push_back(key);
}
void
erase(ripple::uint256 const& key, uint32_t seq)
{
update(key, {}, seq);
pendingDeletes_.push_back(key);
}
*/
std::optional<Blob>
select(CacheEntry const& entry, uint32_t seq) const;
public:
void
update(std::vector<LedgerObject> const& blobs, uint32_t seq);
std::optional<Blob>
get(ripple::uint256 const& key, uint32_t seq) const;
std::optional<LedgerObject>
getSuccessor(ripple::uint256 const& key, uint32_t seq) const;
std::optional<LedgerObject>
getPredecessor(ripple::uint256 const& key, uint32_t seq) const;
};
} // namespace Backend
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,571 +0,0 @@
#ifndef RIPPLE_CORE_PG_H_INCLUDED
#define RIPPLE_CORE_PG_H_INCLUDED
#include <ripple/basics/StringUtilities.h>
#include <ripple/basics/chrono.h>
#include <ripple/ledger/ReadView.h>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/icl/closed_interval.hpp>
#include <boost/json.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/log/trivial.hpp>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <libpq-fe.h>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
// These postgres structs must be freed only by the postgres API.
using pg_result_type = std::unique_ptr<PGresult, void (*)(PGresult*)>;
using pg_connection_type = std::unique_ptr<PGconn, void (*)(PGconn*)>;
using asio_socket_type = std::unique_ptr<
boost::asio::ip::tcp::socket,
void (*)(boost::asio::ip::tcp::socket*)>;
/** first: command
* second: parameter values
*
* The 2nd member takes an optional string to
* distinguish between NULL parameters and empty strings. An empty
* item corresponds to a NULL parameter.
*
* Postgres reads each parameter as a c-string, regardless of actual type.
* Binary types (bytea) need to be converted to hex and prepended with
* \x ("\\x").
*/
using pg_params =
std::pair<char const*, std::vector<std::optional<std::string>>>;
/** Parameter values for pg API. */
using pg_formatted_params = std::vector<char const*>;
/** Parameters for managing postgres connections. */
struct PgConfig
{
/** Maximum connections allowed to db. */
std::size_t max_connections{1000};
/** Close idle connections past this duration. */
std::chrono::seconds timeout{600};
/** Index of DB connection parameter names. */
std::vector<char const*> keywordsIdx;
/** DB connection parameter names. */
std::vector<std::string> keywords;
/** Index of DB connection parameter values. */
std::vector<char const*> valuesIdx;
/** DB connection parameter values. */
std::vector<std::string> values;
};
//-----------------------------------------------------------------------------
/** Class that operates on postgres query results.
*
* The functions that return results do not check first whether the
* expected results are actually there. Therefore, the caller first needs
* to check whether or not a valid response was returned using the operator
* bool() overload. If number of tuples or fields are unknown, then check
* those. Each result field should be checked for null before attempting
* to return results. Finally, the caller must know the type of the field
* before calling the corresponding function to return a field. Postgres
* internally stores each result field as null-terminated strings.
*/
class PgResult
{
// The result object must be freed using the libpq API PQclear() call.
pg_result_type result_{nullptr, [](PGresult* result) { PQclear(result); }};
std::optional<std::pair<ExecStatusType, std::string>> error_;
public:
/** Constructor for when the process is stopping.
*
*/
PgResult()
{
}
/** Constructor for successful query results.
*
* @param result Query result.
*/
explicit PgResult(pg_result_type&& result) : result_(std::move(result))
{
}
/** Constructor for failed query results.
*
* @param result Query result that contains error information.
* @param conn Postgres connection that contains error information.
*/
PgResult(PGresult* result, PGconn* conn)
: error_({PQresultStatus(result), PQerrorMessage(conn)})
{
}
/** Return field as a null-terminated string pointer.
*
* Note that this function does not guarantee that the result struct
* exists, or that the row and fields exist, or that the field is
* not null.
*
* @param ntuple Row number.
* @param nfield Field number.
* @return Field contents.
*/
char const*
c_str(int ntuple = 0, int nfield = 0) const
{
return PQgetvalue(result_.get(), ntuple, nfield);
}
std::vector<unsigned char>
asUnHexedBlob(int ntuple = 0, int nfield = 0) const
{
std::string_view view{c_str(ntuple, nfield) + 2};
auto res = ripple::strUnHex(view.size(), view.cbegin(), view.cend());
if (res)
return *res;
return {};
}
ripple::uint256
asUInt256(int ntuple = 0, int nfield = 0) const
{
ripple::uint256 val;
if (!val.parseHex(c_str(ntuple, nfield) + 2))
throw std::runtime_error("Pg - failed to parse hex into uint256");
return val;
}
/** Return field as equivalent to Postgres' INT type (32 bit signed).
*
* Note that this function does not guarantee that the result struct
* exists, or that the row and fields exist, or that the field is
* not null, or that the type is that requested.
* @param ntuple Row number.
* @param nfield Field number.
* @return Field contents.
*/
std::int32_t
asInt(int ntuple = 0, int nfield = 0) const
{
return boost::lexical_cast<std::int32_t>(
PQgetvalue(result_.get(), ntuple, nfield));
}
/** Return field as equivalent to Postgres' BIGINT type (64 bit signed).
*
* Note that this function does not guarantee that the result struct
* exists, or that the row and fields exist, or that the field is
* not null, or that the type is that requested.
* @param ntuple Row number.
* @param nfield Field number.
* @return Field contents.
*/
std::int64_t
asBigInt(int ntuple = 0, int nfield = 0) const
{
return boost::lexical_cast<std::int64_t>(
PQgetvalue(result_.get(), ntuple, nfield));
}
/** Returns whether the field is NULL or not.
*
* Note that this function does not guarantee that the result struct
* exists, or that the row and fields exist.
*
* @param ntuple Row number.
* @param nfield Field number.
* @return Whether field is NULL.
*/
bool
isNull(int ntuple = 0, int nfield = 0) const
{
return PQgetisnull(result_.get(), ntuple, nfield);
}
/** Check whether a valid response occurred.
*
* @return Whether or not the query returned a valid response.
*/
operator bool() const
{
return result_ != nullptr;
}
/** Message describing the query results suitable for diagnostics.
*
* If error, then the postgres error type and message are returned.
* Otherwise, "ok"
*
* @return Query result message.
*/
std::string
msg() const;
/** Get number of rows in result.
*
* Note that this function does not guarantee that the result struct
* exists.
*
* @return Number of result rows.
*/
int
ntuples() const
{
return PQntuples(result_.get());
}
/** Get number of fields in result.
*
* Note that this function does not guarantee that the result struct
* exists.
*
* @return Number of result fields.
*/
int
nfields() const
{
return PQnfields(result_.get());
}
/** Return result status of the command.
*
* Note that this function does not guarantee that the result struct
* exists.
*
* @return
*/
ExecStatusType
status() const
{
return PQresultStatus(result_.get());
}
};
/* Class that contains and operates upon a postgres connection. */
class Pg
{
friend class PgPool;
friend class PgQuery;
PgConfig const& config_;
boost::asio::io_context::strand strand_;
bool& stop_;
std::mutex& mutex_;
asio_socket_type socket_{nullptr, [](boost::asio::ip::tcp::socket*) {}};
// The connection object must be freed using the libpq API PQfinish() call.
pg_connection_type conn_{nullptr, [](PGconn* conn) { PQfinish(conn); }};
inline asio_socket_type
getSocket(boost::asio::yield_context& strand);
inline PgResult
waitForStatus(boost::asio::yield_context& yield, ExecStatusType expected);
inline void
flush(boost::asio::yield_context& yield);
/** Clear results from the connection.
*
* Results from previous commands must be cleared before new commands
* can be processed. This function should be called on connections
* that weren't processed completely before being reused, such as
* when being checked-in.
*
* @return whether or not connection still exists.
*/
bool
clear();
/** Connect to postgres.
*
* Idempotently connects to postgres by first checking whether an
* existing connection is already present. If connection is not present
* or in an errored state, reconnects to the database.
*/
void
connect(boost::asio::yield_context& yield);
/** Disconnect from postgres. */
void
disconnect()
{
conn_.reset();
socket_.reset();
}
/** Execute postgres query.
*
* If parameters are included, then the command should contain only a
* single SQL statement. If no parameters, then multiple SQL statements
* delimited by semi-colons can be processed. The response is from
* the last command executed.
*
* @param command postgres API command string.
* @param nParams postgres API number of parameters.
* @param values postgres API array of parameter.
* @return Query result object.
*/
PgResult
query(
char const* command,
std::size_t const nParams,
char const* const* values,
boost::asio::yield_context& yield);
/** Execute postgres query with no parameters.
*
* @param command Query string.
* @return Query result object;
*/
PgResult
query(char const* command, boost::asio::yield_context& yield)
{
return query(command, 0, nullptr, yield);
}
/** Execute postgres query with parameters.
*
* @param dbParams Database command and parameter values.
* @return Query result object.
*/
PgResult
query(pg_params const& dbParams, boost::asio::yield_context& yield);
/** Insert multiple records into a table using Postgres' bulk COPY.
*
* Throws upon error.
*
* @param table Name of table for import.
* @param records Records in the COPY IN format.
*/
void
bulkInsert(
char const* table,
std::string const& records,
boost::asio::yield_context& yield);
public:
/** Constructor for Pg class.
*
* @param config Config parameters.
* @param j Logger object.
* @param stop Reference to connection pool's stop flag.
* @param mutex Reference to connection pool's mutex.
*/
Pg(PgConfig const& config,
boost::asio::io_context& ctx,
bool& stop,
std::mutex& mutex)
: config_(config), strand_(ctx), stop_(stop), mutex_(mutex)
{
}
};
//-----------------------------------------------------------------------------
/** Database connection pool.
*
* Allow re-use of postgres connections. Postgres connections are created
* as needed until configurable limit is reached. After use, each connection
* is placed in a container ordered by time of use. Each request for
* a connection grabs the most recently used connection from the container.
* If none are available, a new connection is used (up to configured limit).
* Idle connections are destroyed periodically after configurable
* timeout duration.
*
* This should be stored as a shared pointer so PgQuery objects can safely
* outlive it.
*/
class PgPool
{
friend class PgQuery;
using clock_type = std::chrono::steady_clock;
boost::asio::io_context& ioc_;
PgConfig config_;
std::mutex mutex_;
std::condition_variable cond_;
std::size_t connections_{};
bool stop_{false};
/** Idle database connections ordered by timestamp to allow timing out. */
std::multimap<std::chrono::time_point<clock_type>, std::unique_ptr<Pg>>
idle_;
/** Get a postgres connection object.
*
* Return the most recent idle connection in the pool, if available.
* Otherwise, return a new connection unless we're at the threshold.
* If so, then wait until a connection becomes available.
*
* @return Postgres object.
*/
std::unique_ptr<Pg>
checkout();
/** Return a postgres object to the pool for reuse.
*
* If connection is healthy, place in pool for reuse. After calling this,
* the container no longer have a connection unless checkout() is called.
*
* @param pg Pg object.
*/
void
checkin(std::unique_ptr<Pg>& pg);
public:
/** Connection pool constructor.
*
* @param pgConfig Postgres config.
* @param j Logger object.
* @param parent Stoppable parent.
*/
PgPool(boost::asio::io_context& ioc, boost::json::object const& config);
~PgPool()
{
onStop();
}
PgConfig&
config()
{
return config_;
}
/** Initiate idle connection timer.
*
* The PgPool object needs to be fully constructed to support asynchronous
* operations.
*/
void
setup();
/** Prepare for process shutdown. (Stoppable) */
void
onStop();
};
//-----------------------------------------------------------------------------
/** Class to query postgres.
*
* This class should be used by functions outside of this
* compilation unit for querying postgres. It automatically acquires and
* relinquishes a database connection to handle each query.
*/
class PgQuery
{
private:
std::shared_ptr<PgPool> pool_;
std::unique_ptr<Pg> pg_;
public:
PgQuery() = delete;
PgQuery(std::shared_ptr<PgPool> const& pool)
: pool_(pool), pg_(pool->checkout())
{
}
~PgQuery()
{
pool_->checkin(pg_);
}
// TODO. add sendQuery and getResult, for sending the query and getting the
// result asynchronously. This could be useful for sending a bunch of
// requests concurrently
/** Execute postgres query with parameters.
*
* @param dbParams Database command with parameters.
* @return Result of query, including errors.
*/
PgResult
operator()(pg_params const& dbParams, boost::asio::yield_context& yield)
{
if (!pg_) // It means we're stopping. Return empty result.
return PgResult();
return pg_->query(dbParams, yield);
}
/** Execute postgres query with only command statement.
*
* @param command Command statement.
* @return Result of query, including errors.
*/
PgResult
operator()(char const* command, boost::asio::yield_context& yield)
{
return operator()(pg_params{command, {}}, yield);
}
/** Insert multiple records into a table using Postgres' bulk COPY.
*
* Throws upon error.
*
* @param table Name of table for import.
* @param records Records in the COPY IN format.
*/
void
bulkInsert(
char const* table,
std::string const& records,
boost::asio::yield_context& yield)
{
pg_->bulkInsert(table, records, yield);
}
};
//-----------------------------------------------------------------------------
/** Create Postgres connection pool manager.
*
* @param pgConfig Configuration for Postgres.
* @param j Logger object.
* @param parent Stoppable parent object.
* @return Postgres connection pool manager
*/
std::shared_ptr<PgPool>
make_PgPool(boost::asio::io_context& ioc, boost::json::object const& pgConfig);
/** Initialize the Postgres schema.
*
* This function ensures that the database is running the latest version
* of the schema.
*
* @param pool Postgres connection pool manager.
*/
void
initSchema(std::shared_ptr<PgPool> const& pool);
void
initAccountTx(std::shared_ptr<PgPool> const& pool);
// Load the ledger info for the specified ledger/s from the database
// @param whichLedger specifies the ledger to load via ledger sequence, ledger
// hash or std::monostate (which loads the most recent)
// @return vector of LedgerInfos
std::optional<ripple::LedgerInfo>
getLedger(
std::variant<std::monostate, ripple::uint256, std::uint32_t> const&
whichLedger,
std::shared_ptr<PgPool>& pgPool);
#endif // RIPPLE_CORE_PG_H_INCLUDED

View File

@@ -1,860 +0,0 @@
#include <boost/asio.hpp>
#include <boost/format.hpp>
#include <backend/PostgresBackend.h>
#include <thread>
namespace Backend {
// Type alias for async completion handlers
using completion_token = boost::asio::yield_context;
using function_type = void(boost::system::error_code);
using result_type = boost::asio::async_result<completion_token, function_type>;
using handler_type = typename result_type::completion_handler_type;
struct HandlerWrapper
{
handler_type handler;
HandlerWrapper(handler_type&& handler_) : handler(std::move(handler_))
{
}
};
PostgresBackend::PostgresBackend(
boost::asio::io_context& ioc,
boost::json::object const& config)
: BackendInterface(config)
, pgPool_(make_PgPool(ioc, config))
, writeConnection_(pgPool_)
{
if (config.contains("write_interval"))
{
writeInterval_ = config.at("write_interval").as_int64();
}
}
void
PostgresBackend::writeLedger(
ripple::LedgerInfo const& ledgerInfo,
std::string&& ledgerHeader)
{
synchronous([&](boost::asio::yield_context yield) {
auto cmd = boost::format(
R"(INSERT INTO ledgers
VALUES (%u,'\x%s', '\x%s',%u,%u,%u,%u,%u,'\x%s','\x%s'))");
auto ledgerInsert = boost::str(
cmd % ledgerInfo.seq % ripple::strHex(ledgerInfo.hash) %
ripple::strHex(ledgerInfo.parentHash) % ledgerInfo.drops.drops() %
ledgerInfo.closeTime.time_since_epoch().count() %
ledgerInfo.parentCloseTime.time_since_epoch().count() %
ledgerInfo.closeTimeResolution.count() % ledgerInfo.closeFlags %
ripple::strHex(ledgerInfo.accountHash) %
ripple::strHex(ledgerInfo.txHash));
auto res = writeConnection_(ledgerInsert.data(), yield);
abortWrite_ = !res;
inProcessLedger = ledgerInfo.seq;
});
}
void
PostgresBackend::writeAccountTransactions(
std::vector<AccountTransactionsData>&& data)
{
if (abortWrite_)
return;
PgQuery pg(pgPool_);
for (auto const& record : data)
{
for (auto const& a : record.accounts)
{
std::string acct = ripple::strHex(a);
accountTxBuffer_ << "\\\\x" << acct << '\t'
<< std::to_string(record.ledgerSequence) << '\t'
<< std::to_string(record.transactionIndex) << '\t'
<< "\\\\x" << ripple::strHex(record.txHash)
<< '\n';
}
}
}
void
PostgresBackend::doWriteLedgerObject(
std::string&& key,
std::uint32_t const seq,
std::string&& blob)
{
synchronous([&](boost::asio::yield_context yield) {
if (abortWrite_)
return;
objectsBuffer_ << "\\\\x" << ripple::strHex(key) << '\t'
<< std::to_string(seq) << '\t' << "\\\\x"
<< ripple::strHex(blob) << '\n';
numRowsInObjectsBuffer_++;
// If the buffer gets too large, the insert fails. Not sure why. So we
// insert after 1 million records
if (numRowsInObjectsBuffer_ % writeInterval_ == 0)
{
BOOST_LOG_TRIVIAL(info)
<< __func__ << " Flushing large buffer. num objects = "
<< numRowsInObjectsBuffer_;
writeConnection_.bulkInsert("objects", objectsBuffer_.str(), yield);
BOOST_LOG_TRIVIAL(info) << __func__ << " Flushed large buffer";
objectsBuffer_.str("");
}
});
}
void
PostgresBackend::writeSuccessor(
std::string&& key,
std::uint32_t const seq,
std::string&& successor)
{
synchronous([&](boost::asio::yield_context yield) {
if (range)
{
if (successors_.count(key) > 0)
return;
successors_.insert(key);
}
successorBuffer_ << "\\\\x" << ripple::strHex(key) << '\t'
<< std::to_string(seq) << '\t' << "\\\\x"
<< ripple::strHex(successor) << '\n';
BOOST_LOG_TRIVIAL(trace)
<< __func__ << ripple::strHex(key) << " - " << std::to_string(seq);
numRowsInSuccessorBuffer_++;
if (numRowsInSuccessorBuffer_ % writeInterval_ == 0)
{
BOOST_LOG_TRIVIAL(info)
<< __func__ << " Flushing large buffer. num successors = "
<< numRowsInSuccessorBuffer_;
writeConnection_.bulkInsert(
"successor", successorBuffer_.str(), yield);
BOOST_LOG_TRIVIAL(info) << __func__ << " Flushed large buffer";
successorBuffer_.str("");
}
});
}
void
PostgresBackend::writeTransaction(
std::string&& hash,
std::uint32_t const seq,
std::uint32_t const date,
std::string&& transaction,
std::string&& metadata)
{
if (abortWrite_)
return;
transactionsBuffer_ << "\\\\x" << ripple::strHex(hash) << '\t'
<< std::to_string(seq) << '\t' << std::to_string(date)
<< '\t' << "\\\\x" << ripple::strHex(transaction)
<< '\t' << "\\\\x" << ripple::strHex(metadata) << '\n';
}
std::uint32_t
checkResult(PgResult const& res, std::uint32_t const numFieldsExpected)
{
if (!res)
{
auto msg = res.msg();
BOOST_LOG_TRIVIAL(error) << __func__ << " - " << msg;
if (msg.find("statement timeout"))
throw DatabaseTimeout();
assert(false);
throw DatabaseTimeout();
}
if (res.status() != PGRES_TUPLES_OK)
{
std::stringstream msg;
msg << " : Postgres response should have been "
"PGRES_TUPLES_OK but instead was "
<< res.status() << " - msg = " << res.msg();
BOOST_LOG_TRIVIAL(error) << __func__ << " - " << msg.str();
assert(false);
throw DatabaseTimeout();
}
BOOST_LOG_TRIVIAL(trace)
<< __func__ << " Postgres result msg : " << res.msg();
if (res.isNull() || res.ntuples() == 0)
{
return 0;
}
else if (res.ntuples() > 0)
{
if (res.nfields() != numFieldsExpected)
{
std::stringstream msg;
msg << "Wrong number of fields in Postgres "
"response. Expected "
<< numFieldsExpected << ", but got " << res.nfields();
throw std::runtime_error(msg.str());
assert(false);
}
}
return res.ntuples();
}
ripple::LedgerInfo
parseLedgerInfo(PgResult const& res)
{
std::int64_t ledgerSeq = res.asBigInt(0, 0);
ripple::uint256 hash = res.asUInt256(0, 1);
ripple::uint256 prevHash = res.asUInt256(0, 2);
std::int64_t totalCoins = res.asBigInt(0, 3);
std::int64_t closeTime = res.asBigInt(0, 4);
std::int64_t parentCloseTime = res.asBigInt(0, 5);
std::int64_t closeTimeRes = res.asBigInt(0, 6);
std::int64_t closeFlags = res.asBigInt(0, 7);
ripple::uint256 accountHash = res.asUInt256(0, 8);
ripple::uint256 txHash = res.asUInt256(0, 9);
using time_point = ripple::NetClock::time_point;
using duration = ripple::NetClock::duration;
ripple::LedgerInfo info;
info.seq = ledgerSeq;
info.hash = hash;
info.parentHash = prevHash;
info.drops = totalCoins;
info.closeTime = time_point{duration{closeTime}};
info.parentCloseTime = time_point{duration{parentCloseTime}};
info.closeFlags = closeFlags;
info.closeTimeResolution = duration{closeTimeRes};
info.accountHash = accountHash;
info.txHash = txHash;
info.validated = true;
return info;
}
std::optional<std::uint32_t>
PostgresBackend::fetchLatestLedgerSequence(
boost::asio::yield_context& yield) const
{
PgQuery pgQuery(pgPool_);
pgQuery(set_timeout, yield);
auto const query =
"SELECT ledger_seq FROM ledgers ORDER BY ledger_seq DESC LIMIT 1";
if (auto res = pgQuery(query, yield); checkResult(res, 1))
return res.asBigInt(0, 0);
return {};
}
std::optional<ripple::LedgerInfo>
PostgresBackend::fetchLedgerBySequence(
std::uint32_t const sequence,
boost::asio::yield_context& yield) const
{
PgQuery pgQuery(pgPool_);
pgQuery(set_timeout, yield);
std::stringstream sql;
sql << "SELECT * FROM ledgers WHERE ledger_seq = "
<< std::to_string(sequence);
if (auto res = pgQuery(sql.str().data(), yield); checkResult(res, 10))
return parseLedgerInfo(res);
return {};
}
std::optional<ripple::LedgerInfo>
PostgresBackend::fetchLedgerByHash(
ripple::uint256 const& hash,
boost::asio::yield_context& yield) const
{
PgQuery pgQuery(pgPool_);
pgQuery(set_timeout, yield);
std::stringstream sql;
sql << "SELECT * FROM ledgers WHERE ledger_hash = \'\\x"
<< ripple::to_string(hash) << "\'";
if (auto res = pgQuery(sql.str().data(), yield); checkResult(res, 10))
return parseLedgerInfo(res);
return {};
}
std::optional<LedgerRange>
PostgresBackend::hardFetchLedgerRange(boost::asio::yield_context& yield) const
{
auto range = PgQuery(pgPool_)("SELECT complete_ledgers()", yield);
if (!range)
return {};
std::string res{range.c_str()};
BOOST_LOG_TRIVIAL(debug) << "range is = " << res;
try
{
size_t minVal = 0;
size_t maxVal = 0;
if (res == "empty" || res == "error" || res.empty())
return {};
else if (size_t delim = res.find('-'); delim != std::string::npos)
{
minVal = std::stol(res.substr(0, delim));
maxVal = std::stol(res.substr(delim + 1));
}
else
{
minVal = maxVal = std::stol(res);
}
return LedgerRange{minVal, maxVal};
}
catch (std::exception&)
{
BOOST_LOG_TRIVIAL(error)
<< __func__ << " : "
<< "Error parsing result of getCompleteLedgers()";
}
return {};
}
std::optional<Blob>
PostgresBackend::doFetchLedgerObject(
ripple::uint256 const& key,
std::uint32_t const sequence,
boost::asio::yield_context& yield) const
{
PgQuery pgQuery(pgPool_);
pgQuery(set_timeout, yield);
std::stringstream sql;
sql << "SELECT object FROM objects WHERE key = "
<< "\'\\x" << ripple::strHex(key) << "\'"
<< " AND ledger_seq <= " << std::to_string(sequence)
<< " ORDER BY ledger_seq DESC LIMIT 1";
if (auto res = pgQuery(sql.str().data(), yield); checkResult(res, 1))
{
auto blob = res.asUnHexedBlob(0, 0);
if (blob.size())
return blob;
}
return {};
}
// returns a transaction, metadata pair
std::optional<TransactionAndMetadata>
PostgresBackend::fetchTransaction(
ripple::uint256 const& hash,
boost::asio::yield_context& yield) const
{
PgQuery pgQuery(pgPool_);
pgQuery(set_timeout, yield);
std::stringstream sql;
sql << "SELECT transaction,metadata,ledger_seq,date FROM transactions "
"WHERE hash = "
<< "\'\\x" << ripple::strHex(hash) << "\'";
if (auto res = pgQuery(sql.str().data(), yield); checkResult(res, 4))
{
return {
{res.asUnHexedBlob(0, 0),
res.asUnHexedBlob(0, 1),
res.asBigInt(0, 2),
res.asBigInt(0, 3)}};
}
return {};
}
std::vector<TransactionAndMetadata>
PostgresBackend::fetchAllTransactionsInLedger(
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const
{
PgQuery pgQuery(pgPool_);
pgQuery(set_timeout, yield);
std::stringstream sql;
sql << "SELECT transaction, metadata, ledger_seq,date FROM transactions "
"WHERE "
<< "ledger_seq = " << std::to_string(ledgerSequence);
auto res = pgQuery(sql.str().data(), yield);
if (size_t numRows = checkResult(res, 4))
{
std::vector<TransactionAndMetadata> txns;
for (size_t i = 0; i < numRows; ++i)
{
txns.push_back(
{res.asUnHexedBlob(i, 0),
res.asUnHexedBlob(i, 1),
res.asBigInt(i, 2),
res.asBigInt(i, 3)});
}
return txns;
}
return {};
}
std::vector<ripple::uint256>
PostgresBackend::fetchAllTransactionHashesInLedger(
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const
{
PgQuery pgQuery(pgPool_);
pgQuery(set_timeout, yield);
std::stringstream sql;
sql << "SELECT hash FROM transactions WHERE "
<< "ledger_seq = " << std::to_string(ledgerSequence);
auto res = pgQuery(sql.str().data(), yield);
if (size_t numRows = checkResult(res, 1))
{
std::vector<ripple::uint256> hashes;
for (size_t i = 0; i < numRows; ++i)
{
hashes.push_back(res.asUInt256(i, 0));
}
return hashes;
}
return {};
}
std::optional<ripple::uint256>
PostgresBackend::doFetchSuccessorKey(
ripple::uint256 key,
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const
{
PgQuery pgQuery(pgPool_);
pgQuery(set_timeout, yield);
std::stringstream sql;
sql << "SELECT next FROM successor WHERE key = "
<< "\'\\x" << ripple::strHex(key) << "\'"
<< " AND ledger_seq <= " << std::to_string(ledgerSequence)
<< " ORDER BY ledger_seq DESC LIMIT 1";
if (auto res = pgQuery(sql.str().data(), yield); checkResult(res, 1))
{
auto next = res.asUInt256(0, 0);
if (next == lastKey)
return {};
return next;
}
return {};
}
std::vector<TransactionAndMetadata>
PostgresBackend::fetchTransactions(
std::vector<ripple::uint256> const& hashes,
boost::asio::yield_context& yield) const
{
if (!hashes.size())
return {};
std::vector<TransactionAndMetadata> results;
results.resize(hashes.size());
handler_type handler(std::forward<decltype(yield)>(yield));
result_type result(handler);
auto hw = new HandlerWrapper(std::move(handler));
auto start = std::chrono::system_clock::now();
std::atomic_uint numRemaining = hashes.size();
std::atomic_bool errored = false;
for (size_t i = 0; i < hashes.size(); ++i)
{
auto const& hash = hashes[i];
boost::asio::spawn(
get_associated_executor(yield),
[this, &hash, &results, hw, &numRemaining, &errored, i](
boost::asio::yield_context yield) {
BOOST_LOG_TRIVIAL(trace) << __func__ << " getting txn = " << i;
PgQuery pgQuery(pgPool_);
std::stringstream sql;
sql << "SELECT transaction,metadata,ledger_seq,date FROM "
"transactions "
"WHERE HASH = \'\\x"
<< ripple::strHex(hash) << "\'";
try
{
if (auto const res = pgQuery(sql.str().data(), yield);
checkResult(res, 4))
{
results[i] = {
res.asUnHexedBlob(0, 0),
res.asUnHexedBlob(0, 1),
res.asBigInt(0, 2),
res.asBigInt(0, 3)};
}
}
catch (DatabaseTimeout const&)
{
errored = true;
}
if (--numRemaining == 0)
{
handler_type h(std::move(hw->handler));
h(boost::system::error_code{});
}
});
}
// Yields the worker to the io_context until handler is called.
result.get();
delete hw;
auto end = std::chrono::system_clock::now();
auto duration =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
BOOST_LOG_TRIVIAL(info)
<< __func__ << " fetched " << std::to_string(hashes.size())
<< " transactions asynchronously. took "
<< std::to_string(duration.count());
if (errored)
{
BOOST_LOG_TRIVIAL(error) << __func__ << " Database fetch timed out";
throw DatabaseTimeout();
}
return results;
}
std::vector<Blob>
PostgresBackend::doFetchLedgerObjects(
std::vector<ripple::uint256> const& keys,
std::uint32_t const sequence,
boost::asio::yield_context& yield) const
{
if (!keys.size())
return {};
PgQuery pgQuery(pgPool_);
pgQuery(set_timeout, yield);
std::vector<Blob> results;
results.resize(keys.size());
handler_type handler(std::forward<decltype(yield)>(yield));
result_type result(handler);
auto hw = new HandlerWrapper(std::move(handler));
std::atomic_uint numRemaining = keys.size();
std::atomic_bool errored = false;
auto start = std::chrono::system_clock::now();
for (size_t i = 0; i < keys.size(); ++i)
{
auto const& key = keys[i];
boost::asio::spawn(
boost::asio::get_associated_executor(yield),
[this, &key, &results, &numRemaining, &errored, hw, i, sequence](
boost::asio::yield_context yield) {
PgQuery pgQuery(pgPool_);
std::stringstream sql;
sql << "SELECT object FROM "
"objects "
"WHERE key = \'\\x"
<< ripple::strHex(key) << "\'"
<< " AND ledger_seq <= " << std::to_string(sequence)
<< " ORDER BY ledger_seq DESC LIMIT 1";
try
{
if (auto const res = pgQuery(sql.str().data(), yield);
checkResult(res, 1))
results[i] = res.asUnHexedBlob();
}
catch (DatabaseTimeout const& ex)
{
errored = true;
}
if (--numRemaining == 0)
{
handler_type h(std::move(hw->handler));
h(boost::system::error_code{});
}
});
}
// Yields the worker to the io_context until handler is called.
result.get();
delete hw;
auto end = std::chrono::system_clock::now();
auto duration =
std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
BOOST_LOG_TRIVIAL(info)
<< __func__ << " fetched " << std::to_string(keys.size())
<< " objects asynchronously. ms = " << std::to_string(duration.count());
if (errored)
{
BOOST_LOG_TRIVIAL(error) << __func__ << " Database fetch timed out";
throw DatabaseTimeout();
}
return results;
}
std::vector<LedgerObject>
PostgresBackend::fetchLedgerDiff(
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const
{
PgQuery pgQuery(pgPool_);
pgQuery(set_timeout, yield);
std::stringstream sql;
sql << "SELECT key,object FROM objects "
"WHERE "
<< "ledger_seq = " << std::to_string(ledgerSequence);
auto res = pgQuery(sql.str().data(), yield);
if (size_t numRows = checkResult(res, 2))
{
std::vector<LedgerObject> objects;
for (size_t i = 0; i < numRows; ++i)
{
objects.push_back({res.asUInt256(i, 0), res.asUnHexedBlob(i, 1)});
}
return objects;
}
return {};
}
AccountTransactions
PostgresBackend::fetchAccountTransactions(
ripple::AccountID const& account,
std::uint32_t const limit,
bool forward,
std::optional<AccountTransactionsCursor> const& cursor,
boost::asio::yield_context& yield) const
{
PgQuery pgQuery(pgPool_);
pgQuery(set_timeout, yield);
pg_params dbParams;
char const*& command = dbParams.first;
std::vector<std::optional<std::string>>& values = dbParams.second;
command =
"SELECT account_tx($1::bytea, $2::bigint, $3::bool, "
"$4::bigint, $5::bigint)";
values.resize(5);
values[0] = "\\x" + strHex(account);
values[1] = std::to_string(limit);
values[2] = std::to_string(forward);
if (cursor)
{
values[3] = std::to_string(cursor->ledgerSequence);
values[4] = std::to_string(cursor->transactionIndex);
}
for (size_t i = 0; i < values.size(); ++i)
{
BOOST_LOG_TRIVIAL(debug) << "value " << std::to_string(i) << " = "
<< (values[i] ? values[i].value() : "null");
}
auto start = std::chrono::system_clock::now();
auto res = pgQuery(dbParams, yield);
auto end = std::chrono::system_clock::now();
auto duration = ((end - start).count()) / 1000000000.0;
BOOST_LOG_TRIVIAL(info)
<< __func__ << " : executed stored_procedure in "
<< std::to_string(duration)
<< " num records = " << std::to_string(checkResult(res, 1));
checkResult(res, 1);
char const* resultStr = res.c_str();
BOOST_LOG_TRIVIAL(debug) << __func__ << " : "
<< "postgres result = " << resultStr
<< " : account = " << strHex(account);
boost::json::value raw = boost::json::parse(resultStr);
boost::json::object responseObj = raw.as_object();
BOOST_LOG_TRIVIAL(debug) << " parsed = " << responseObj;
if (responseObj.contains("transactions"))
{
auto txns = responseObj.at("transactions").as_array();
std::vector<ripple::uint256> hashes;
for (auto& hashHex : txns)
{
ripple::uint256 hash;
if (hash.parseHex(hashHex.at("hash").as_string().c_str() + 2))
hashes.push_back(hash);
}
if (responseObj.contains("cursor"))
{
return {
fetchTransactions(hashes, yield),
{{responseObj.at("cursor").at("ledger_sequence").as_int64(),
responseObj.at("cursor")
.at("transaction_index")
.as_int64()}}};
}
return {fetchTransactions(hashes, yield), {}};
}
return {{}, {}};
} // namespace Backend
void
PostgresBackend::open(bool readOnly)
{
initSchema(pgPool_);
initAccountTx(pgPool_);
}
void
PostgresBackend::close()
{
}
void
PostgresBackend::startWrites() const
{
synchronous([&](boost::asio::yield_context yield) {
numRowsInObjectsBuffer_ = 0;
abortWrite_ = false;
auto res = writeConnection_("BEGIN", yield);
if (!res || res.status() != PGRES_COMMAND_OK)
{
std::stringstream msg;
msg << "Postgres error creating transaction: " << res.msg();
throw std::runtime_error(msg.str());
}
});
}
bool
PostgresBackend::doFinishWrites()
{
synchronous([&](boost::asio::yield_context yield) {
if (!abortWrite_)
{
std::string txStr = transactionsBuffer_.str();
writeConnection_.bulkInsert("transactions", txStr, yield);
writeConnection_.bulkInsert(
"account_transactions", accountTxBuffer_.str(), yield);
std::string objectsStr = objectsBuffer_.str();
if (objectsStr.size())
writeConnection_.bulkInsert("objects", objectsStr, yield);
BOOST_LOG_TRIVIAL(debug)
<< __func__ << " objects size = " << objectsStr.size()
<< " txns size = " << txStr.size();
std::string successorStr = successorBuffer_.str();
if (successorStr.size())
writeConnection_.bulkInsert("successor", successorStr, yield);
if (!range)
{
std::stringstream indexCreate;
indexCreate
<< "CREATE INDEX diff ON objects USING hash(ledger_seq) "
"WHERE NOT "
"ledger_seq = "
<< std::to_string(inProcessLedger);
writeConnection_(indexCreate.str().data(), yield);
}
}
auto res = writeConnection_("COMMIT", yield);
if (!res || res.status() != PGRES_COMMAND_OK)
{
std::stringstream msg;
msg << "Postgres error committing transaction: " << res.msg();
throw std::runtime_error(msg.str());
}
transactionsBuffer_.str("");
transactionsBuffer_.clear();
objectsBuffer_.str("");
objectsBuffer_.clear();
successorBuffer_.str("");
successorBuffer_.clear();
successors_.clear();
accountTxBuffer_.str("");
accountTxBuffer_.clear();
numRowsInObjectsBuffer_ = 0;
});
return !abortWrite_;
}
bool
PostgresBackend::doOnlineDelete(
std::uint32_t const numLedgersToKeep,
boost::asio::yield_context& yield) const
{
auto rng = fetchLedgerRange();
if (!rng)
return false;
std::uint32_t minLedger = rng->maxSequence - numLedgersToKeep;
if (minLedger <= rng->minSequence)
return false;
PgQuery pgQuery(pgPool_);
pgQuery("SET statement_timeout TO 0", yield);
std::optional<ripple::uint256> cursor;
while (true)
{
auto [objects, curCursor] = retryOnTimeout([&]() {
return fetchLedgerPage(cursor, minLedger, 256, false, yield);
});
BOOST_LOG_TRIVIAL(debug) << __func__ << " fetched a page";
std::stringstream objectsBuffer;
for (auto& obj : objects)
{
objectsBuffer << "\\\\x" << ripple::strHex(obj.key) << '\t'
<< std::to_string(minLedger) << '\t' << "\\\\x"
<< ripple::strHex(obj.blob) << '\n';
}
pgQuery.bulkInsert("objects", objectsBuffer.str(), yield);
cursor = curCursor;
if (!cursor)
break;
}
BOOST_LOG_TRIVIAL(info) << __func__ << " finished inserting into objects";
{
std::stringstream sql;
sql << "DELETE FROM ledgers WHERE ledger_seq < "
<< std::to_string(minLedger);
auto res = pgQuery(sql.str().data(), yield);
if (res.msg() != "ok")
throw std::runtime_error("Error deleting from ledgers table");
}
{
std::stringstream sql;
sql << "DELETE FROM keys WHERE ledger_seq < "
<< std::to_string(minLedger);
auto res = pgQuery(sql.str().data(), yield);
if (res.msg() != "ok")
throw std::runtime_error("Error deleting from keys table");
}
{
std::stringstream sql;
sql << "DELETE FROM books WHERE ledger_seq < "
<< std::to_string(minLedger);
auto res = pgQuery(sql.str().data(), yield);
if (res.msg() != "ok")
throw std::runtime_error("Error deleting from books table");
}
return true;
}
} // namespace Backend

View File

@@ -1,145 +0,0 @@
#ifndef RIPPLE_APP_REPORTING_POSTGRESBACKEND_H_INCLUDED
#define RIPPLE_APP_REPORTING_POSTGRESBACKEND_H_INCLUDED
#include <boost/json.hpp>
#include <backend/BackendInterface.h>
namespace Backend {
class PostgresBackend : public BackendInterface
{
private:
mutable size_t numRowsInObjectsBuffer_ = 0;
mutable std::stringstream objectsBuffer_;
mutable size_t numRowsInSuccessorBuffer_ = 0;
mutable std::stringstream successorBuffer_;
mutable std::stringstream transactionsBuffer_;
mutable std::stringstream accountTxBuffer_;
std::shared_ptr<PgPool> pgPool_;
mutable PgQuery writeConnection_;
mutable bool abortWrite_ = false;
std::uint32_t writeInterval_ = 1000000;
std::uint32_t inProcessLedger = 0;
mutable std::unordered_set<std::string> successors_;
const char* const set_timeout = "SET statement_timeout TO 10000";
public:
PostgresBackend(
boost::asio::io_context& ioc,
boost::json::object const& config);
std::optional<std::uint32_t>
fetchLatestLedgerSequence(boost::asio::yield_context& yield) const override;
std::optional<ripple::LedgerInfo>
fetchLedgerBySequence(
std::uint32_t const sequence,
boost::asio::yield_context& yield) const override;
std::optional<ripple::LedgerInfo>
fetchLedgerByHash(
ripple::uint256 const& hash,
boost::asio::yield_context& yield) const override;
std::optional<Blob>
doFetchLedgerObject(
ripple::uint256 const& key,
std::uint32_t const sequence,
boost::asio::yield_context& yield) const override;
// returns a transaction, metadata pair
std::optional<TransactionAndMetadata>
fetchTransaction(
ripple::uint256 const& hash,
boost::asio::yield_context& yield) const override;
std::vector<TransactionAndMetadata>
fetchAllTransactionsInLedger(
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const override;
std::vector<ripple::uint256>
fetchAllTransactionHashesInLedger(
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const override;
std::vector<LedgerObject>
fetchLedgerDiff(
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const override;
std::optional<LedgerRange>
hardFetchLedgerRange(boost::asio::yield_context& yield) const override;
std::optional<ripple::uint256>
doFetchSuccessorKey(
ripple::uint256 key,
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const override;
std::vector<TransactionAndMetadata>
fetchTransactions(
std::vector<ripple::uint256> const& hashes,
boost::asio::yield_context& yield) const override;
std::vector<Blob>
doFetchLedgerObjects(
std::vector<ripple::uint256> const& keys,
std::uint32_t const sequence,
boost::asio::yield_context& yield) const override;
AccountTransactions
fetchAccountTransactions(
ripple::AccountID const& account,
std::uint32_t const limit,
bool forward,
std::optional<AccountTransactionsCursor> const& cursor,
boost::asio::yield_context& yield) const override;
void
writeLedger(
ripple::LedgerInfo const& ledgerInfo,
std::string&& ledgerHeader) override;
void
doWriteLedgerObject(
std::string&& key,
std::uint32_t const seq,
std::string&& blob) override;
void
writeSuccessor(
std::string&& key,
std::uint32_t const seq,
std::string&& successor) override;
void
writeTransaction(
std::string&& hash,
std::uint32_t const seq,
std::uint32_t const date,
std::string&& transaction,
std::string&& metadata) override;
void
writeAccountTransactions(
std::vector<AccountTransactionsData>&& data) override;
void
open(bool readOnly) override;
void
close() override;
void
startWrites() const override;
bool
doFinishWrites() override;
bool
doOnlineDelete(
std::uint32_t const numLedgersToKeep,
boost::asio::yield_context& yield) const override;
};
} // namespace Backend
#endif

View File

@@ -1,174 +0,0 @@
The data model used by clio is different than that used by rippled.
rippled uses what is known as a SHAMap, which is a tree structure, with
actual ledger and transaction data at the leaves of the tree. Looking up a record
is a tree traversal, where the key is used to determine the path to the proper
leaf node. The path from root to leaf is used as a proof-tree on the p2p network,
where nodes can prove that a piece of data is present in a ledger by sending
the path from root to leaf. Other nodes can verify this path and be certain
that the data does actually exist in the ledger in question.
clio instead flattens the data model, so lookups are O(1). This results in time
and space savings. This is possible because clio does not participate in the peer
to peer protocol, and thus does not need to verify any data. clio fully trusts the
rippled nodes that are being used as a data source.
clio uses certain features of database query languages to make this happen. Many
databases provide the necessary features to implement the clio data model. At the
time of writing, the data model is implemented in PostgreSQL and CQL (the query
language used by Apache Cassandra and ScyllaDB).
The below examples are a sort of pseudo query language
## Ledgers
We store ledger headers in a ledgers table. In PostgreSQL, we store
the headers in their deserialized form, so we can look up by sequence or hash.
In Cassandra, we store the headers as blobs. The primary table maps a ledger sequence
to the blob, and a secondary table maps a ledger hash to a ledger sequence.
## Transactions
Transactions are stored in a very basic table, with a schema like so:
```
CREATE TABLE transactions (
hash blob,
ledger_sequence int,
transaction blob,
PRIMARY KEY(hash))
```
The primary key is the hash.
A common query pattern is fetching all transactions in a ledger. In PostgreSQL,
nothing special is needed for this. We just query:
```
SELECT * FROM transactions WHERE ledger_sequence = s;
```
Cassandra doesn't handle queries like this well, since `ledger_sequence` is not
the primary key, so we use a second table that maps a ledger sequence number
to all of the hashes in that ledger:
```
CREATE TABLE transaction_hashes (
ledger_sequence int,
hash blob,
PRIMARY KEY(ledger_sequence, blob))
```
This table uses a compound primary key, so we can have multiple records with
the same ledger sequence but different hash. Looking up all of the transactions
in a given ledger then requires querying the transaction_hashes table to get the hashes of
all of the transactions in the ledger, and then using those hashes to query the
transactions table. Sometimes we only want the hashes though.
## Ledger data
Ledger data is more complicated than transaction data. Objects have different versions,
where applying transactions in a particular ledger changes an object with a given
key. A basic example is an account root object: the balance changes with every
transaction sent or received, though the key (object ID) for this object remains the same.
Ledger data then is modeled like so:
```
CREATE TABLE objects (
id blob,
ledger_sequence int,
object blob,
PRIMARY KEY(key,ledger_sequence))
```
The `objects` table has a compound primary key. This is essential. Looking up
a ledger object as of a given ledger then is just:
```
SELECT object FROM objects WHERE id = ? and ledger_sequence <= ?
ORDER BY ledger_sequence DESC LIMIT 1;
```
This gives us the most recent ledger object written at or before a specified ledger.
When a ledger object is deleted, we write a record where `object` is just an empty blob.
### Next
Generally RPCs that read ledger data will just use the above query pattern. However,
a few RPCs (`book_offers` and `ledger_data`) make use of a certain tree operation
called `successor`, which takes in an object id and ledger sequence, and returns
the id of the successor object in the ledger. This is the object in the ledger with the smallest id
greater than the input id.
This problem is quite difficult for clio's data model, since computing this
generally requires the inner nodes of the tree, which clio doesn't store. A naive
way to do this with PostgreSQL is like so:
```
SELECT * FROM objects WHERE id > ? AND ledger_sequence <= s ORDER BY id ASC, ledger_sequence DESC LIMIT 1;
```
This query is not really possible with Cassandra, unless you use ALLOW FILTERING, which
is an anti pattern (for good reason!). It would require contacting basically every node
in the entire cluster.
But even with Postgres, this query is not scalable. Why? Consider what the query
is doing at the database level. The database starts at the input id, and begins scanning
the table in ascending order of id. It needs to skip over any records that don't actually
exist in the desired ledger, which are objects that have been deleted, or objects that
were created later. As ledger history grows, this query skips over more and more records,
which results in the query taking longer and longer. The time this query takes grows
unbounded then, as ledger history just keeps growing. With under a million ledgers, this
query is usable, but as we approach 10 million ledgers are more, the query starts to become very slow.
To alleviate this issue, the data model uses a checkpointing method. We create a second
table called keys, like so:
```
CREATE TABLE keys (
ledger_sequence int,
id blob,
PRIMARY KEY(ledger_sequence, id)
)
```
However, this table does not have an entry for every ledger sequence. Instead,
this table has an entry for rougly every 1 million ledgers. We call these ledgers
flag ledgers. For each flag ledger, the keys table contains every object id in that
ledger, as well as every object id that existed in any ledger between the last flag
ledger and this one. This is a lot of keys, but not every key that ever existed (which
is what the naive attempt at implementing successor was iterating over). In this manner,
the performance is bounded. If we wanted to increase the performance of the successor operation,
we can increase the frequency of flag ledgers. However, this will use more space. 1 million
was chosen as a reasonable tradeoff to bound the performance, but not use too much space,
especially since this is only needed for two RPC calls.
We write to this table every ledger, for each new key. However, we also need to handle
keys that existed in the previous flag ledger. To do that, at each flag ledger, we
iterate through the previous flag ledger, and write any keys that are still present
in the new flag ledger. This is done asynchronously.
## Account Transactions
rippled offers a RPC called `account_tx`. This RPC returns all transactions that
affect a given account, and allows users to page backwards or forwards in time.
Generally, this is a modeled with a table like so:
```
CREATE TABLE account_tx (
account blob,
ledger_sequence int,
transaction_index int,
hash blob,
PRIMARY KEY(account,ledger_sequence,transaction_index))
```
An example of looking up from this table going backwards in time is:
```
SELECT hash FROM account_tx WHERE account = ?
AND ledger_sequence <= ? and transaction_index <= ?
ORDER BY ledger_sequence DESC, transaction_index DESC;
```
This query returns the hashes, and then we use those hashes to read from the
transactions table.
## Comments
There are various nuances around how these data models are tuned and optimized
for each database implementation. Cassandra and PostgreSQL are very different,
so some slight modifications are needed. However, the general model outlined here
is implemented by both databases, and when adding a new database, this general model
should be followed, unless there is a good reason not to. Generally, a database will be
decently similar to either PostgreSQL or Cassandra, so using those as a basis should
be sufficient.
Whatever database is used, clio requires strong consistency, and durability. For this
reason, any replication strategy needs to maintain strong consistency.

View File

@@ -1,120 +0,0 @@
#include <backend/SimpleCache.h>
namespace Backend {
uint32_t
SimpleCache::latestLedgerSequence() const
{
std::shared_lock lck{mtx_};
return latestSeq_;
}
void
SimpleCache::update(
std::vector<LedgerObject> const& objs,
uint32_t seq,
bool isBackground)
{
if (disabled_)
return;
{
std::unique_lock lck{mtx_};
if (seq > latestSeq_)
{
assert(seq == latestSeq_ + 1 || latestSeq_ == 0);
latestSeq_ = seq;
}
for (auto const& obj : objs)
{
if (obj.blob.size())
{
if (isBackground && deletes_.count(obj.key))
continue;
auto& e = map_[obj.key];
if (seq > e.seq)
{
e = {seq, obj.blob};
}
}
else
{
map_.erase(obj.key);
if (!full_ && !isBackground)
deletes_.insert(obj.key);
}
}
}
}
std::optional<LedgerObject>
SimpleCache::getSuccessor(ripple::uint256 const& key, uint32_t seq) const
{
if (!full_)
return {};
std::shared_lock{mtx_};
if (seq != latestSeq_)
return {};
auto e = map_.upper_bound(key);
if (e == map_.end())
return {};
return {{e->first, e->second.blob}};
}
std::optional<LedgerObject>
SimpleCache::getPredecessor(ripple::uint256 const& key, uint32_t seq) const
{
if (!full_)
return {};
std::shared_lock lck{mtx_};
if (seq != latestSeq_)
return {};
auto e = map_.lower_bound(key);
if (e == map_.begin())
return {};
--e;
return {{e->first, e->second.blob}};
}
std::optional<Blob>
SimpleCache::get(ripple::uint256 const& key, uint32_t seq) const
{
if (seq > latestSeq_)
return {};
std::shared_lock lck{mtx_};
auto e = map_.find(key);
if (e == map_.end())
return {};
if (seq < e->second.seq)
return {};
return {e->second.blob};
}
void
SimpleCache::setDisabled()
{
disabled_ = true;
}
void
SimpleCache::setFull()
{
if (disabled_)
return;
full_ = true;
std::unique_lock lck{mtx_};
deletes_.clear();
}
bool
SimpleCache::isFull() const
{
return full_;
}
size_t
SimpleCache::size() const
{
std::shared_lock lck{mtx_};
return map_.size();
}
} // namespace Backend

View File

@@ -1,68 +0,0 @@
#ifndef CLIO_SIMPLECACHE_H_INCLUDED
#define CLIO_SIMPLECACHE_H_INCLUDED
#include <ripple/basics/base_uint.h>
#include <ripple/basics/hardened_hash.h>
#include <backend/Types.h>
#include <map>
#include <mutex>
#include <shared_mutex>
#include <utility>
#include <vector>
namespace Backend {
class SimpleCache
{
struct CacheEntry
{
uint32_t seq = 0;
Blob blob;
};
std::map<ripple::uint256, CacheEntry> map_;
mutable std::shared_mutex mtx_;
uint32_t latestSeq_ = 0;
std::atomic_bool full_ = false;
std::atomic_bool disabled_ = false;
// temporary set to prevent background thread from writing already deleted
// data. not used when cache is full
std::unordered_set<ripple::uint256, ripple::hardened_hash<>> deletes_;
public:
// Update the cache with new ledger objects
// set isBackground to true when writing old data from a background thread
void
update(
std::vector<LedgerObject> const& blobs,
uint32_t seq,
bool isBackground = false);
std::optional<Blob>
get(ripple::uint256 const& key, uint32_t seq) const;
// always returns empty optional if isFull() is false
std::optional<LedgerObject>
getSuccessor(ripple::uint256 const& key, uint32_t seq) const;
// always returns empty optional if isFull() is false
std::optional<LedgerObject>
getPredecessor(ripple::uint256 const& key, uint32_t seq) const;
void
setDisabled();
void
setFull();
uint32_t
latestLedgerSequence() const;
// whether the cache has all data for the most recent ledger
bool
isFull() const;
size_t
size() const;
};
} // namespace Backend
#endif

View File

@@ -1,73 +0,0 @@
#ifndef CLIO_TYPES_H_INCLUDED
#define CLIO_TYPES_H_INCLUDED
#include <ripple/basics/base_uint.h>
#include <optional>
#include <string>
#include <vector>
namespace Backend {
// *** return types
using Blob = std::vector<unsigned char>;
struct LedgerObject
{
ripple::uint256 key;
Blob blob;
bool
operator==(const LedgerObject& other) const
{
return key == other.key && blob == other.blob;
}
};
struct LedgerPage
{
std::vector<LedgerObject> objects;
std::optional<ripple::uint256> cursor;
};
struct BookOffersPage
{
std::vector<LedgerObject> offers;
std::optional<ripple::uint256> cursor;
};
struct TransactionAndMetadata
{
Blob transaction;
Blob metadata;
std::uint32_t ledgerSequence;
std::uint32_t date;
bool
operator==(const TransactionAndMetadata& other) const
{
return transaction == other.transaction && metadata == other.metadata &&
ledgerSequence == other.ledgerSequence && date == other.date;
}
};
struct AccountTransactionsCursor
{
std::uint32_t ledgerSequence;
std::uint32_t transactionIndex;
};
struct AccountTransactions
{
std::vector<TransactionAndMetadata> txns;
std::optional<AccountTransactionsCursor> cursor;
};
struct LedgerRange
{
std::uint32_t minSequence;
std::uint32_t maxSequence;
};
constexpr ripple::uint256 firstKey{
"0000000000000000000000000000000000000000000000000000000000000000"};
constexpr ripple::uint256 lastKey{
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"};
constexpr ripple::uint256 hi192{
"0000000000000000000000000000000000000000000000001111111111111111"};
} // namespace Backend
#endif

68
src/data/BackendFactory.h Normal file
View File

@@ -0,0 +1,68 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2022, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/BackendInterface.h>
#include <data/CassandraBackend.h>
#include <util/config/Config.h>
#include <util/log/Logger.h>
#include <boost/algorithm/string.hpp>
namespace data {
/**
* @brief A factory function that creates the backend based on a config.
*
* @param config The clio config to use
* @return A shared_ptr<BackendInterface> with the selected implementation
*/
std::shared_ptr<BackendInterface>
make_Backend(util::Config const& config)
{
static util::Logger log{"Backend"};
LOG(log.info()) << "Constructing BackendInterface";
auto const readOnly = config.valueOr("read_only", false);
auto const type = config.value<std::string>("database.type");
std::shared_ptr<BackendInterface> backend = nullptr;
// TODO: retire `cassandra-new` by next release after 2.0
if (boost::iequals(type, "cassandra") or boost::iequals(type, "cassandra-new"))
{
auto cfg = config.section("database." + type);
backend = std::make_shared<data::cassandra::CassandraBackend>(data::cassandra::SettingsProvider{cfg}, readOnly);
}
if (!backend)
throw std::runtime_error("Invalid database type");
auto const rng = backend->hardFetchLedgerRangeNoThrow();
if (rng)
{
backend->updateRange(rng->minSequence);
backend->updateRange(rng->maxSequence);
}
LOG(log.info()) << "Constructed BackendInterface Successfully";
return backend;
}
} // namespace data

View File

@@ -1,50 +1,56 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2022, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <data/BackendInterface.h>
#include <util/log/Logger.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STLedgerEntry.h>
#include <backend/BackendInterface.h>
namespace Backend {
// local to compilation unit loggers
namespace {
util::Logger gLog{"Backend"};
} // namespace
namespace data {
bool
BackendInterface::finishWrites(std::uint32_t const ledgerSequence)
{
LOG(gLog.debug()) << "Want finish writes for " << ledgerSequence;
auto commitRes = doFinishWrites();
if (commitRes)
{
LOG(gLog.debug()) << "Successfully commited. Updating range now to " << ledgerSequence;
updateRange(ledgerSequence);
}
return commitRes;
}
void
BackendInterface::writeLedgerObject(
std::string&& key,
std::uint32_t const seq,
std::string&& blob)
BackendInterface::writeLedgerObject(std::string&& key, std::uint32_t const seq, std::string&& blob)
{
assert(key.size() == sizeof(ripple::uint256));
ripple::uint256 key256 = ripple::uint256::fromVoid(key.data());
doWriteLedgerObject(std::move(key), seq, std::move(blob));
}
std::optional<LedgerRange>
BackendInterface::hardFetchLedgerRangeNoThrow(
boost::asio::yield_context& yield) const
{
BOOST_LOG_TRIVIAL(debug) << __func__;
while (true)
{
try
{
return hardFetchLedgerRange(yield);
}
catch (DatabaseTimeout& t)
{
;
}
}
}
std::optional<LedgerRange>
BackendInterface::hardFetchLedgerRangeNoThrow() const
{
BOOST_LOG_TRIVIAL(debug) << __func__;
return retryOnTimeout([&]() { return hardFetchLedgerRange(); });
}
@@ -53,26 +59,22 @@ std::optional<Blob>
BackendInterface::fetchLedgerObject(
ripple::uint256 const& key,
std::uint32_t const sequence,
boost::asio::yield_context& yield) const
boost::asio::yield_context yield) const
{
auto obj = cache_.get(key, sequence);
if (obj)
{
BOOST_LOG_TRIVIAL(trace)
<< __func__ << " - cache hit - " << ripple::strHex(key);
LOG(gLog.trace()) << "Cache hit - " << ripple::strHex(key);
return *obj;
}
else
{
BOOST_LOG_TRIVIAL(trace)
<< __func__ << " - cache miss - " << ripple::strHex(key);
LOG(gLog.trace()) << "Cache miss - " << ripple::strHex(key);
auto dbObj = doFetchLedgerObject(key, sequence, yield);
if (!dbObj)
BOOST_LOG_TRIVIAL(trace)
<< __func__ << " - missed cache and missed in db";
LOG(gLog.trace()) << "Missed cache and missed in db";
else
BOOST_LOG_TRIVIAL(trace)
<< __func__ << " - missed cache but found in db";
LOG(gLog.trace()) << "Missed cache but found in db";
return dbObj;
}
}
@@ -81,7 +83,7 @@ std::vector<Blob>
BackendInterface::fetchLedgerObjects(
std::vector<ripple::uint256> const& keys,
std::uint32_t const sequence,
boost::asio::yield_context& yield) const
boost::asio::yield_context yield) const
{
std::vector<Blob> results;
results.resize(keys.size());
@@ -94,9 +96,7 @@ BackendInterface::fetchLedgerObjects(
else
misses.push_back(keys[i]);
}
BOOST_LOG_TRIVIAL(trace)
<< __func__ << " - cache hits = " << keys.size() - misses.size()
<< " - cache misses = " << misses.size();
LOG(gLog.trace()) << "Cache hits = " << keys.size() - misses.size() << " - cache misses = " << misses.size();
if (misses.size())
{
@@ -118,15 +118,13 @@ std::optional<ripple::uint256>
BackendInterface::fetchSuccessorKey(
ripple::uint256 key,
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const
boost::asio::yield_context yield) const
{
auto succ = cache_.getSuccessor(key, ledgerSequence);
if (succ)
BOOST_LOG_TRIVIAL(trace)
<< __func__ << " - cache hit - " << ripple::strHex(key);
LOG(gLog.trace()) << "Cache hit - " << ripple::strHex(key);
else
BOOST_LOG_TRIVIAL(trace)
<< __func__ << " - cache miss - " << ripple::strHex(key);
LOG(gLog.trace()) << "Cache miss - " << ripple::strHex(key);
return succ ? succ->key : doFetchSuccessorKey(key, ledgerSequence, yield);
}
@@ -134,7 +132,7 @@ std::optional<LedgerObject>
BackendInterface::fetchSuccessorObject(
ripple::uint256 key,
std::uint32_t const ledgerSequence,
boost::asio::yield_context& yield) const
boost::asio::yield_context yield) const
{
auto succ = fetchSuccessorKey(key, ledgerSequence, yield);
if (succ)
@@ -153,8 +151,7 @@ BackendInterface::fetchBookOffers(
ripple::uint256 const& book,
std::uint32_t const ledgerSequence,
std::uint32_t const limit,
std::optional<ripple::uint256> const& cursor,
boost::asio::yield_context& yield) const
boost::asio::yield_context yield) const
{
// TODO try to speed this up. This can take a few seconds. The goal is
// to get it down to a few hundred milliseconds.
@@ -162,10 +159,7 @@ BackendInterface::fetchBookOffers(
const ripple::uint256 bookEnd = ripple::getQualityNext(book);
ripple::uint256 uTipIndex = book;
std::vector<ripple::uint256> keys;
auto getMillis = [](auto diff) {
return std::chrono::duration_cast<std::chrono::milliseconds>(diff)
.count();
};
auto getMillis = [](auto diff) { return std::chrono::duration_cast<std::chrono::milliseconds>(diff).count(); };
auto begin = std::chrono::system_clock::now();
std::uint32_t numSucc = 0;
std::uint32_t numPages = 0;
@@ -180,30 +174,24 @@ BackendInterface::fetchBookOffers(
succMillis += getMillis(mid2 - mid1);
if (!offerDir || offerDir->key >= bookEnd)
{
BOOST_LOG_TRIVIAL(trace) << __func__ << " - offerDir.has_value() "
<< offerDir.has_value() << " breaking";
LOG(gLog.trace()) << "offerDir.has_value() " << offerDir.has_value() << " breaking";
break;
}
uTipIndex = offerDir->key;
while (keys.size() < limit)
{
++numPages;
ripple::STLedgerEntry sle{
ripple::SerialIter{
offerDir->blob.data(), offerDir->blob.size()},
offerDir->key};
ripple::STLedgerEntry sle{ripple::SerialIter{offerDir->blob.data(), offerDir->blob.size()}, offerDir->key};
auto indexes = sle.getFieldV256(ripple::sfIndexes);
keys.insert(keys.end(), indexes.begin(), indexes.end());
auto next = sle.getFieldU64(ripple::sfIndexNext);
if (!next)
{
BOOST_LOG_TRIVIAL(trace)
<< __func__ << " next is empty. breaking";
LOG(gLog.trace()) << "Next is empty. breaking";
break;
}
auto nextKey = ripple::keylet::page(uTipIndex, next);
auto nextDir =
fetchLedgerObject(nextKey.key, ledgerSequence, yield);
auto nextDir = fetchLedgerObject(nextKey.key, ledgerSequence, yield);
assert(nextDir);
offerDir->blob = *nextDir;
offerDir->key = nextKey.key;
@@ -215,40 +203,56 @@ BackendInterface::fetchBookOffers(
auto objs = fetchLedgerObjects(keys, ledgerSequence, yield);
for (size_t i = 0; i < keys.size() && i < limit; ++i)
{
BOOST_LOG_TRIVIAL(trace)
<< __func__ << " key = " << ripple::strHex(keys[i])
<< " blob = " << ripple::strHex(objs[i])
<< " ledgerSequence = " << ledgerSequence;
LOG(gLog.trace()) << "Key = " << ripple::strHex(keys[i]) << " blob = " << ripple::strHex(objs[i])
<< " ledgerSequence = " << ledgerSequence;
assert(objs[i].size());
page.offers.push_back({keys[i], objs[i]});
}
auto end = std::chrono::system_clock::now();
BOOST_LOG_TRIVIAL(debug)
<< __func__ << " "
<< "Fetching " << std::to_string(keys.size()) << " offers took "
<< std::to_string(getMillis(mid - begin))
<< " milliseconds. Fetching next dir took "
<< std::to_string(succMillis) << " milliseonds. Fetched next dir "
<< std::to_string(numSucc) << " times"
<< " Fetching next page of dir took " << std::to_string(pageMillis)
<< " milliseconds"
<< ". num pages = " << std::to_string(numPages)
<< ". Fetching all objects took "
<< std::to_string(getMillis(end - mid))
<< " milliseconds. total time = "
<< std::to_string(getMillis(end - begin)) << " milliseconds"
<< " book = " << ripple::strHex(book);
LOG(gLog.debug()) << "Fetching " << std::to_string(keys.size()) << " offers took "
<< std::to_string(getMillis(mid - begin)) << " milliseconds. Fetching next dir took "
<< std::to_string(succMillis) << " milliseonds. Fetched next dir " << std::to_string(numSucc)
<< " times"
<< " Fetching next page of dir took " << std::to_string(pageMillis) << " milliseconds"
<< ". num pages = " << std::to_string(numPages) << ". Fetching all objects took "
<< std::to_string(getMillis(end - mid))
<< " milliseconds. total time = " << std::to_string(getMillis(end - begin)) << " milliseconds"
<< " book = " << ripple::strHex(book);
return page;
}
std::optional<LedgerRange>
BackendInterface::hardFetchLedgerRange() const
{
return synchronous([this](auto yield) { return hardFetchLedgerRange(yield); });
}
std::optional<LedgerRange>
BackendInterface::fetchLedgerRange() const
{
std::shared_lock lck(rngMtx_);
return range;
}
void
BackendInterface::updateRange(uint32_t newMax)
{
std::scoped_lock lck(rngMtx_);
assert(!range || newMax >= range->maxSequence);
if (!range)
range = {newMax, newMax};
else
range->maxSequence = newMax;
}
LedgerPage
BackendInterface::fetchLedgerPage(
std::optional<ripple::uint256> const& cursor,
std::uint32_t const ledgerSequence,
std::uint32_t const limit,
bool outOfOrder,
boost::asio::yield_context& yield) const
boost::asio::yield_context yield) const
{
LedgerPage page;
@@ -256,11 +260,8 @@ BackendInterface::fetchLedgerPage(
bool reachedEnd = false;
while (keys.size() < limit && !reachedEnd)
{
ripple::uint256 const& curCursor = keys.size() ? keys.back()
: cursor ? *cursor
: firstKey;
std::uint32_t const seq =
outOfOrder ? range->maxSequence : ledgerSequence;
ripple::uint256 const& curCursor = keys.size() ? keys.back() : cursor ? *cursor : firstKey;
std::uint32_t const seq = outOfOrder ? range->maxSequence : ledgerSequence;
auto succ = fetchSuccessorKey(curCursor, seq, yield);
if (!succ)
reachedEnd = true;
@@ -275,16 +276,14 @@ BackendInterface::fetchLedgerPage(
page.objects.push_back({std::move(keys[i]), std::move(objects[i])});
else if (!outOfOrder)
{
BOOST_LOG_TRIVIAL(error)
<< __func__ << " incorrect successor table. key = "
<< ripple::strHex(keys[i]) << " - seq = " << ledgerSequence;
LOG(gLog.error()) << "Deleted or non-existent object in successor table. key = " << ripple::strHex(keys[i])
<< " - seq = " << ledgerSequence;
std::stringstream msg;
for (size_t j = 0; j < objects.size(); ++j)
{
msg << " - " << ripple::strHex(keys[j]);
}
BOOST_LOG_TRIVIAL(error) << __func__ << msg.str();
assert(false);
LOG(gLog.error()) << msg.str();
}
}
if (keys.size() && !reachedEnd)
@@ -294,9 +293,7 @@ BackendInterface::fetchLedgerPage(
}
std::optional<ripple::Fees>
BackendInterface::fetchFees(
std::uint32_t const seq,
boost::asio::yield_context& yield) const
BackendInterface::fetchFees(std::uint32_t const seq, boost::asio::yield_context yield) const
{
ripple::Fees fees;
@@ -305,7 +302,7 @@ BackendInterface::fetchFees(
if (!bytes)
{
BOOST_LOG_TRIVIAL(error) << __func__ << " - could not find fees";
LOG(gLog.error()) << "Could not find fees";
return {};
}
@@ -315,9 +312,6 @@ BackendInterface::fetchFees(
if (sle.getFieldIndex(ripple::sfBaseFee) != -1)
fees.base = sle.getFieldU64(ripple::sfBaseFee);
if (sle.getFieldIndex(ripple::sfReferenceFeeUnits) != -1)
fees.units = sle.getFieldU32(ripple::sfReferenceFeeUnits);
if (sle.getFieldIndex(ripple::sfReserveBase) != -1)
fees.reserve = sle.getFieldU32(ripple::sfReserveBase);
@@ -327,4 +321,4 @@ BackendInterface::fetchFees(
return fees;
}
} // namespace Backend
} // namespace data

567
src/data/BackendInterface.h Normal file
View File

@@ -0,0 +1,567 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2022, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/DBHelpers.h>
#include <data/LedgerCache.h>
#include <data/Types.h>
#include <util/config/Config.h>
#include <util/log/Logger.h>
#include <ripple/protocol/Fees.h>
#include <ripple/protocol/LedgerHeader.h>
#include <boost/asio/spawn.hpp>
#include <boost/json.hpp>
#include <thread>
#include <type_traits>
namespace data {
/**
* @brief Represents a database timeout error.
*/
class DatabaseTimeout : public std::exception
{
public:
const char*
what() const throw() override
{
return "Database read timed out. Please retry the request";
}
};
/**
* @brief A helper function that catches DatabaseTimout exceptions and retries indefinitely.
*
* @tparam FnType The type of function object to execute
* @param func The function object to execute
* @param waitMs Delay between retry attempts
* @return auto The same as the return type of func
*/
template <class FnType>
auto
retryOnTimeout(FnType func, size_t waitMs = 500)
{
static util::Logger log{"Backend"};
while (true)
{
try
{
return func();
}
catch (DatabaseTimeout const&)
{
LOG(log.error()) << "Database request timed out. Sleeping and retrying ... ";
std::this_thread::sleep_for(std::chrono::milliseconds(waitMs));
}
}
}
/**
* @brief Synchronously executes the given function object inside a coroutine.
*
* @tparam FnType The type of function object to execute
* @param func The function object to execute
* @return auto The same as the return type of func
*/
template <class FnType>
auto
synchronous(FnType&& func)
{
boost::asio::io_context ctx;
using R = typename boost::result_of<FnType(boost::asio::yield_context)>::type;
if constexpr (!std::is_same<R, void>::value)
{
R res;
boost::asio::spawn(ctx, [&func, &res](auto yield) { res = func(yield); });
ctx.run();
return res;
}
else
{
boost::asio::spawn(ctx, [&func](auto yield) { func(yield); });
ctx.run();
}
}
/**
* @brief Synchronously execute the given function object and retry until no DatabaseTimeout is thrown.
*
* @tparam FnType The type of function object to execute
* @param func The function object to execute
* @return auto The same as the return type of func
*/
template <class FnType>
auto
synchronousAndRetryOnTimeout(FnType&& func)
{
return retryOnTimeout([&]() { return synchronous(func); });
}
/**
* @brief The interface to the database used by Clio.
*/
class BackendInterface
{
protected:
mutable std::shared_mutex rngMtx_;
std::optional<LedgerRange> range;
LedgerCache cache_;
public:
BackendInterface() = default;
virtual ~BackendInterface() = default;
// TODO: Remove this hack. Cache should not be exposed thru BackendInterface
/**
* @return Immutable cache
*/
LedgerCache const&
cache() const
{
return cache_;
}
/**
* @return Mutable cache
*/
LedgerCache&
cache()
{
return cache_;
}
/**
* @brief Fetches a specific ledger by sequence number.
*
* @param sequence The sequence number to fetch for
* @param yield The coroutine context
* @return The ripple::LedgerHeader if found; nullopt otherwise
*/
virtual std::optional<ripple::LedgerHeader>
fetchLedgerBySequence(std::uint32_t const sequence, boost::asio::yield_context yield) const = 0;
/**
* @brief Fetches a specific ledger by hash.
*
* @param hash The hash to fetch for
* @param yield The coroutine context
* @return The ripple::LedgerHeader if found; nullopt otherwise
*/
virtual std::optional<ripple::LedgerHeader>
fetchLedgerByHash(ripple::uint256 const& hash, boost::asio::yield_context yield) const = 0;
/**
* @brief Fetches the latest ledger sequence.
*
* @param yield The coroutine context
* @return Latest sequence wrapped in an optional if found; nullopt otherwise
*/
virtual std::optional<std::uint32_t>
fetchLatestLedgerSequence(boost::asio::yield_context yield) const = 0;
/**
* @brief Fetch the current ledger range.
*
* @return The current ledger range if populated; nullopt otherwise
*/
std::optional<LedgerRange>
fetchLedgerRange() const;
/**
* @brief Updates the range of sequences that are stored in the DB.
*
* @param newMax The new maximum sequence available
*/
void
updateRange(uint32_t newMax);
/**
* @brief Fetch the fees from a specific ledger sequence.
*
* @param seq The sequence to fetch for
* @param yield The coroutine context
* @return ripple::Fees if fees are found; nullopt otherwise
*/
std::optional<ripple::Fees>
fetchFees(std::uint32_t const seq, boost::asio::yield_context yield) const;
/**
* @brief Fetches a specific transaction.
*
* @param hash The hash of the transaction to fetch
* @param yield The coroutine context
* @return TransactionAndMetadata if transaction is found; nullopt otherwise
*/
virtual std::optional<TransactionAndMetadata>
fetchTransaction(ripple::uint256 const& hash, boost::asio::yield_context yield) const = 0;
/**
* @brief Fetches multiple transactions.
*
* @param hashes A vector of hashes to fetch transactions for
* @param yield The coroutine context
* @return A vector of TransactionAndMetadata matching the given hashes
*/
virtual std::vector<TransactionAndMetadata>
fetchTransactions(std::vector<ripple::uint256> const& hashes, boost::asio::yield_context yield) const = 0;
/**
* @brief Fetches all transactions for a specific account.
*
* @param account The account to fetch transactions for
* @param limit The maximum number of transactions per result page
* @param forward Whether to fetch the page forwards or backwards from the given cursor
* @param cursor The cursor to resume fetching from
* @param yield The coroutine context
* @return Results and a cursor to resume from
*/
virtual TransactionsAndCursor
fetchAccountTransactions(
ripple::AccountID const& account,
std::uint32_t const limit,
bool forward,
std::optional<TransactionsCursor> const& cursor,
boost::asio::yield_context yield) const = 0;
/**
* @brief Fetches all transactions from a specific ledger.
*
* @param ledgerSequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return Results as a vector of TransactionAndMetadata
*/
virtual std::vector<TransactionAndMetadata>
fetchAllTransactionsInLedger(std::uint32_t const ledgerSequence, boost::asio::yield_context yield) const = 0;
/**
* @brief Fetches all transaction hashes from a specific ledger.
*
* @param ledgerSequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return Hashes as ripple::uint256 in a vector
*/
virtual std::vector<ripple::uint256>
fetchAllTransactionHashesInLedger(std::uint32_t const ledgerSequence, boost::asio::yield_context yield) const = 0;
/**
* @brief Fetches a specific NFT.
*
* @param tokenID The ID of the NFT
* @param ledgerSequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return NFT object on success; nullopt otherwise
*/
virtual std::optional<NFT>
fetchNFT(ripple::uint256 const& tokenID, std::uint32_t const ledgerSequence, boost::asio::yield_context yield)
const = 0;
/**
* @brief Fetches all transactions for a specific NFT.
*
* @param tokenID The ID of the NFT
* @param limit The maximum number of transactions per result page
* @param forward Whether to fetch the page forwards or backwards from the given cursor
* @param cursorIn The cursor to resume fetching from
* @param yield The coroutine context
* @return Results and a cursor to resume from
*/
virtual TransactionsAndCursor
fetchNFTTransactions(
ripple::uint256 const& tokenID,
std::uint32_t const limit,
bool const forward,
std::optional<TransactionsCursor> const& cursorIn,
boost::asio::yield_context yield) const = 0;
/**
* @brief Fetches a specific ledger object.
*
* Currently the real fetch happens in doFetchLedgerObject and fetchLedgerObject attempts to fetch from Cache first
* and only calls out to the real DB if a cache miss ocurred.
*
* @param key The key of the object
* @param sequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return The object as a Blob on success; nullopt otherwise
*/
std::optional<Blob>
fetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield) const;
/**
* @brief Fetches all ledger objects by their keys.
*
* Currently the real fetch happens in doFetchLedgerObjects and fetchLedgerObjects attempts to fetch from Cache
* first and only calls out to the real DB for each of the keys that was not found in the cache.
*
* @param keys A vector with the keys of the objects to fetch
* @param sequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return A vector of ledger objects as Blobs
*/
std::vector<Blob>
fetchLedgerObjects(
std::vector<ripple::uint256> const& keys,
std::uint32_t const sequence,
boost::asio::yield_context yield) const;
/**
* @brief The database-specific implementation for fetching a ledger object.
*
* @param key The key to fetch for
* @param sequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return The object as a Blob on success; nullopt otherwise
*/
virtual std::optional<Blob>
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield)
const = 0;
/**
* @brief The database-specific implementation for fetching ledger objects.
*
* @param keys The keys to fetch for
* @param sequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return A vector of Blobs representing each fetched object
*/
virtual std::vector<Blob>
doFetchLedgerObjects(
std::vector<ripple::uint256> const& keys,
std::uint32_t const sequence,
boost::asio::yield_context yield) const = 0;
/**
* @brief Returns the difference between ledgers.
*
* @param ledgerSequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return A vector of LedgerObject representing the diff
*/
virtual std::vector<LedgerObject>
fetchLedgerDiff(std::uint32_t const ledgerSequence, boost::asio::yield_context yield) const = 0;
/**
* @brief Fetches a page of ledger objects, ordered by key/index.
*
* @param cursor The cursor to resume fetching from
* @param ledgerSequence The ledger sequence to fetch for
* @param limit The maximum number of transactions per result page
* @param outOfOrder If set to true max available sequence is used instead of ledgerSequence
* @param yield The coroutine context
* @return The ledger page
*/
LedgerPage
fetchLedgerPage(
std::optional<ripple::uint256> const& cursor,
std::uint32_t const ledgerSequence,
std::uint32_t const limit,
bool outOfOrder,
boost::asio::yield_context yield) const;
/**
* @brief Fetches the successor object.
*
* @param key The key to fetch for
* @param ledgerSequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return The sucessor on success; nullopt otherwise
*/
std::optional<LedgerObject>
fetchSuccessorObject(ripple::uint256 key, std::uint32_t const ledgerSequence, boost::asio::yield_context yield)
const;
/**
* @brief Fetches the successor key.
*
* Thea real fetch happens in doFetchSuccessorKey. This function will attempt to lookup the successor in the cache
* first and only if it's not found in the cache will it fetch from the actual DB.
*
* @param key The key to fetch for
* @param ledgerSequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return The sucessor key on success; nullopt otherwise
*/
std::optional<ripple::uint256>
fetchSuccessorKey(ripple::uint256 key, std::uint32_t const ledgerSequence, boost::asio::yield_context yield) const;
/**
* @brief Database-specific implementation of fetching the successor key
*
* @param key The key to fetch for
* @param ledgerSequence The ledger sequence to fetch for
* @param yield The coroutine context
* @return The sucessor on success; nullopt otherwise
*/
virtual std::optional<ripple::uint256>
doFetchSuccessorKey(ripple::uint256 key, std::uint32_t const ledgerSequence, boost::asio::yield_context yield)
const = 0;
/**
* @brief Fetches book offers.
*
* @param book Unsigned 256-bit integer.
* @param ledgerSequence The ledger sequence to fetch for
* @param limit Pagaing limit as to how many transactions returned per page.
* @param yield The coroutine context
* @return The book offers page
*/
BookOffersPage
fetchBookOffers(
ripple::uint256 const& book,
std::uint32_t const ledgerSequence,
std::uint32_t const limit,
boost::asio::yield_context yield) const;
/**
* @brief Synchronously fetches the ledger range from DB.
*
* This function just wraps hardFetchLedgerRange(boost::asio::yield_context) using synchronous(FnType&&).
*
* @return The ledger range if available; nullopt otherwise
*/
std::optional<LedgerRange>
hardFetchLedgerRange() const;
/**
* @brief Fetches the ledger range from DB.
*
* @return The ledger range if available; nullopt otherwise
*/
virtual std::optional<LedgerRange>
hardFetchLedgerRange(boost::asio::yield_context yield) const = 0;
/**
* @brief Fetches the ledger range from DB retrying until no DatabaseTimeout is thrown.
*
* @return The ledger range if available; nullopt otherwise
*/
std::optional<LedgerRange>
hardFetchLedgerRangeNoThrow() const;
/**
* @brief Writes to a specific ledger.
*
* @param ledgerHeader Ledger header.
* @param blob r-value string serialization of ledger header.
*/
virtual void
writeLedger(ripple::LedgerHeader const& ledgerHeader, std::string&& blob) = 0;
/**
* @brief Writes a new ledger object.
*
* @param key The key to write the ledger object under
* @param seq The ledger sequence to write for
* @param blob The data to write
*/
virtual void
writeLedgerObject(std::string&& key, std::uint32_t const seq, std::string&& blob);
/**
* @brief Writes a new transaction.
*
* @param hash The hash of the transaction
* @param seq The ledger sequence to write for
* @param date The timestamp of the entry
* @param transaction The transaction data to write
* @param metadata The metadata to write
*/
virtual void
writeTransaction(
std::string&& hash,
std::uint32_t const seq,
std::uint32_t const date,
std::string&& transaction,
std::string&& metadata) = 0;
/**
* @brief Writes NFTs to the database.
*
* @param data A vector of NFTsData objects representing the NFTs
*/
virtual void
writeNFTs(std::vector<NFTsData>&& data) = 0;
/**
* @brief Write a new set of account transactions.
*
* @param data A vector of AccountTransactionsData objects representing the account transactions
*/
virtual void
writeAccountTransactions(std::vector<AccountTransactionsData>&& data) = 0;
/**
* @brief Write NFTs transactions.
*
* @param data A vector of NFTTransactionsData objects
*/
virtual void
writeNFTTransactions(std::vector<NFTTransactionsData>&& data) = 0;
/**
* @brief Write a new successor.
*
* @param key Key of the object that the passed successor will be the successor for
* @param seq The ledger sequence to write for
* @param successor The successor data to write
*/
virtual void
writeSuccessor(std::string&& key, std::uint32_t const seq, std::string&& successor) = 0;
/**
* @brief Starts a write transaction with the DB. No-op for cassandra.
*
* Note: Can potentially be deprecated and removed.
*/
virtual void
startWrites() const = 0;
/**
* @brief Tells database we finished writing all data for a specific ledger.
*
* Uses doFinishWrites to synchronize with the pending writes.
*
* @param ledgerSequence The ledger sequence to finish writing for
* @return true on success; false otherwise
*/
bool
finishWrites(std::uint32_t const ledgerSequence);
/**
* @return true if database is overwhelmed; false otherwise
*/
virtual bool
isTooBusy() const = 0;
private:
virtual void
doWriteLedgerObject(std::string&& key, std::uint32_t const seq, std::string&& blob) = 0;
virtual bool
doFinishWrites() = 0;
};
} // namespace data
using BackendInterface = data::BackendInterface;

829
src/data/CassandraBackend.h Normal file
View File

@@ -0,0 +1,829 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/BackendInterface.h>
#include <data/cassandra/Concepts.h>
#include <data/cassandra/Handle.h>
#include <data/cassandra/Schema.h>
#include <data/cassandra/SettingsProvider.h>
#include <data/cassandra/impl/ExecutionStrategy.h>
#include <util/LedgerUtils.h>
#include <util/Profiler.h>
#include <util/log/Logger.h>
#include <ripple/protocol/LedgerHeader.h>
#include <ripple/protocol/nft.h>
#include <boost/asio/spawn.hpp>
namespace data::cassandra {
/**
* @brief Implements @ref BackendInterface for Cassandra/ScyllaDB.
*
* Note: This is a safer and more correct rewrite of the original implementation of the backend.
*
* @tparam SettingsProviderType The settings provider type to use
* @tparam ExecutionStrategyType The execution strategy type to use
*/
template <SomeSettingsProvider SettingsProviderType, SomeExecutionStrategy ExecutionStrategyType>
class BasicCassandraBackend : public BackendInterface
{
util::Logger log_{"Backend"};
SettingsProviderType settingsProvider_;
Schema<SettingsProviderType> schema_;
Handle handle_;
// have to be mutable because BackendInterface constness :(
mutable ExecutionStrategyType executor_;
std::atomic_uint32_t ledgerSequence_ = 0u;
public:
/**
* @brief Create a new cassandra/scylla backend instance.
*
* @param settingsProvider The settings provider to use
* @param readOnly Whether the database should be in readonly mode
*/
BasicCassandraBackend(SettingsProviderType settingsProvider, bool readOnly)
: settingsProvider_{std::move(settingsProvider)}
, schema_{settingsProvider_}
, handle_{settingsProvider_.getSettings()}
, executor_{settingsProvider_.getSettings(), handle_}
{
if (auto const res = handle_.connect(); not res)
throw std::runtime_error("Could not connect to Cassandra: " + res.error());
if (not readOnly)
{
if (auto const res = handle_.execute(schema_.createKeyspace); not res)
{
// on datastax, creation of keyspaces can be configured to only be done thru the admin interface.
// this does not mean that the keyspace does not already exist tho.
if (res.error().code() != CASS_ERROR_SERVER_UNAUTHORIZED)
throw std::runtime_error("Could not create keyspace: " + res.error());
}
if (auto const res = handle_.executeEach(schema_.createSchema); not res)
throw std::runtime_error("Could not create schema: " + res.error());
}
try
{
schema_.prepareStatements(handle_);
}
catch (std::runtime_error const& ex)
{
LOG(log_.error()) << "Failed to prepare the statements: " << ex.what() << "; readOnly: " << readOnly;
throw;
}
LOG(log_.info()) << "Created (revamped) CassandraBackend";
}
TransactionsAndCursor
fetchAccountTransactions(
ripple::AccountID const& account,
std::uint32_t const limit,
bool forward,
std::optional<TransactionsCursor> const& cursorIn,
boost::asio::yield_context yield) const override
{
auto rng = fetchLedgerRange();
if (!rng)
return {{}, {}};
Statement statement = [this, forward, &account]() {
if (forward)
return schema_->selectAccountTxForward.bind(account);
else
return schema_->selectAccountTx.bind(account);
}();
auto cursor = cursorIn;
if (cursor)
{
statement.bindAt(1, cursor->asTuple());
LOG(log_.debug()) << "account = " << ripple::strHex(account) << " tuple = " << cursor->ledgerSequence
<< cursor->transactionIndex;
}
else
{
auto const seq = forward ? rng->minSequence : rng->maxSequence;
auto const placeHolder = forward ? 0u : std::numeric_limits<std::uint32_t>::max();
statement.bindAt(1, std::make_tuple(placeHolder, placeHolder));
LOG(log_.debug()) << "account = " << ripple::strHex(account) << " idx = " << seq
<< " tuple = " << placeHolder;
}
// FIXME: Limit is a hack to support uint32_t properly for the time
// being. Should be removed later and schema updated to use proper
// types.
statement.bindAt(2, Limit{limit});
auto const res = executor_.read(yield, statement);
auto const& results = res.value();
if (not results.hasRows())
{
LOG(log_.debug()) << "No rows returned";
return {};
}
std::vector<ripple::uint256> hashes = {};
auto numRows = results.numRows();
LOG(log_.info()) << "num_rows = " << numRows;
for (auto [hash, data] : extract<ripple::uint256, std::tuple<uint32_t, uint32_t>>(results))
{
hashes.push_back(hash);
if (--numRows == 0)
{
LOG(log_.debug()) << "Setting cursor";
cursor = data;
// forward queries by ledger/tx sequence `>=`
// so we have to advance the index by one
if (forward)
++cursor->transactionIndex;
}
}
auto const txns = fetchTransactions(hashes, yield);
LOG(log_.debug()) << "Txns = " << txns.size();
if (txns.size() == limit)
{
LOG(log_.debug()) << "Returning cursor";
return {txns, cursor};
}
return {txns, {}};
}
bool
doFinishWrites() override
{
// wait for other threads to finish their writes
executor_.sync();
if (!range)
{
executor_.writeSync(schema_->updateLedgerRange, ledgerSequence_, false, ledgerSequence_);
}
if (not executeSyncUpdate(schema_->updateLedgerRange.bind(ledgerSequence_, true, ledgerSequence_ - 1)))
{
LOG(log_.warn()) << "Update failed for ledger " << ledgerSequence_;
return false;
}
LOG(log_.info()) << "Committed ledger " << ledgerSequence_;
return true;
}
void
writeLedger(ripple::LedgerHeader const& ledgerInfo, std::string&& blob) override
{
executor_.write(schema_->insertLedgerHeader, ledgerInfo.seq, std::move(blob));
executor_.write(schema_->insertLedgerHash, ledgerInfo.hash, ledgerInfo.seq);
ledgerSequence_ = ledgerInfo.seq;
}
std::optional<std::uint32_t>
fetchLatestLedgerSequence(boost::asio::yield_context yield) const override
{
if (auto const res = executor_.read(yield, schema_->selectLatestLedger); res)
{
if (auto const& result = res.value(); result)
{
if (auto const maybeValue = result.template get<uint32_t>(); maybeValue)
return maybeValue;
LOG(log_.error()) << "Could not fetch latest ledger - no rows";
return std::nullopt;
}
LOG(log_.error()) << "Could not fetch latest ledger - no result";
}
else
{
LOG(log_.error()) << "Could not fetch latest ledger: " << res.error();
}
return std::nullopt;
}
std::optional<ripple::LedgerHeader>
fetchLedgerBySequence(std::uint32_t const sequence, boost::asio::yield_context yield) const override
{
auto const res = executor_.read(yield, schema_->selectLedgerBySeq, sequence);
if (res)
{
if (auto const& result = res.value(); result)
{
if (auto const maybeValue = result.template get<std::vector<unsigned char>>(); maybeValue)
{
return util::deserializeHeader(ripple::makeSlice(*maybeValue));
}
LOG(log_.error()) << "Could not fetch ledger by sequence - no rows";
return std::nullopt;
}
LOG(log_.error()) << "Could not fetch ledger by sequence - no result";
}
else
{
LOG(log_.error()) << "Could not fetch ledger by sequence: " << res.error();
}
return std::nullopt;
}
std::optional<ripple::LedgerHeader>
fetchLedgerByHash(ripple::uint256 const& hash, boost::asio::yield_context yield) const override
{
if (auto const res = executor_.read(yield, schema_->selectLedgerByHash, hash); res)
{
if (auto const& result = res.value(); result)
{
if (auto const maybeValue = result.template get<uint32_t>(); maybeValue)
return fetchLedgerBySequence(*maybeValue, yield);
LOG(log_.error()) << "Could not fetch ledger by hash - no rows";
return std::nullopt;
}
LOG(log_.error()) << "Could not fetch ledger by hash - no result";
}
else
{
LOG(log_.error()) << "Could not fetch ledger by hash: " << res.error();
}
return std::nullopt;
}
std::optional<LedgerRange>
hardFetchLedgerRange(boost::asio::yield_context yield) const override
{
if (auto const res = executor_.read(yield, schema_->selectLedgerRange); res)
{
auto const& results = res.value();
if (not results.hasRows())
{
LOG(log_.debug()) << "Could not fetch ledger range - no rows";
return std::nullopt;
}
// TODO: this is probably a good place to use user type in
// cassandra instead of having two rows with bool flag. or maybe at
// least use tuple<int, int>?
LedgerRange range;
std::size_t idx = 0;
for (auto [seq] : extract<uint32_t>(results))
{
if (idx == 0)
range.maxSequence = range.minSequence = seq;
else if (idx == 1)
range.maxSequence = seq;
++idx;
}
if (range.minSequence > range.maxSequence)
std::swap(range.minSequence, range.maxSequence);
LOG(log_.debug()) << "After hardFetchLedgerRange range is " << range.minSequence << ":"
<< range.maxSequence;
return range;
}
else
{
LOG(log_.error()) << "Could not fetch ledger range: " << res.error();
}
return std::nullopt;
}
std::vector<TransactionAndMetadata>
fetchAllTransactionsInLedger(std::uint32_t const ledgerSequence, boost::asio::yield_context yield) const override
{
auto hashes = fetchAllTransactionHashesInLedger(ledgerSequence, yield);
return fetchTransactions(hashes, yield);
}
std::vector<ripple::uint256>
fetchAllTransactionHashesInLedger(std::uint32_t const ledgerSequence, boost::asio::yield_context yield)
const override
{
auto start = std::chrono::system_clock::now();
auto const res = executor_.read(yield, schema_->selectAllTransactionHashesInLedger, ledgerSequence);
if (not res)
{
LOG(log_.error()) << "Could not fetch all transaction hashes: " << res.error();
return {};
}
auto const& result = res.value();
if (not result.hasRows())
{
LOG(log_.error()) << "Could not fetch all transaction hashes - no rows; ledger = "
<< std::to_string(ledgerSequence);
return {};
}
std::vector<ripple::uint256> hashes;
for (auto [hash] : extract<ripple::uint256>(result))
hashes.push_back(std::move(hash));
auto end = std::chrono::system_clock::now();
LOG(log_.debug()) << "Fetched " << hashes.size() << " transaction hashes from Cassandra in "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< " milliseconds";
return hashes;
}
std::optional<NFT>
fetchNFT(ripple::uint256 const& tokenID, std::uint32_t const ledgerSequence, boost::asio::yield_context yield)
const override
{
auto const res = executor_.read(yield, schema_->selectNFT, tokenID, ledgerSequence);
if (not res)
return std::nullopt;
if (auto const maybeRow = res->template get<uint32_t, ripple::AccountID, bool>(); maybeRow)
{
auto [seq, owner, isBurned] = *maybeRow;
auto result = std::make_optional<NFT>(tokenID, seq, owner, isBurned);
// now fetch URI. Usually we will have the URI even for burned NFTs,
// but if the first ledger on this clio included NFTokenBurn
// transactions we will not have the URIs for any of those tokens.
// In any other case not having the URI indicates something went
// wrong with our data.
//
// TODO - in the future would be great for any handlers that use
// this could inject a warning in this case (the case of not having
// a URI because it was burned in the first ledger) to indicate that
// even though we are returning a blank URI, the NFT might have had
// one.
auto uriRes = executor_.read(yield, schema_->selectNFTURI, tokenID, ledgerSequence);
if (uriRes)
{
if (auto const maybeUri = uriRes->template get<ripple::Blob>(); maybeUri)
result->uri = *maybeUri;
}
return result;
}
LOG(log_.error()) << "Could not fetch NFT - no rows";
return std::nullopt;
}
TransactionsAndCursor
fetchNFTTransactions(
ripple::uint256 const& tokenID,
std::uint32_t const limit,
bool const forward,
std::optional<TransactionsCursor> const& cursorIn,
boost::asio::yield_context yield) const override
{
auto rng = fetchLedgerRange();
if (!rng)
return {{}, {}};
Statement statement = [this, forward, &tokenID]() {
if (forward)
return schema_->selectNFTTxForward.bind(tokenID);
else
return schema_->selectNFTTx.bind(tokenID);
}();
auto cursor = cursorIn;
if (cursor)
{
statement.bindAt(1, cursor->asTuple());
LOG(log_.debug()) << "token_id = " << ripple::strHex(tokenID) << " tuple = " << cursor->ledgerSequence
<< cursor->transactionIndex;
}
else
{
auto const seq = forward ? rng->minSequence : rng->maxSequence;
auto const placeHolder = forward ? 0 : std::numeric_limits<std::uint32_t>::max();
statement.bindAt(1, std::make_tuple(placeHolder, placeHolder));
LOG(log_.debug()) << "token_id = " << ripple::strHex(tokenID) << " idx = " << seq
<< " tuple = " << placeHolder;
}
statement.bindAt(2, Limit{limit});
auto const res = executor_.read(yield, statement);
auto const& results = res.value();
if (not results.hasRows())
{
LOG(log_.debug()) << "No rows returned";
return {};
}
std::vector<ripple::uint256> hashes = {};
auto numRows = results.numRows();
LOG(log_.info()) << "num_rows = " << numRows;
for (auto [hash, data] : extract<ripple::uint256, std::tuple<uint32_t, uint32_t>>(results))
{
hashes.push_back(hash);
if (--numRows == 0)
{
LOG(log_.debug()) << "Setting cursor";
cursor = data;
// forward queries by ledger/tx sequence `>=`
// so we have to advance the index by one
if (forward)
++cursor->transactionIndex;
}
}
auto const txns = fetchTransactions(hashes, yield);
LOG(log_.debug()) << "NFT Txns = " << txns.size();
if (txns.size() == limit)
{
LOG(log_.debug()) << "Returning cursor";
return {txns, cursor};
}
return {txns, {}};
}
std::optional<Blob>
doFetchLedgerObject(ripple::uint256 const& key, std::uint32_t const sequence, boost::asio::yield_context yield)
const override
{
LOG(log_.debug()) << "Fetching ledger object for seq " << sequence << ", key = " << ripple::to_string(key);
if (auto const res = executor_.read(yield, schema_->selectObject, key, sequence); res)
{
if (auto const result = res->template get<Blob>(); result)
{
if (result->size())
return *result;
}
else
{
LOG(log_.debug()) << "Could not fetch ledger object - no rows";
}
}
else
{
LOG(log_.error()) << "Could not fetch ledger object: " << res.error();
}
return std::nullopt;
}
std::optional<TransactionAndMetadata>
fetchTransaction(ripple::uint256 const& hash, boost::asio::yield_context yield) const override
{
if (auto const res = executor_.read(yield, schema_->selectTransaction, hash); res)
{
if (auto const maybeValue = res->template get<Blob, Blob, uint32_t, uint32_t>(); maybeValue)
{
auto [transaction, meta, seq, date] = *maybeValue;
return std::make_optional<TransactionAndMetadata>(transaction, meta, seq, date);
}
else
{
LOG(log_.debug()) << "Could not fetch transaction - no rows";
}
}
else
{
LOG(log_.error()) << "Could not fetch transaction: " << res.error();
}
return std::nullopt;
}
std::optional<ripple::uint256>
doFetchSuccessorKey(ripple::uint256 key, std::uint32_t const ledgerSequence, boost::asio::yield_context yield)
const override
{
if (auto const res = executor_.read(yield, schema_->selectSuccessor, key, ledgerSequence); res)
{
if (auto const result = res->template get<ripple::uint256>(); result)
{
if (*result == lastKey)
return std::nullopt;
return *result;
}
else
{
LOG(log_.debug()) << "Could not fetch successor - no rows";
}
}
else
{
LOG(log_.error()) << "Could not fetch successor: " << res.error();
}
return std::nullopt;
}
std::vector<TransactionAndMetadata>
fetchTransactions(std::vector<ripple::uint256> const& hashes, boost::asio::yield_context yield) const override
{
if (hashes.size() == 0)
return {};
auto const numHashes = hashes.size();
std::vector<TransactionAndMetadata> results;
results.reserve(numHashes);
std::vector<Statement> statements;
statements.reserve(numHashes);
auto const timeDiff = util::timed([this, yield, &results, &hashes, &statements]() {
// TODO: seems like a job for "hash IN (list of hashes)" instead?
std::transform(
std::cbegin(hashes), std::cend(hashes), std::back_inserter(statements), [this](auto const& hash) {
return schema_->selectTransaction.bind(hash);
});
auto const entries = executor_.readEach(yield, statements);
std::transform(
std::cbegin(entries),
std::cend(entries),
std::back_inserter(results),
[](auto const& res) -> TransactionAndMetadata {
if (auto const maybeRow = res.template get<Blob, Blob, uint32_t, uint32_t>(); maybeRow)
return *maybeRow;
else
return {};
});
});
assert(numHashes == results.size());
LOG(log_.debug()) << "Fetched " << numHashes << " transactions from Cassandra in " << timeDiff
<< " milliseconds";
return results;
}
std::vector<Blob>
doFetchLedgerObjects(
std::vector<ripple::uint256> const& keys,
std::uint32_t const sequence,
boost::asio::yield_context yield) const override
{
if (keys.size() == 0)
return {};
auto const numKeys = keys.size();
LOG(log_.trace()) << "Fetching " << numKeys << " objects";
std::vector<Blob> results;
results.reserve(numKeys);
std::vector<Statement> statements;
statements.reserve(numKeys);
// TODO: seems like a job for "key IN (list of keys)" instead?
std::transform(
std::cbegin(keys), std::cend(keys), std::back_inserter(statements), [this, &sequence](auto const& key) {
return schema_->selectObject.bind(key, sequence);
});
auto const entries = executor_.readEach(yield, statements);
std::transform(
std::cbegin(entries), std::cend(entries), std::back_inserter(results), [](auto const& res) -> Blob {
if (auto const maybeValue = res.template get<Blob>(); maybeValue)
return *maybeValue;
else
return {};
});
LOG(log_.trace()) << "Fetched " << numKeys << " objects";
return results;
}
std::vector<LedgerObject>
fetchLedgerDiff(std::uint32_t const ledgerSequence, boost::asio::yield_context yield) const override
{
auto const [keys, timeDiff] = util::timed([this, &ledgerSequence, yield]() -> std::vector<ripple::uint256> {
auto const res = executor_.read(yield, schema_->selectDiff, ledgerSequence);
if (not res)
{
LOG(log_.error()) << "Could not fetch ledger diff: " << res.error() << "; ledger = " << ledgerSequence;
return {};
}
auto const& results = res.value();
if (not results)
{
LOG(log_.error()) << "Could not fetch ledger diff - no rows; ledger = " << ledgerSequence;
return {};
}
std::vector<ripple::uint256> keys;
for (auto [key] : extract<ripple::uint256>(results))
keys.push_back(key);
return keys;
});
// one of the above errors must have happened
if (keys.empty())
return {};
LOG(log_.debug()) << "Fetched " << keys.size() << " diff hashes from Cassandra in " << timeDiff
<< " milliseconds";
auto const objs = fetchLedgerObjects(keys, ledgerSequence, yield);
std::vector<LedgerObject> results;
results.reserve(keys.size());
std::transform(
std::cbegin(keys),
std::cend(keys),
std::cbegin(objs),
std::back_inserter(results),
[](auto const& key, auto const& obj) {
return LedgerObject{key, obj};
});
return results;
}
void
doWriteLedgerObject(std::string&& key, std::uint32_t const seq, std::string&& blob) override
{
LOG(log_.trace()) << " Writing ledger object " << key.size() << ":" << seq << " [" << blob.size() << " bytes]";
if (range)
executor_.write(schema_->insertDiff, seq, key);
executor_.write(schema_->insertObject, std::move(key), seq, std::move(blob));
}
void
writeSuccessor(std::string&& key, std::uint32_t const seq, std::string&& successor) override
{
LOG(log_.trace()) << "Writing successor. key = " << key.size() << " bytes. "
<< " seq = " << std::to_string(seq) << " successor = " << successor.size() << " bytes.";
assert(key.size() != 0);
assert(successor.size() != 0);
executor_.write(schema_->insertSuccessor, std::move(key), seq, std::move(successor));
}
void
writeAccountTransactions(std::vector<AccountTransactionsData>&& data) override
{
std::vector<Statement> statements;
statements.reserve(data.size() * 10); // assume 10 transactions avg
for (auto& record : data)
{
std::transform(
std::begin(record.accounts),
std::end(record.accounts),
std::back_inserter(statements),
[this, &record](auto&& account) {
return schema_->insertAccountTx.bind(
std::move(account),
std::make_tuple(record.ledgerSequence, record.transactionIndex),
record.txHash);
});
}
executor_.write(std::move(statements));
}
void
writeNFTTransactions(std::vector<NFTTransactionsData>&& data) override
{
std::vector<Statement> statements;
statements.reserve(data.size());
std::transform(std::cbegin(data), std::cend(data), std::back_inserter(statements), [this](auto const& record) {
return schema_->insertNFTTx.bind(
record.tokenID, std::make_tuple(record.ledgerSequence, record.transactionIndex), record.txHash);
});
executor_.write(std::move(statements));
}
void
writeTransaction(
std::string&& hash,
std::uint32_t const seq,
std::uint32_t const date,
std::string&& transaction,
std::string&& metadata) override
{
LOG(log_.trace()) << "Writing txn to cassandra";
executor_.write(schema_->insertLedgerTransaction, seq, hash);
executor_.write(
schema_->insertTransaction, std::move(hash), seq, date, std::move(transaction), std::move(metadata));
}
void
writeNFTs(std::vector<NFTsData>&& data) override
{
std::vector<Statement> statements;
statements.reserve(data.size() * 3);
for (NFTsData const& record : data)
{
statements.push_back(
schema_->insertNFT.bind(record.tokenID, record.ledgerSequence, record.owner, record.isBurned));
// If `uri` is set (and it can be set to an empty uri), we know this
// is a net-new NFT. That is, this NFT has not been seen before by
// us _OR_ it is in the extreme edge case of a re-minted NFT ID with
// the same NFT ID as an already-burned token. In this case, we need
// to record the URI and link to the issuer_nf_tokens table.
if (record.uri)
{
statements.push_back(schema_->insertIssuerNFT.bind(
ripple::nft::getIssuer(record.tokenID),
static_cast<uint32_t>(ripple::nft::getTaxon(record.tokenID)),
record.tokenID));
statements.push_back(
schema_->insertNFTURI.bind(record.tokenID, record.ledgerSequence, record.uri.value()));
}
}
executor_.write(std::move(statements));
}
void
startWrites() const override
{
// Note: no-op in original implementation too.
// probably was used in PG to start a transaction or smth.
}
bool
isTooBusy() const override
{
return executor_.isTooBusy();
}
private:
bool
executeSyncUpdate(Statement statement)
{
auto const res = executor_.writeSync(statement);
auto maybeSuccess = res->template get<bool>();
if (not maybeSuccess)
{
LOG(log_.error()) << "executeSyncUpdate - error getting result - no row";
return false;
}
if (not maybeSuccess.value())
{
LOG(log_.warn()) << "Update failed. Checking if DB state is what we expect";
// error may indicate that another writer wrote something.
// in this case let's just compare the current state of things
// against what we were trying to write in the first place and
// use that as the source of truth for the result.
auto rng = hardFetchLedgerRangeNoThrow();
return rng && rng->maxSequence == ledgerSequence_;
}
return true;
}
};
using CassandraBackend = BasicCassandraBackend<SettingsProvider, detail::DefaultExecutionStrategy<>>;
} // namespace data::cassandra

253
src/data/DBHelpers.h Normal file
View File

@@ -0,0 +1,253 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2022, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
/** @file */
#pragma once
#include <ripple/basics/Log.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/SField.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/protocol/TxMeta.h>
#include <boost/container/flat_set.hpp>
#include <data/Types.h>
/**
* @brief Struct used to keep track of what to write to account_transactions/account_tx tables.
*/
struct AccountTransactionsData
{
boost::container::flat_set<ripple::AccountID> accounts;
std::uint32_t ledgerSequence;
std::uint32_t transactionIndex;
ripple::uint256 txHash;
AccountTransactionsData(ripple::TxMeta& meta, ripple::uint256 const& txHash, beast::Journal& j)
: accounts(meta.getAffectedAccounts())
, ledgerSequence(meta.getLgrSeq())
, transactionIndex(meta.getIndex())
, txHash(txHash)
{
}
AccountTransactionsData() = default;
};
/**
* @brief Represents a link from a tx to an NFT that was targeted/modified/created by it.
*
* Gets written to nf_token_transactions table and the like.
*/
struct NFTTransactionsData
{
ripple::uint256 tokenID;
std::uint32_t ledgerSequence;
std::uint32_t transactionIndex;
ripple::uint256 txHash;
NFTTransactionsData(ripple::uint256 const& tokenID, ripple::TxMeta const& meta, ripple::uint256 const& txHash)
: tokenID(tokenID), ledgerSequence(meta.getLgrSeq()), transactionIndex(meta.getIndex()), txHash(txHash)
{
}
};
/**
* @brief Represents an NFT state at a particular ledger.
*
* Gets written to nf_tokens table and the like.
*/
struct NFTsData
{
ripple::uint256 tokenID;
std::uint32_t ledgerSequence;
// The transaction index is only stored because we want to store only the
// final state of an NFT per ledger. Since we pull this from transactions
// we keep track of which tx index created this so we can de-duplicate, as
// it is possible for one ledger to have multiple txs that change the
// state of the same NFT. This field is not applicable when we are loading
// initial NFT state via ledger objects, since we do not have to tiebreak
// NFT state for a given ledger in that case.
std::optional<std::uint32_t> transactionIndex;
ripple::AccountID owner;
// We only set the uri if this is a mint tx, or if we are
// loading initial state from NFTokenPage objects. In other words,
// uri should only be set if the etl process believes this NFT hasn't
// been seen before in our local database. We do this so that we don't
// write to the the nf_token_uris table every
// time the same NFT changes hands. We also can infer if there is a URI
// that we need to write to the issuer_nf_tokens table.
std::optional<ripple::Blob> uri;
bool isBurned = false;
// This constructor is used when parsing an NFTokenMint tx.
// Unfortunately because of the extreme edge case of being able to
// re-mint an NFT with the same ID, we must explicitly record a null
// URI. For this reason, we _always_ write this field as a result of
// this tx.
NFTsData(
ripple::uint256 const& tokenID,
ripple::AccountID const& owner,
ripple::Blob const& uri,
ripple::TxMeta const& meta)
: tokenID(tokenID), ledgerSequence(meta.getLgrSeq()), transactionIndex(meta.getIndex()), owner(owner), uri(uri)
{
}
// This constructor is used when parsing an NFTokenBurn or
// NFTokenAcceptOffer tx
NFTsData(ripple::uint256 const& tokenID, ripple::AccountID const& owner, ripple::TxMeta const& meta, bool isBurned)
: tokenID(tokenID)
, ledgerSequence(meta.getLgrSeq())
, transactionIndex(meta.getIndex())
, owner(owner)
, isBurned(isBurned)
{
}
// This constructor is used when parsing an NFTokenPage directly from
// ledger state.
// Unfortunately because of the extreme edge case of being able to
// re-mint an NFT with the same ID, we must explicitly record a null
// URI. For this reason, we _always_ write this field as a result of
// this tx.
NFTsData(
ripple::uint256 const& tokenID,
std::uint32_t const ledgerSequence,
ripple::AccountID const& owner,
ripple::Blob const& uri)
: tokenID(tokenID), ledgerSequence(ledgerSequence), owner(owner), uri(uri)
{
}
};
/**
* @brief Check whether the supplied object is an offer.
*
* @param object The object to check
* @return true if the object is an offer; false otherwise
*/
template <class T>
inline bool
isOffer(T const& object)
{
short offer_bytes = (object[1] << 8) | object[2];
return offer_bytes == 0x006f;
}
/**
* @brief Check whether the supplied hex represents an offer object.
*
* @param object The object to check
* @return true if the object is an offer; false otherwise
*/
template <class T>
inline bool
isOfferHex(T const& object)
{
auto blob = ripple::strUnHex(4, object.begin(), object.begin() + 4);
if (blob)
return isOffer(*blob);
return false;
}
/**
* @brief Check whether the supplied object is a dir node.
*
* @param object The object to check
* @return true if the object is a dir node; false otherwise
*/
template <class T>
inline bool
isDirNode(T const& object)
{
short spaceKey = (object.data()[1] << 8) | object.data()[2];
return spaceKey == 0x0064;
}
/**
* @brief Check whether the supplied object is a book dir.
*
* @param key The key into the object
* @param object The object to check
* @return true if the object is a book dir; false otherwise
*/
template <class T, class R>
inline bool
isBookDir(T const& key, R const& object)
{
if (!isDirNode(object))
return false;
ripple::STLedgerEntry const sle{ripple::SerialIter{object.data(), object.size()}, key};
return !sle[~ripple::sfOwner].has_value();
}
/**
* @brief Get the book out of an offer object.
*
* @param offer The offer to get the book for
* @return Book as ripple::uint256
*/
template <class T>
inline ripple::uint256
getBook(T const& offer)
{
ripple::SerialIter it{offer.data(), offer.size()};
ripple::SLE sle{it, {}};
ripple::uint256 book = sle.getFieldH256(ripple::sfBookDirectory);
return book;
}
/**
* @brief Get the book base.
*
* @param key The key to get the book base out of
* @return Book base as ripple::uint256
*/
template <class T>
inline ripple::uint256
getBookBase(T const& key)
{
assert(key.size() == ripple::uint256::size());
ripple::uint256 ret;
for (size_t i = 0; i < 24; ++i)
ret.data()[i] = key.data()[i];
return ret;
}
/**
* @brief Stringify a ripple::uint256.
*
* @param input The input value
* @return The input value as a string
*/
inline std::string
uint256ToString(ripple::uint256 const& input)
{
return {reinterpret_cast<const char*>(input.data()), input.size()};
}
/** @brief The ripple epoch start timestamp. Midnight on 1st January 2000. */
static constexpr std::uint32_t rippleEpochStart = 946684800;

160
src/data/LedgerCache.cpp Normal file
View File

@@ -0,0 +1,160 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2022, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <data/LedgerCache.h>
namespace data {
uint32_t
LedgerCache::latestLedgerSequence() const
{
std::shared_lock lck{mtx_};
return latestSeq_;
}
void
LedgerCache::update(std::vector<LedgerObject> const& objs, uint32_t seq, bool isBackground)
{
if (disabled_)
return;
{
std::scoped_lock lck{mtx_};
if (seq > latestSeq_)
{
assert(seq == latestSeq_ + 1 || latestSeq_ == 0);
latestSeq_ = seq;
}
for (auto const& obj : objs)
{
if (obj.blob.size())
{
if (isBackground && deletes_.count(obj.key))
continue;
auto& e = map_[obj.key];
if (seq > e.seq)
{
e = {seq, obj.blob};
}
}
else
{
map_.erase(obj.key);
if (!full_ && !isBackground)
deletes_.insert(obj.key);
}
}
}
}
std::optional<LedgerObject>
LedgerCache::getSuccessor(ripple::uint256 const& key, uint32_t seq) const
{
if (!full_)
return {};
std::shared_lock{mtx_};
successorReqCounter_++;
if (seq != latestSeq_)
return {};
auto e = map_.upper_bound(key);
if (e == map_.end())
return {};
successorHitCounter_++;
return {{e->first, e->second.blob}};
}
std::optional<LedgerObject>
LedgerCache::getPredecessor(ripple::uint256 const& key, uint32_t seq) const
{
if (!full_)
return {};
std::shared_lock lck{mtx_};
if (seq != latestSeq_)
return {};
auto e = map_.lower_bound(key);
if (e == map_.begin())
return {};
--e;
return {{e->first, e->second.blob}};
}
std::optional<Blob>
LedgerCache::get(ripple::uint256 const& key, uint32_t seq) const
{
std::shared_lock lck{mtx_};
if (seq > latestSeq_)
return {};
objectReqCounter_++;
auto e = map_.find(key);
if (e == map_.end())
return {};
if (seq < e->second.seq)
return {};
objectHitCounter_++;
return {e->second.blob};
}
void
LedgerCache::setDisabled()
{
disabled_ = true;
}
void
LedgerCache::setFull()
{
if (disabled_)
return;
full_ = true;
std::scoped_lock lck{mtx_};
deletes_.clear();
}
bool
LedgerCache::isFull() const
{
return full_;
}
size_t
LedgerCache::size() const
{
std::shared_lock lck{mtx_};
return map_.size();
}
float
LedgerCache::getObjectHitRate() const
{
if (!objectReqCounter_)
return 1;
return ((float)objectHitCounter_) / objectReqCounter_;
}
float
LedgerCache::getSuccessorHitRate() const
{
if (!successorReqCounter_)
return 1;
return ((float)successorHitCounter_) / successorReqCounter_;
}
} // namespace data

154
src/data/LedgerCache.h Normal file
View File

@@ -0,0 +1,154 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2022, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <ripple/basics/base_uint.h>
#include <ripple/basics/hardened_hash.h>
#include <data/Types.h>
#include <map>
#include <mutex>
#include <shared_mutex>
#include <utility>
#include <vector>
namespace data {
/**
* @brief Cache for an entire ledger.
*/
class LedgerCache
{
struct CacheEntry
{
uint32_t seq = 0;
Blob blob;
};
// counters for fetchLedgerObject(s) hit rate
mutable std::atomic_uint32_t objectReqCounter_ = 0;
mutable std::atomic_uint32_t objectHitCounter_ = 0;
// counters for fetchSuccessorKey hit rate
mutable std::atomic_uint32_t successorReqCounter_ = 0;
mutable std::atomic_uint32_t successorHitCounter_ = 0;
std::map<ripple::uint256, CacheEntry> map_;
mutable std::shared_mutex mtx_;
uint32_t latestSeq_ = 0;
std::atomic_bool full_ = false;
std::atomic_bool disabled_ = false;
// temporary set to prevent background thread from writing already deleted data. not used when cache is full
std::unordered_set<ripple::uint256, ripple::hardened_hash<>> deletes_;
public:
/**
* @brief Update the cache with new ledger objects.
*
* @param blobs The ledger objects to update cache with
* @param seq The sequence to update cache for
* @param isBackground Should be set to true when writing old data from a background thread
*/
void
update(std::vector<LedgerObject> const& blobs, uint32_t seq, bool isBackground = false);
/**
* @brief Fetch a cached object by its key and sequence number.
*
* @param key The key to fetch for
* @param seq The sequence to fetch for
* @return If found in cache, will return the cached Blob; otherwise nullopt is returned
*/
std::optional<Blob>
get(ripple::uint256 const& key, uint32_t seq) const;
/**
* @brief Gets a cached successor.
*
* Note: This function always returns std::nullopt when @ref isFull() returns false.
*
* @param key The key to fetch for
* @param seq The sequence to fetch for
* @return If found in cache, will return the cached successor; otherwise nullopt is returned
*/
std::optional<LedgerObject>
getSuccessor(ripple::uint256 const& key, uint32_t seq) const;
/**
* @brief Gets a cached predcessor.
*
* Note: This function always returns std::nullopt when @ref isFull() returns false.
*
* @param key The key to fetch for
* @param seq The sequence to fetch for
* @return If found in cache, will return the cached predcessor; otherwise nullopt is returned
*/
std::optional<LedgerObject>
getPredecessor(ripple::uint256 const& key, uint32_t seq) const;
/**
* @brief Disables the cache.
*/
void
setDisabled();
/**
* @brief Sets the full flag to true.
*
* This is used when cache loaded in its entirety at startup of the application. This can be either loaded from DB,
* populated together with initial ledger download (on first run) or downloaded from a peer node (specified in
* config).
*/
void
setFull();
/**
* @return The latest ledger sequence for which cache is available.
*/
uint32_t
latestLedgerSequence() const;
/**
* @return true if the cache has all data for the most recent ledger; false otherwise
*/
bool
isFull() const;
/**
* @return The total size of the cache.
*/
size_t
size() const;
/**
* @return A number representing the success rate of hitting an object in the cache versus missing it.
*/
float
getObjectHitRate() const;
/**
* @return A number representing the success rate of hitting a successor in the cache versus missing it.
*/
float
getSuccessorHitRate() const;
};
} // namespace data

221
src/data/README.md Normal file
View File

@@ -0,0 +1,221 @@
# Backend
## Background
The backend of Clio is responsible for handling the proper reading and writing of past ledger data from and to a given database. As of right now, Cassandra and ScyllaDB are the only supported databases that are production-ready. Support for database types can be easily extended by creating new implementations which implements the virtual methods of `BackendInterface.h`. Then, use the Factory Object Design Pattern to simply add logic statements to `BackendFactory.h` that return the new database interface for a specific `type` in Clio's configuration file.
## Data Model
The data model used by Clio to read and write ledger data is different from what Rippled uses. Rippled uses a novel data structure named [*SHAMap*](https://github.com/ripple/rippled/blob/master/src/ripple/shamap/README.md), which is a combination of a Merkle Tree and a Radix Trie. In a SHAMap, ledger objects are stored in the root vertices of the tree. Thus, looking up a record located at the leaf node of the SHAMap executes a tree search, where the path from the root node to the leaf node is the key of the record. Rippled nodes can also generate a proof-tree by forming a subtree with all the path nodes and their neighbors, which can then be used to prove the existnce of the leaf node data to other Rippled nodes. In short, the main purpose of the SHAMap data structure is to facilitate the fast validation of data integrity between different decentralized Rippled nodes.
Since Clio only extracts past validated ledger data from a group of trusted Rippled nodes, it can be safely assumed that these ledger data are correct without the need to validate with other nodes in the XRP peer-to-peer network. Because of this, Clio is able to use a flattened data model to store the past validated ledger data, which allows for direct record lookup with much faster constant time operations.
There are three main types of data in each XRP ledger version, they are [Ledger Header](https://xrpl.org/ledger-header.html), [Transaction Set](https://xrpl.org/transaction-formats.html) and [State Data](https://xrpl.org/ledger-object-types.html). Due to the structural differences of the different types of databases, Clio may choose to represent these data using a different schema for each unique database type.
**Keywords**
*Sequence*: A unique incrementing identification number used to label the different ledger versions.
*Hash*: The SHA512-half (calculate SHA512 and take the first 256 bits) hash of various ledger data like the entire ledger or specific ledger objects.
*Ledger Object*: The [binary-encoded](https://xrpl.org/serialization.html) STObject containing specific data (i.e. metadata, transaction data).
*Metadata*: The data containing [detailed information](https://xrpl.org/transaction-metadata.html#transaction-metadata) of the outcome of a specific transaction, regardless of whether the transaction was successful.
*Transaction data*: The data containing the [full details](https://xrpl.org/transaction-common-fields.html) of a specific transaction.
*Object Index*: The pseudo-random unique identifier of a ledger object, created by hashing the data of the object.
## Cassandra Implementation
Cassandra is a distributed wide-column NoSQL database designed to handle large data throughput with high availability and no single point of failure. By leveraging Cassandra, Clio will be able to quickly and reliably scale up when needed simply by adding more Cassandra nodes to the Cassandra cluster configuration.
In Cassandra, Clio will be creating 9 tables to store the ledger data, they are `ledger_transactions`, `transactions`, `ledger_hashes`, `ledger_range`, `objects`, `ledgers`, `diff`, `account_tx`, and `successor`. Their schemas and how they work are detailed below.
*Note, if you would like visually explore the data structure of the Cassandra database, you can first run Clio server with database `type` configured as `cassandra` to fill ledger data from Rippled nodes into Cassandra, then use a GUI database management tool like [Datastax's Opcenter](https://docs.datastax.com/en/install/6.0/install/opscInstallOpsc.html) to interactively view it.*
### `ledger_transactions`
```
CREATE TABLE clio.ledger_transactions (
ledger_sequence bigint, # The sequence number of the ledger version
hash blob, # Hash of all the transactions on this ledger version
PRIMARY KEY (ledger_sequence, hash)
) WITH CLUSTERING ORDER BY (hash ASC) ...
```
This table stores the hashes of all transactions in a given ledger sequence ordered by the hash value in ascending order.
### `transactions`
```
CREATE TABLE clio.transactions (
hash blob PRIMARY KEY, # The transaction hash
date bigint, # Date of the transaction
ledger_sequence bigint, # The sequence that the transaction was validated
metadata blob, # Metadata of the transaction
transaction blob # Data of the transaction
) ...
```
This table stores the full transaction and metadata of each ledger version with the transaction hash as the primary key.
To look up all the transactions that were validated in a ledger version with sequence `n`, one can first get the all the transaction hashes in that ledger version by querying `SELECT * FROM ledger_transactions WHERE ledger_sequence = n;`. Then, iterate through the list of hashes and query `SELECT * FROM transactions WHERE hash = one_of_the_hash_from_the_list;` to get the detailed transaction data.
### `ledger_hashes`
```
CREATE TABLE clio.ledger_hashes (
hash blob PRIMARY KEY, # Hash of entire ledger version's data
sequence bigint # The sequence of the ledger version
) ...
```
This table stores the hash of all ledger versions by their sequences.
### `ledger_range`
```
CREATE TABLE clio.ledger_range (
is_latest boolean PRIMARY KEY, # Whether this sequence is the stopping range
sequence bigint # The sequence number of the starting/stopping range
) ...
```
This table marks the range of ledger versions that is stored on this specific Cassandra node. Because of its nature, there are only two records in this table with `false` and `true` values for `is_latest`, marking the starting and ending sequence of the ledger range.
### `objects`
```
CREATE TABLE clio.objects (
key blob, # Object index of the object
sequence bigint, # The sequence this object was last updated
object blob, # Data of the object
PRIMARY KEY (key, sequence)
) WITH CLUSTERING ORDER BY (sequence DESC) ...
```
This table stores the specific data of all objects that ever existed on the XRP network, even if they are deleted (which is represented with a special `0x` value). The records are ordered by descending sequence, where the newest validated ledger objects are at the top.
This table is updated when all data for a given ledger sequence has been written to the various tables in the database. For each ledger, many associated records are written to different tables. This table is used as a synchronization mechanism, to prevent the application from reading data from a ledger for which all data has not yet been fully written.
### `ledgers`
```
CREATE TABLE clio.ledgers (
sequence bigint PRIMARY KEY, # Sequence of the ledger version
header blob # Data of the header
) ...
```
This table stores the ledger header data of specific ledger versions by their sequence.
### `diff`
```
CREATE TABLE clio.diff (
seq bigint, # Sequence of the ledger version
key blob, # Hash of changes in the ledger version
PRIMARY KEY (seq, key)
) WITH CLUSTERING ORDER BY (key ASC) ...
```
This table stores the object index of all the changes in each ledger version.
### `account_tx`
```
CREATE TABLE clio.account_tx (
account blob,
seq_idx frozen<tuple<bigint, bigint>>, # Tuple of (ledger_index, transaction_index)
hash blob, # Hash of the transaction
PRIMARY KEY (account, seq_idx)
) WITH CLUSTERING ORDER BY (seq_idx DESC) ...
```
This table stores the list of transactions affecting a given account. This includes transactions made by the account, as well as transactions received.
### `successor`
```
CREATE TABLE clio.successor (
key blob, # Object index
seq bigint, # The sequnce that this ledger object's predecessor and successor was updated
next blob, # Index of the next object that existed in this sequence
PRIMARY KEY (key, seq)
) WITH CLUSTERING ORDER BY (seq ASC) ...
```
This table is the important backbone of how histories of ledger objects are stored in Cassandra. The successor table stores the object index of all ledger objects that were validated on the XRP network along with the ledger sequence that the object was upated on. Due to the unique nature of the table with each key being ordered by the sequence, by tracing through the table with a specific sequence number, Clio can recreate a Linked List data structure that represents all the existing ledger object at that ledger sequence. The special value of `0x00...00` and `0xFF...FF` are used to label the head and tail of the Linked List in the successor table. The diagram below showcases how tracing through the same table but with different sequence parameter filtering can result in different Linked List data representing the corresponding past state of the ledger objects. A query like `SELECT * FROM successor WHERE key = ? AND seq <= n ORDER BY seq DESC LIMIT 1;` can effectively trace through the successor table and get the Linked List of a specific sequence `n`.
![Successor Table Trace Diagram](https://raw.githubusercontent.com/Shoukozumi/clio/9b2ea3efb6b164b02e9a5f0ef6717065a70f078c/src/backend/README.png)
*P.S.: The `diff` is `(DELETE 0x00...02, CREATE 0x00...03)` for `seq=1001` and `(CREATE 0x00...04)` for `seq=1002`, which is both accurately reflected with the Linked List trace*
In each new ledger version with sequence `n`, a ledger object `v` can either be **created**, **modified**, or **deleted**. For all three of these operations, the procedure to update the successor table can be broken down in to two steps:
1. Trace through the Linked List of the previous sequence to to find the ledger object `e` with the greatest object index smaller or equal than the `v`'s index. Save `e`'s `next` value (the index of the next ledger object) as `w`.
2. If `v` is...
1. Being **created**, add two new records of `seq=n` with one being `e` pointing to `v`, and `v` pointing to `w` (Linked List insertion operation).
2. Being **modified**, do nothing.
3. Being **deleted**, add a record of `seq=n` with `e` pointing to `v`'s `next` value (Linked List deletion operation).
### NFT data model
In `rippled` NFTs are stored in NFTokenPage ledger objects. This object is
implemented to save ledger space and has the property that it gives us O(1)
lookup time for an NFT, assuming we know who owns the NFT at a particular
ledger. However, if we do not know who owns the NFT at a specific ledger
height we have no alternative in rippled other than scanning the entire
ledger. Because of this tradeoff, clio implements a special NFT indexing data
structure that allows clio users to query NFTs quickly, while keeping
rippled's space-saving optimizations.
#### `nf_tokens`
```
CREATE TABLE clio.nf_tokens (
token_id blob, # The NFT's ID
sequence bigint, # Sequence of ledger version
owner blob, # The account ID of the owner of this NFT at this ledger
is_burned boolean, # True if token was burned in this ledger
PRIMARY KEY (token_id, sequence)
) WITH CLUSTERING ORDER BY (sequence DESC) ...
```
This table indexes NFT IDs with their owner at a given ledger. So
```
SELECT * FROM nf_tokens
WHERE token_id = N AND seq <= Y
ORDER BY seq DESC LIMIT 1;
```
will give you the owner of token N at ledger Y and whether it was burned. If
the token is burned, the owner field indicates the account that owned the
token at the time it was burned; it does not indicate the person who burned
the token, necessarily. If you need to determine who burned the token you can
use the `nft_history` API, which will give you the NFTokenBurn transaction
that burned this token, along with the account that submitted that
transaction.
#### `issuer_nf_tokens_v2`
```
CREATE TABLE clio.issuer_nf_tokens_v2 (
issuer blob, # The NFT issuer's account ID
taxon bigint, # The NFT's token taxon
token_id blob, # The NFT's ID
PRIMARY KEY (issuer, taxon, token_id)
) WITH CLUSTERING ORDER BY (taxon ASC, token_id ASC) ...
```
This table indexes token IDs against their issuer and issuer/taxon
combination. This is useful for determining all the NFTs a specific account
issued, or all the NFTs a specific account issued with a specific taxon. It is
not useful to know all the NFTs with a given taxon while excluding issuer, since the
meaning of a taxon is left to an issuer.
#### `nf_token_uris`
```
CREATE TABLE clio.nf_token_uris (
token_id blob, # The NFT's ID
sequence bigint, # Sequence of ledger version
uri blob, # The NFT's URI
PRIMARY KEY (token_id, sequence)
) WITH CLUSTERING ORDER BY (sequence DESC) ...
```
This table is used to store an NFT's URI. Without storing this here, we would
need to traverse the NFT owner's entire set of NFTs to find the URI, again due
to the way that NFTs are stored in rippled. Furthermore, instead of storing
this in the `nf_tokens` table, we store it here to save space. A given NFT
will have only one entry in this table (see caveat below), written to this
table as soon as clio sees the NFTokenMint transaction, or when clio loads an
NFTokenPage from the initial ledger it downloaded. However, the `nf_tokens`
table is written to every time an NFT changes ownership, or if it is burned.
Given this, why do we have to store the sequence? Unfortunately there is an
extreme edge case where a given NFT ID can be burned, and then re-minted with
a different URI. This is extremely unlikely, and might be fixed in a future
version to rippled, but just in case we can handle that edge case by allowing
a given NFT ID to have a new URI assigned in this case, without removing the
prior URI.
#### `nf_token_transactions`
```
CREATE TABLE clio.nf_token_transactions (
token_id blob, # The NFT's ID
seq_idx tuple<bigint, bigint>, # Tuple of (ledger_index, transaction_index)
hash blob, # Hash of the transaction
PRIMARY KEY (token_id, seq_idx)
) WITH CLUSTERING ORDER BY (seq_idx DESC) ...
```
This table is the NFT equivalent of `account_tx`. It's motivated by the exact
same reasons and serves the analogous purpose here. It drives the
`nft_history` API.

191
src/data/Types.h Normal file
View File

@@ -0,0 +1,191 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2022, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <ripple/basics/base_uint.h>
#include <ripple/protocol/AccountID.h>
#include <optional>
#include <string>
#include <vector>
namespace data {
using Blob = std::vector<unsigned char>;
/**
* @brief Represents an object in the ledger.
*/
struct LedgerObject
{
ripple::uint256 key;
Blob blob;
bool
operator==(const LedgerObject& other) const
{
return key == other.key && blob == other.blob;
}
};
/**
* @brief Represents a page of LedgerObjects.
*/
struct LedgerPage
{
std::vector<LedgerObject> objects;
std::optional<ripple::uint256> cursor;
};
/**
* @brief Represents a page of book offer objects.
*/
struct BookOffersPage
{
std::vector<LedgerObject> offers;
std::optional<ripple::uint256> cursor;
};
/**
* @brief Represents a transaction and its metadata bundled together.
*/
struct TransactionAndMetadata
{
Blob transaction;
Blob metadata;
std::uint32_t ledgerSequence = 0;
std::uint32_t date = 0;
TransactionAndMetadata() = default;
TransactionAndMetadata(
Blob const& transaction,
Blob const& metadata,
std::uint32_t ledgerSequence,
std::uint32_t date)
: transaction{transaction}, metadata{metadata}, ledgerSequence{ledgerSequence}, date{date}
{
}
TransactionAndMetadata(std::tuple<Blob, Blob, std::uint32_t, std::uint32_t> data)
: transaction{std::get<0>(data)}
, metadata{std::get<1>(data)}
, ledgerSequence{std::get<2>(data)}
, date{std::get<3>(data)}
{
}
bool
operator==(const TransactionAndMetadata& other) const
{
return transaction == other.transaction && metadata == other.metadata &&
ledgerSequence == other.ledgerSequence && date == other.date;
}
};
/**
* @brief Represents a cursor into the transactions table.
*/
struct TransactionsCursor
{
std::uint32_t ledgerSequence = 0;
std::uint32_t transactionIndex = 0;
TransactionsCursor() = default;
TransactionsCursor(std::uint32_t ledgerSequence, std::uint32_t transactionIndex)
: ledgerSequence{ledgerSequence}, transactionIndex{transactionIndex}
{
}
TransactionsCursor(std::tuple<std::uint32_t, std::uint32_t> data)
: ledgerSequence{std::get<0>(data)}, transactionIndex{std::get<1>(data)}
{
}
TransactionsCursor&
operator=(TransactionsCursor const&) = default;
bool
operator==(TransactionsCursor const& other) const = default;
[[nodiscard]] std::tuple<std::uint32_t, std::uint32_t>
asTuple() const
{
return std::make_tuple(ledgerSequence, transactionIndex);
}
};
/**
* @brief Represests a bundle of transactions with metadata and a cursor to the next page.
*/
struct TransactionsAndCursor
{
std::vector<TransactionAndMetadata> txns;
std::optional<TransactionsCursor> cursor;
};
/**
* @brief Represents a NFToken.
*/
struct NFT
{
ripple::uint256 tokenID;
std::uint32_t ledgerSequence;
ripple::AccountID owner;
Blob uri;
bool isBurned;
NFT() = default;
NFT(ripple::uint256 const& tokenID,
std::uint32_t ledgerSequence,
ripple::AccountID const& owner,
Blob const& uri,
bool isBurned)
: tokenID{tokenID}, ledgerSequence{ledgerSequence}, owner{owner}, uri{uri}, isBurned{isBurned}
{
}
NFT(ripple::uint256 const& tokenID, std::uint32_t ledgerSequence, ripple::AccountID const& owner, bool isBurned)
: NFT(tokenID, ledgerSequence, owner, {}, isBurned)
{
}
// clearly two tokens are the same if they have the same ID, but this struct stores the state of a given token at a
// given ledger sequence, so we also need to compare with ledgerSequence.
bool
operator==(NFT const& other) const
{
return tokenID == other.tokenID && ledgerSequence == other.ledgerSequence;
}
};
/**
* @brief Stores a range of sequences as a min and max pair.
*/
struct LedgerRange
{
std::uint32_t minSequence = 0;
std::uint32_t maxSequence = 0;
};
constexpr ripple::uint256 firstKey{"0000000000000000000000000000000000000000000000000000000000000000"};
constexpr ripple::uint256 lastKey{"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"};
constexpr ripple::uint256 hi192{"0000000000000000000000000000000000000000000000001111111111111111"};
} // namespace data

View File

@@ -0,0 +1,88 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/Types.h>
#include <boost/asio/spawn.hpp>
#include <chrono>
#include <concepts>
#include <optional>
#include <string>
namespace data::cassandra {
/**
* @brief The requirements of a settings provider.
*/
// clang-format off
template <typename T>
concept SomeSettingsProvider = requires(T a) {
{ a.getSettings() } -> std::same_as<Settings>;
{ a.getKeyspace() } -> std::same_as<std::string>;
{ a.getTablePrefix() } -> std::same_as<std::optional<std::string>>;
{ a.getReplicationFactor() } -> std::same_as<uint16_t>;
{ a.getTtl() } -> std::same_as<uint16_t>;
};
// clang-format on
/**
* @brief The requirements of an execution strategy.
*/
// clang-format off
template <typename T>
concept SomeExecutionStrategy = requires(
T a,
Settings settings,
Handle handle,
Statement statement,
std::vector<Statement> statements,
PreparedStatement prepared,
boost::asio::yield_context token
) {
{ T(settings, handle) };
{ a.sync() } -> std::same_as<void>;
{ a.isTooBusy() } -> std::same_as<bool>;
{ a.writeSync(statement) } -> std::same_as<ResultOrError>;
{ a.writeSync(prepared) } -> std::same_as<ResultOrError>;
{ a.write(prepared) } -> std::same_as<void>;
{ a.write(std::move(statements)) } -> std::same_as<void>;
{ a.read(token, prepared) } -> std::same_as<ResultOrError>;
{ a.read(token, statement) } -> std::same_as<ResultOrError>;
{ a.read(token, statements) } -> std::same_as<ResultOrError>;
{ a.readEach(token, statements) } -> std::same_as<std::vector<Result>>;
};
// clang-format on
/**
* @brief The requirements of a retry policy.
*/
// clang-format off
template <typename T>
concept SomeRetryPolicy = requires(T a, boost::asio::io_context ioc, CassandraError err, uint32_t attempt) {
{ T(ioc) };
{ a.shouldRetry(err) } -> std::same_as<bool>;
{ a.retry([](){}) } -> std::same_as<void>;
{ a.calculateDelay(attempt) } -> std::same_as<std::chrono::milliseconds>;
};
// clang-format on
} // namespace data::cassandra

111
src/data/cassandra/Error.h Normal file
View File

@@ -0,0 +1,111 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <cassandra.h>
#include <string>
namespace data::cassandra {
/**
* @brief A simple container for both error message and error code.
*/
class CassandraError
{
std::string message_;
uint32_t code_;
public:
CassandraError() = default; // default constructible required by Expected
CassandraError(std::string message, uint32_t code) : message_{message}, code_{code}
{
}
template <typename T>
friend std::string
operator+(T const& lhs, CassandraError const& rhs) requires std::is_convertible_v<T, std::string>
{
return lhs + rhs.message();
}
template <typename T>
friend bool
operator==(T const& lhs, CassandraError const& rhs) requires std::is_convertible_v<T, std::string>
{
return lhs == rhs.message();
}
template <std::integral T>
friend bool
operator==(T const& lhs, CassandraError const& rhs)
{
return lhs == rhs.code();
}
friend std::ostream&
operator<<(std::ostream& os, CassandraError const& err)
{
os << err.message();
return os;
}
/**
* @return The final error message as a std::string
*/
std::string
message() const
{
return message_;
}
/**
* @return The error code
*/
uint32_t
code() const
{
return code_;
}
/**
* @return true if the wrapped error is considered a timeout; false otherwise
*/
bool
isTimeout() const
{
if (code_ == CASS_ERROR_LIB_NO_HOSTS_AVAILABLE or code_ == CASS_ERROR_LIB_REQUEST_TIMED_OUT or
code_ == CASS_ERROR_SERVER_UNAVAILABLE or code_ == CASS_ERROR_SERVER_OVERLOADED or
code_ == CASS_ERROR_SERVER_READ_TIMEOUT)
return true;
return false;
}
/**
* @return true if the wrapped error is an invalid query; false otherwise
*/
bool
isInvalidQuery() const
{
return code_ == CASS_ERROR_SERVER_INVALID_QUERY;
}
};
} // namespace data::cassandra

View File

@@ -0,0 +1,155 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <data/cassandra/Handle.h>
namespace data::cassandra {
Handle::Handle(Settings clusterSettings) : cluster_{clusterSettings}
{
}
Handle::Handle(std::string_view contactPoints) : Handle{Settings::defaultSettings().withContactPoints(contactPoints)}
{
}
Handle::~Handle()
{
[[maybe_unused]] auto _ = disconnect(); // attempt to disconnect
}
Handle::FutureType
Handle::asyncConnect() const
{
return cass_session_connect(session_, cluster_);
}
Handle::MaybeErrorType
Handle::connect() const
{
return asyncConnect().await();
}
Handle::FutureType
Handle::asyncConnect(std::string_view keyspace) const
{
return cass_session_connect_keyspace(session_, cluster_, keyspace.data());
}
Handle::MaybeErrorType
Handle::connect(std::string_view keyspace) const
{
return asyncConnect(keyspace).await();
}
Handle::FutureType
Handle::asyncDisconnect() const
{
return cass_session_close(session_);
}
Handle::MaybeErrorType
Handle::disconnect() const
{
return asyncDisconnect().await();
}
Handle::FutureType
Handle::asyncReconnect(std::string_view keyspace) const
{
if (auto rc = asyncDisconnect().await(); not rc) // sync
throw std::logic_error("Reconnect to keyspace '" + std::string{keyspace} + "' failed: " + rc.error());
return asyncConnect(keyspace);
}
Handle::MaybeErrorType
Handle::reconnect(std::string_view keyspace) const
{
return asyncReconnect(keyspace).await();
}
std::vector<Handle::FutureType>
Handle::asyncExecuteEach(std::vector<Statement> const& statements) const
{
std::vector<Handle::FutureType> futures;
for (auto const& statement : statements)
futures.push_back(cass_session_execute(session_, statement));
return futures;
}
Handle::MaybeErrorType
Handle::executeEach(std::vector<Statement> const& statements) const
{
for (auto futures = asyncExecuteEach(statements); auto const& future : futures)
{
if (auto const rc = future.await(); not rc)
return rc;
}
return {};
}
Handle::FutureType
Handle::asyncExecute(Statement const& statement) const
{
return cass_session_execute(session_, statement);
}
Handle::FutureWithCallbackType
Handle::asyncExecute(Statement const& statement, std::function<void(Handle::ResultOrErrorType)>&& cb) const
{
return Handle::FutureWithCallbackType{cass_session_execute(session_, statement), std::move(cb)};
}
Handle::ResultOrErrorType
Handle::execute(Statement const& statement) const
{
return asyncExecute(statement).get();
}
Handle::FutureType
Handle::asyncExecute(std::vector<Statement> const& statements) const
{
return cass_session_execute_batch(session_, Batch{statements});
}
Handle::MaybeErrorType
Handle::execute(std::vector<Statement> const& statements) const
{
return asyncExecute(statements).await();
}
Handle::FutureWithCallbackType
Handle::asyncExecute(std::vector<Statement> const& statements, std::function<void(Handle::ResultOrErrorType)>&& cb)
const
{
return Handle::FutureWithCallbackType{cass_session_execute_batch(session_, Batch{statements}), std::move(cb)};
}
Handle::PreparedStatementType
Handle::prepare(std::string_view query) const
{
Handle::FutureType future = cass_session_prepare(session_, query.data());
if (auto const rc = future.await(); rc)
return cass_future_get_prepared(future);
else
throw std::runtime_error(rc.error().message());
}
} // namespace data::cassandra

332
src/data/cassandra/Handle.h Normal file
View File

@@ -0,0 +1,332 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/Error.h>
#include <data/cassandra/Types.h>
#include <data/cassandra/impl/Batch.h>
#include <data/cassandra/impl/Cluster.h>
#include <data/cassandra/impl/Future.h>
#include <data/cassandra/impl/ManagedObject.h>
#include <data/cassandra/impl/Result.h>
#include <data/cassandra/impl/Session.h>
#include <data/cassandra/impl/Statement.h>
#include <util/Expected.h>
#include <cassandra.h>
#include <chrono>
#include <compare>
#include <iterator>
#include <vector>
namespace data::cassandra {
/**
* @brief Represents a handle to the cassandra database cluster
*/
class Handle
{
detail::Cluster cluster_;
detail::Session session_;
public:
using ResultOrErrorType = ResultOrError;
using MaybeErrorType = MaybeError;
using FutureWithCallbackType = FutureWithCallback;
using FutureType = Future;
using StatementType = Statement;
using PreparedStatementType = PreparedStatement;
using ResultType = Result;
/**
* @brief Construct a new handle from a @ref detail::Settings object.
*
* @param clusterSettings The settings to use
*/
explicit Handle(Settings clusterSettings = Settings::defaultSettings());
/**
* @brief Construct a new handle with default settings and only by setting the contact points.
*
* @param contactPoints The contact points to use instead of settings
*/
explicit Handle(std::string_view contactPoints);
/**
* @brief Disconnects gracefully if possible.
*/
~Handle();
/**
* @brief Move is supported.
*/
Handle(Handle&&) = default;
/**
* @brief Connect to the cluster asynchronously.
*
* @return A future
*/
[[nodiscard]] FutureType
asyncConnect() const;
/**
* @brief Synchonous version of the above.
*
* See @ref asyncConnect() const for how this works.
*
* @return Possibly an error
*/
[[nodiscard]] MaybeErrorType
connect() const;
/**
* @brief Connect to the the specified keyspace asynchronously.
*
* @param keyspace The keyspace to use
* @return A future
*/
[[nodiscard]] FutureType
asyncConnect(std::string_view keyspace) const;
/**
* @brief Synchonous version of the above.
*
* See @ref asyncConnect(std::string_view) const for how this works.
*
* @param keyspace The keyspace to use
* @return Possibly an error
*/
[[nodiscard]] MaybeErrorType
connect(std::string_view keyspace) const;
/**
* @brief Disconnect from the cluster asynchronously.
*
* @return A future
*/
[[nodiscard]] FutureType
asyncDisconnect() const;
/**
* @brief Synchonous version of the above.
*
* See @ref asyncDisconnect() const for how this works.
*
* @return Possibly an error
*/
[[maybe_unused]] MaybeErrorType
disconnect() const;
/**
* @brief Reconnect to the the specified keyspace asynchronously.
*
* @param keyspace The keyspace to use
* @return A future
*/
[[nodiscard]] FutureType
asyncReconnect(std::string_view keyspace) const;
/**
* @brief Synchonous version of the above.
*
* See @ref asyncReconnect(std::string_view) const for how this works.
*
* @param keyspace The keyspace to use
* @return Possibly an error
*/
[[nodiscard]] MaybeErrorType
reconnect(std::string_view keyspace) const;
/**
* @brief Execute a simple query with optional args asynchronously.
*
* @param query The query to execute
* @param args The arguments to bind for execution
* @return A future
*/
template <typename... Args>
[[nodiscard]] FutureType
asyncExecute(std::string_view query, Args&&... args) const
{
auto statement = StatementType{query, std::forward<Args>(args)...};
return cass_session_execute(session_, statement);
}
/**
* @brief Synchonous version of the above.
*
* See asyncExecute(std::string_view, Args&&...) const for how this works.
*
* @param query The query to execute
* @param args The arguments to bind for execution
* @return The result or an error
*/
template <typename... Args>
[[maybe_unused]] ResultOrErrorType
execute(std::string_view query, Args&&... args) const
{
return asyncExecute<Args...>(query, std::forward<Args>(args)...).get();
}
/**
* @brief Execute each of the statements asynchronously.
*
* Batched version is not always the right option.
* Especially since it only supports INSERT, UPDATE and DELETE statements.
* This can be used as an alternative when statements need to execute in bulk.
*
* @param statements The statements to execute
* @return A vector of future objects
*/
[[nodiscard]] std::vector<FutureType>
asyncExecuteEach(std::vector<StatementType> const& statements) const;
/**
* @brief Synchonous version of the above.
*
* See @ref asyncExecuteEach(std::vector<StatementType> const&) const for how this works.
*
* @param statements The statements to execute
* @return Possibly an error
*/
[[maybe_unused]] MaybeErrorType
executeEach(std::vector<StatementType> const& statements) const;
/**
* @brief Execute a prepared statement with optional args asynchronously.
*
* @param statement The prepared statement to execute
* @param args The arguments to bind for execution
* @return A future
*/
template <typename... Args>
[[nodiscard]] FutureType
asyncExecute(PreparedStatementType const& statement, Args&&... args) const
{
auto bound = statement.bind<Args...>(std::forward<Args>(args)...);
return cass_session_execute(session_, bound);
}
/**
* @brief Synchonous version of the above.
*
* See asyncExecute(std::vector<StatementType> const&, Args&&...) const for how this works.
*
* @param statement The prepared statement to bind and execute
* @param args The arguments to bind for execution
* @return The result or an error
*/
template <typename... Args>
[[maybe_unused]] ResultOrErrorType
execute(PreparedStatementType const& statement, Args&&... args) const
{
return asyncExecute<Args...>(statement, std::forward<Args>(args)...).get();
}
/**
* @brief Execute one (bound or simple) statements asynchronously.
*
* @param statement The statement to execute
* @return A future
*/
[[nodiscard]] FutureType
asyncExecute(StatementType const& statement) const;
/**
* @brief Execute one (bound or simple) statements asynchronously with a callback.
*
* @param statement The statement to execute
* @param cb The callback to execute when data is ready
* @return A future that holds onto the callback provided
*/
[[nodiscard]] FutureWithCallbackType
asyncExecute(StatementType const& statement, std::function<void(ResultOrErrorType)>&& cb) const;
/**
* @brief Synchonous version of the above.
*
* See @ref asyncExecute(StatementType const&) const for how this works.
*
* @param statement The statement to execute
* @return The result or an error
*/
[[maybe_unused]] ResultOrErrorType
execute(StatementType const& statement) const;
/**
* @brief Execute a batch of (bound or simple) statements asynchronously.
*
* @param statements The statements to execute
* @return A future
*/
[[nodiscard]] FutureType
asyncExecute(std::vector<StatementType> const& statements) const;
/**
* @brief Synchonous version of the above.
*
* See @ref asyncExecute(std::vector<StatementType> const&) const for how this works.
*
* @param statements The statements to execute
* @return Possibly an error
*/
[[maybe_unused]] MaybeErrorType
execute(std::vector<StatementType> const& statements) const;
/**
* @brief Execute a batch of (bound or simple) statements asynchronously with a completion callback.
*
* @param statements The statements to execute
* @param cb The callback to execute when data is ready
* @return A future that holds onto the callback provided
*/
[[nodiscard]] FutureWithCallbackType
asyncExecute(std::vector<StatementType> const& statements, std::function<void(ResultOrErrorType)>&& cb) const;
/**
* @brief Prepare a statement.
*
* @param query
* @return A prepared statement
* @throws std::runtime_error with underlying error description on failure
*/
[[nodiscard]] PreparedStatementType
prepare(std::string_view query) const;
};
/**
* @brief Extracts the results into series of std::tuple<Types...> by creating a simple wrapper with an STL input
* iterator inside.
*
* You can call .begin() and .end() in order to iterate as usual.
* This also means that you can use it in a range-based for or with some algorithms.
*
* @param result The result to iterate
*/
template <typename... Types>
[[nodiscard]] detail::ResultExtractor<Types...>
extract(Handle::ResultType const& result)
{
return {result};
}
} // namespace data::cassandra

662
src/data/cassandra/Schema.h Normal file
View File

@@ -0,0 +1,662 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/Concepts.h>
#include <data/cassandra/Handle.h>
#include <data/cassandra/SettingsProvider.h>
#include <data/cassandra/Types.h>
#include <util/Expected.h>
#include <util/config/Config.h>
#include <util/log/Logger.h>
#include <fmt/compile.h>
namespace data::cassandra {
template <SomeSettingsProvider SettingsProviderType>
[[nodiscard]] std::string inline qualifiedTableName(SettingsProviderType const& provider, std::string_view name)
{
return fmt::format("{}.{}{}", provider.getKeyspace(), provider.getTablePrefix().value_or(""), name);
}
/**
* @brief Manages the DB schema and provides access to prepared statements.
*/
template <SomeSettingsProvider SettingsProviderType>
class Schema
{
util::Logger log_{"Backend"};
std::reference_wrapper<SettingsProviderType const> settingsProvider_;
public:
explicit Schema(SettingsProviderType const& settingsProvider) : settingsProvider_{std::cref(settingsProvider)}
{
}
std::string createKeyspace = [this]() {
return fmt::format(
R"(
CREATE KEYSPACE IF NOT EXISTS {}
WITH replication = {{
'class': 'SimpleStrategy',
'replication_factor': '{}'
}}
AND durable_writes = true
)",
settingsProvider_.get().getKeyspace(),
settingsProvider_.get().getReplicationFactor());
}();
// =======================
// Schema creation queries
// =======================
std::vector<Statement> createSchema = [this]() {
std::vector<Statement> statements;
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
key blob,
sequence bigint,
object blob,
PRIMARY KEY (key, sequence)
)
WITH CLUSTERING ORDER BY (sequence DESC)
AND default_time_to_live = {}
)",
qualifiedTableName(settingsProvider_.get(), "objects"),
settingsProvider_.get().getTtl()));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
hash blob PRIMARY KEY,
ledger_sequence bigint,
date bigint,
transaction blob,
metadata blob
)
WITH default_time_to_live = {}
)",
qualifiedTableName(settingsProvider_.get(), "transactions"),
settingsProvider_.get().getTtl()));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
ledger_sequence bigint,
hash blob,
PRIMARY KEY (ledger_sequence, hash)
)
WITH default_time_to_live = {}
)",
qualifiedTableName(settingsProvider_.get(), "ledger_transactions"),
settingsProvider_.get().getTtl()));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
key blob,
seq bigint,
next blob,
PRIMARY KEY (key, seq)
)
WITH default_time_to_live = {}
)",
qualifiedTableName(settingsProvider_.get(), "successor"),
settingsProvider_.get().getTtl()));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
seq bigint,
key blob,
PRIMARY KEY (seq, key)
)
WITH default_time_to_live = {}
)",
qualifiedTableName(settingsProvider_.get(), "diff"),
settingsProvider_.get().getTtl()));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
account blob,
seq_idx tuple<bigint, bigint>,
hash blob,
PRIMARY KEY (account, seq_idx)
)
WITH CLUSTERING ORDER BY (seq_idx DESC)
AND default_time_to_live = {}
)",
qualifiedTableName(settingsProvider_.get(), "account_tx"),
settingsProvider_.get().getTtl()));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
sequence bigint PRIMARY KEY,
header blob
)
WITH default_time_to_live = {}
)",
qualifiedTableName(settingsProvider_.get(), "ledgers"),
settingsProvider_.get().getTtl()));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
hash blob PRIMARY KEY,
sequence bigint
)
WITH default_time_to_live = {}
)",
qualifiedTableName(settingsProvider_.get(), "ledger_hashes"),
settingsProvider_.get().getTtl()));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
is_latest boolean PRIMARY KEY,
sequence bigint
)
)",
qualifiedTableName(settingsProvider_.get(), "ledger_range")));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
token_id blob,
sequence bigint,
owner blob,
is_burned boolean,
PRIMARY KEY (token_id, sequence)
)
WITH CLUSTERING ORDER BY (sequence DESC)
AND default_time_to_live = {}
)",
qualifiedTableName(settingsProvider_.get(), "nf_tokens"),
settingsProvider_.get().getTtl()));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
issuer blob,
taxon bigint,
token_id blob,
PRIMARY KEY (issuer, taxon, token_id)
)
WITH CLUSTERING ORDER BY (taxon ASC, token_id ASC)
AND default_time_to_live = {}
)",
qualifiedTableName(settingsProvider_.get(), "issuer_nf_tokens_v2"),
settingsProvider_.get().getTtl()));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
token_id blob,
sequence bigint,
uri blob,
PRIMARY KEY (token_id, sequence)
)
WITH CLUSTERING ORDER BY (sequence DESC)
AND default_time_to_live = {}
)",
qualifiedTableName(settingsProvider_.get(), "nf_token_uris"),
settingsProvider_.get().getTtl()));
statements.emplace_back(fmt::format(
R"(
CREATE TABLE IF NOT EXISTS {}
(
token_id blob,
seq_idx tuple<bigint, bigint>,
hash blob,
PRIMARY KEY (token_id, seq_idx)
)
WITH CLUSTERING ORDER BY (seq_idx DESC)
AND default_time_to_live = {}
)",
qualifiedTableName(settingsProvider_.get(), "nf_token_transactions"),
settingsProvider_.get().getTtl()));
return statements;
}();
/**
* @brief Prepared statements holder.
*/
class Statements
{
std::reference_wrapper<SettingsProviderType const> settingsProvider_;
std::reference_wrapper<Handle const> handle_;
public:
Statements(SettingsProviderType const& settingsProvider, Handle const& handle)
: settingsProvider_{settingsProvider}, handle_{std::cref(handle)}
{
}
//
// Insert queries
//
PreparedStatement insertObject = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(key, sequence, object)
VALUES (?, ?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "objects")));
}();
PreparedStatement insertTransaction = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(hash, ledger_sequence, date, transaction, metadata)
VALUES (?, ?, ?, ?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "transactions")));
}();
PreparedStatement insertLedgerTransaction = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(ledger_sequence, hash)
VALUES (?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "ledger_transactions")));
}();
PreparedStatement insertSuccessor = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(key, seq, next)
VALUES (?, ?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "successor")));
}();
PreparedStatement insertDiff = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(seq, key)
VALUES (?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "diff")));
}();
PreparedStatement insertAccountTx = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(account, seq_idx, hash)
VALUES (?, ?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "account_tx")));
}();
PreparedStatement insertNFT = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(token_id, sequence, owner, is_burned)
VALUES (?, ?, ?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "nf_tokens")));
}();
PreparedStatement insertIssuerNFT = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(issuer, taxon, token_id)
VALUES (?, ?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "issuer_nf_tokens_v2")));
}();
PreparedStatement insertNFTURI = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(token_id, sequence, uri)
VALUES (?, ?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "nf_token_uris")));
}();
PreparedStatement insertNFTTx = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(token_id, seq_idx, hash)
VALUES (?, ?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "nf_token_transactions")));
}();
PreparedStatement insertLedgerHeader = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(sequence, header)
VALUES (?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "ledgers")));
}();
PreparedStatement insertLedgerHash = [this]() {
return handle_.get().prepare(fmt::format(
R"(
INSERT INTO {}
(hash, sequence)
VALUES (?, ?)
)",
qualifiedTableName(settingsProvider_.get(), "ledger_hashes")));
}();
//
// Update (and "delete") queries
//
PreparedStatement updateLedgerRange = [this]() {
return handle_.get().prepare(fmt::format(
R"(
UPDATE {}
SET sequence = ?
WHERE is_latest = ?
IF sequence IN (?, null)
)",
qualifiedTableName(settingsProvider_.get(), "ledger_range")));
}();
PreparedStatement deleteLedgerRange = [this]() {
return handle_.get().prepare(fmt::format(
R"(
UPDATE {}
SET sequence = ?
WHERE is_latest = false
)",
qualifiedTableName(settingsProvider_.get(), "ledger_range")));
}();
//
// Select queries
//
PreparedStatement selectSuccessor = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT next
FROM {}
WHERE key = ?
AND seq <= ?
ORDER BY seq DESC
LIMIT 1
)",
qualifiedTableName(settingsProvider_.get(), "successor")));
}();
PreparedStatement selectDiff = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT key
FROM {}
WHERE seq = ?
)",
qualifiedTableName(settingsProvider_.get(), "diff")));
}();
PreparedStatement selectObject = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT object, sequence
FROM {}
WHERE key = ?
AND sequence <= ?
ORDER BY sequence DESC
LIMIT 1
)",
qualifiedTableName(settingsProvider_.get(), "objects")));
}();
PreparedStatement selectTransaction = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT transaction, metadata, ledger_sequence, date
FROM {}
WHERE hash = ?
)",
qualifiedTableName(settingsProvider_.get(), "transactions")));
}();
PreparedStatement selectAllTransactionHashesInLedger = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT hash
FROM {}
WHERE ledger_sequence = ?
)",
qualifiedTableName(settingsProvider_.get(), "ledger_transactions")));
}();
PreparedStatement selectLedgerPageKeys = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT key
FROM {}
WHERE TOKEN(key) >= ?
AND sequence <= ?
PER PARTITION LIMIT 1
LIMIT ?
ALLOW FILTERING
)",
qualifiedTableName(settingsProvider_.get(), "objects")));
}();
PreparedStatement selectLedgerPage = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT object, key
FROM {}
WHERE TOKEN(key) >= ?
AND sequence <= ?
PER PARTITION LIMIT 1
LIMIT ?
ALLOW FILTERING
)",
qualifiedTableName(settingsProvider_.get(), "objects")));
}();
PreparedStatement getToken = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT TOKEN(key)
FROM {}
WHERE key = ?
LIMIT 1
)",
qualifiedTableName(settingsProvider_.get(), "objects")));
}();
PreparedStatement selectAccountTx = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT hash, seq_idx
FROM {}
WHERE account = ?
AND seq_idx < ?
LIMIT ?
)",
qualifiedTableName(settingsProvider_.get(), "account_tx")));
}();
PreparedStatement selectAccountTxForward = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT hash, seq_idx
FROM {}
WHERE account = ?
AND seq_idx > ?
ORDER BY seq_idx ASC
LIMIT ?
)",
qualifiedTableName(settingsProvider_.get(), "account_tx")));
}();
PreparedStatement selectNFT = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT sequence, owner, is_burned
FROM {}
WHERE token_id = ?
AND sequence <= ?
ORDER BY sequence DESC
LIMIT 1
)",
qualifiedTableName(settingsProvider_.get(), "nf_tokens")));
}();
PreparedStatement selectNFTURI = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT uri
FROM {}
WHERE token_id = ?
AND sequence <= ?
ORDER BY sequence DESC
LIMIT 1
)",
qualifiedTableName(settingsProvider_.get(), "nf_token_uris")));
}();
PreparedStatement selectNFTTx = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT hash, seq_idx
FROM {}
WHERE token_id = ?
AND seq_idx < ?
ORDER BY seq_idx DESC
LIMIT ?
)",
qualifiedTableName(settingsProvider_.get(), "nf_token_transactions")));
}();
PreparedStatement selectNFTTxForward = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT hash, seq_idx
FROM {}
WHERE token_id = ?
AND seq_idx >= ?
ORDER BY seq_idx ASC
LIMIT ?
)",
qualifiedTableName(settingsProvider_.get(), "nf_token_transactions")));
}();
PreparedStatement selectLedgerByHash = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT sequence
FROM {}
WHERE hash = ?
LIMIT 1
)",
qualifiedTableName(settingsProvider_.get(), "ledger_hashes")));
}();
PreparedStatement selectLedgerBySeq = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT header
FROM {}
WHERE sequence = ?
)",
qualifiedTableName(settingsProvider_.get(), "ledgers")));
}();
PreparedStatement selectLatestLedger = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT sequence
FROM {}
WHERE is_latest = true
)",
qualifiedTableName(settingsProvider_.get(), "ledger_range")));
}();
PreparedStatement selectLedgerRange = [this]() {
return handle_.get().prepare(fmt::format(
R"(
SELECT sequence
FROM {}
)",
qualifiedTableName(settingsProvider_.get(), "ledger_range")));
}();
};
/**
* @brief Recreates the prepared statements.
*/
void
prepareStatements(Handle const& handle)
{
LOG(log_.info()) << "Preparing cassandra statements";
statements_ = std::make_unique<Statements>(settingsProvider_, handle);
LOG(log_.info()) << "Finished preparing statements";
}
/**
* @brief Provides access to statements.
*/
std::unique_ptr<Statements> const&
operator->() const
{
return statements_;
}
private:
std::unique_ptr<Statements> statements_{nullptr};
};
} // namespace data::cassandra

View File

@@ -0,0 +1,151 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <data/cassandra/SettingsProvider.h>
#include <data/cassandra/impl/Cluster.h>
#include <data/cassandra/impl/Statement.h>
#include <util/config/Config.h>
#include <boost/json.hpp>
#include <fstream>
#include <string>
#include <thread>
namespace data::cassandra {
namespace detail {
inline Settings::ContactPoints
tag_invoke(boost::json::value_to_tag<Settings::ContactPoints>, boost::json::value const& value)
{
if (not value.is_object())
throw std::runtime_error(
"Feed entire Cassandra section to parse "
"Settings::ContactPoints instead");
util::Config obj{value};
Settings::ContactPoints out;
out.contactPoints = obj.valueOrThrow<std::string>("contact_points", "`contact_points` must be a string");
out.port = obj.maybeValue<uint16_t>("port");
return out;
}
inline Settings::SecureConnectionBundle
tag_invoke(boost::json::value_to_tag<Settings::SecureConnectionBundle>, boost::json::value const& value)
{
if (not value.is_string())
throw std::runtime_error("`secure_connect_bundle` must be a string");
return Settings::SecureConnectionBundle{value.as_string().data()};
}
} // namespace detail
SettingsProvider::SettingsProvider(util::Config const& cfg, uint16_t ttl)
: config_{cfg}
, keyspace_{cfg.valueOr<std::string>("keyspace", "clio")}
, tablePrefix_{cfg.maybeValue<std::string>("table_prefix")}
, replicationFactor_{cfg.valueOr<uint16_t>("replication_factor", 3)}
, ttl_{ttl}
, settings_{parseSettings()}
{
}
Settings
SettingsProvider::getSettings() const
{
return settings_;
}
std::optional<std::string>
SettingsProvider::parseOptionalCertificate() const
{
if (auto const certPath = config_.maybeValue<std::string>("certfile"); certPath)
{
auto const path = std::filesystem::path(*certPath);
std::ifstream fileStream(path.string(), std::ios::in);
if (!fileStream)
{
throw std::system_error(errno, std::generic_category(), "Opening certificate " + path.string());
}
std::string contents(std::istreambuf_iterator<char>{fileStream}, std::istreambuf_iterator<char>{});
if (fileStream.bad())
{
throw std::system_error(errno, std::generic_category(), "Reading certificate " + path.string());
}
return contents;
}
return std::nullopt;
}
Settings
SettingsProvider::parseSettings() const
{
auto settings = Settings::defaultSettings();
if (auto const bundle = config_.maybeValue<Settings::SecureConnectionBundle>("secure_connect_bundle"); bundle)
{
settings.connectionInfo = *bundle;
}
else
{
settings.connectionInfo =
config_.valueOrThrow<Settings::ContactPoints>("Missing contact_points in Cassandra config");
}
settings.threads = config_.valueOr<uint32_t>("threads", settings.threads);
settings.maxWriteRequestsOutstanding =
config_.valueOr<uint32_t>("max_write_requests_outstanding", settings.maxWriteRequestsOutstanding);
settings.maxReadRequestsOutstanding =
config_.valueOr<uint32_t>("max_read_requests_outstanding", settings.maxReadRequestsOutstanding);
settings.maxConnectionsPerHost =
config_.valueOr<uint32_t>("max_connections_per_host", settings.maxConnectionsPerHost);
settings.coreConnectionsPerHost =
config_.valueOr<uint32_t>("core_connections_per_host", settings.coreConnectionsPerHost);
settings.maxConcurrentRequestsThreshold = config_.valueOr<uint32_t>(
"max_concurrent_requests_threshold",
(settings.maxReadRequestsOutstanding + settings.maxWriteRequestsOutstanding) / settings.coreConnectionsPerHost);
settings.queueSizeIO = config_.maybeValue<uint32_t>("queue_size_io");
settings.queueSizeEvent = config_.maybeValue<uint32_t>("queue_size_event");
settings.writeBytesHighWatermark = config_.maybeValue<uint32_t>("write_bytes_high_water_mark");
settings.writeBytesLowWatermark = config_.maybeValue<uint32_t>("write_bytes_low_water_mark");
settings.pendingRequestsHighWatermark = config_.maybeValue<uint32_t>("pending_requests_high_water_mark");
settings.pendingRequestsLowWatermark = config_.maybeValue<uint32_t>("pending_requests_low_water_mark");
settings.maxRequestsPerFlush = config_.maybeValue<uint32_t>("max_requests_per_flush");
settings.maxConcurrentCreation = config_.maybeValue<uint32_t>("max_concurrent_creation");
auto const connectTimeoutSecond = config_.maybeValue<uint32_t>("connect_timeout");
if (connectTimeoutSecond)
settings.connectionTimeout = std::chrono::milliseconds{*connectTimeoutSecond * 1000};
auto const requestTimeoutSecond = config_.maybeValue<uint32_t>("request_timeout");
if (requestTimeoutSecond)
settings.requestTimeout = std::chrono::milliseconds{*requestTimeoutSecond * 1000};
settings.certificate = parseOptionalCertificate();
settings.username = config_.maybeValue<std::string>("username");
settings.password = config_.maybeValue<std::string>("password");
return settings;
}
} // namespace data::cassandra

View File

@@ -0,0 +1,102 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/Handle.h>
#include <data/cassandra/Types.h>
#include <util/Expected.h>
#include <util/config/Config.h>
#include <util/log/Logger.h>
namespace data::cassandra {
/**
* @brief Provides settings for @ref BasicCassandraBackend.
*/
class SettingsProvider
{
util::Config config_;
std::string keyspace_;
std::optional<std::string> tablePrefix_;
uint16_t replicationFactor_;
uint16_t ttl_;
Settings settings_;
public:
/**
* @brief Create a settings provider from the specified config.
*
* @param cfg The config of Clio to use
* @param ttl Time to live setting
*/
explicit SettingsProvider(util::Config const& cfg, uint16_t ttl = 0);
/**
* @return The cluster settings
*/
[[nodiscard]] Settings
getSettings() const;
/**
* @return The specified keyspace
*/
[[nodiscard]] inline std::string
getKeyspace() const
{
return keyspace_;
}
/**
* @return The optional table prefix to use in all queries
*/
[[nodiscard]] inline std::optional<std::string>
getTablePrefix() const
{
return tablePrefix_;
}
/**
* @return The replication factor
*/
[[nodiscard]] inline uint16_t
getReplicationFactor() const
{
return replicationFactor_;
}
/**
* @return The default time to live to use in all `create` queries
*/
[[nodiscard]] inline uint16_t
getTtl() const
{
return ttl_;
}
private:
[[nodiscard]] std::optional<std::string>
parseOptionalCertificate() const;
[[nodiscard]] Settings
parseSettings() const;
};
} // namespace data::cassandra

View File

@@ -0,0 +1,67 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <util/Expected.h>
#include <string>
namespace data::cassandra {
namespace detail {
struct Settings;
class Session;
class Cluster;
struct Future;
class FutureWithCallback;
struct Result;
class Statement;
class PreparedStatement;
struct Batch;
} // namespace detail
using Settings = detail::Settings;
using Future = detail::Future;
using FutureWithCallback = detail::FutureWithCallback;
using Result = detail::Result;
using Statement = detail::Statement;
using PreparedStatement = detail::PreparedStatement;
using Batch = detail::Batch;
/**
* @brief A strong type wrapper for int32_t
*
* This is unfortunately needed right now to support uint32_t properly
* because clio uses bigint (int64) everywhere except for when one need
* to specify LIMIT, which needs an int32 :-/
*/
struct Limit
{
int32_t limit;
};
class Handle;
class CassandraError;
using MaybeError = util::Expected<void, CassandraError>;
using ResultOrError = util::Expected<Result, CassandraError>;
using Error = util::Unexpected<CassandraError>;
} // namespace data::cassandra

View File

@@ -0,0 +1,119 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/Concepts.h>
#include <data/cassandra/Handle.h>
#include <data/cassandra/Types.h>
#include <data/cassandra/impl/RetryPolicy.h>
#include <util/Expected.h>
#include <util/log/Logger.h>
#include <boost/asio.hpp>
#include <functional>
#include <memory>
namespace data::cassandra::detail {
/**
* @brief A query executor with a changable retry policy
*
* Note: this is a bit of an anti-pattern and should be done differently
* eventually.
*
* Currently it's basically a saner implementation of the previous design that
* was used in production without much issue but was using raw new/delete and
* could leak easily. This version is slightly better but the overall design is
* flawed and should be reworked.
*/
template <
typename StatementType,
typename HandleType = Handle,
SomeRetryPolicy RetryPolicyType = ExponentialBackoffRetryPolicy>
class AsyncExecutor : public std::enable_shared_from_this<AsyncExecutor<StatementType, HandleType, RetryPolicyType>>
{
using FutureWithCallbackType = typename HandleType::FutureWithCallbackType;
using CallbackType = std::function<void(typename HandleType::ResultOrErrorType)>;
util::Logger log_{"Backend"};
StatementType data_;
RetryPolicyType retryPolicy_;
CallbackType onComplete_;
// does not exist during initial construction, hence optional
std::optional<FutureWithCallbackType> future_;
std::mutex mtx_;
public:
/**
* @brief Create a new instance of the AsyncExecutor and execute it.
*/
static void
run(boost::asio::io_context& ioc, HandleType const& handle, StatementType&& data, CallbackType&& onComplete)
{
// this is a helper that allows us to use std::make_shared below
struct EnableMakeShared : public AsyncExecutor<StatementType, HandleType, RetryPolicyType>
{
EnableMakeShared(boost::asio::io_context& ioc, StatementType&& data, CallbackType&& onComplete)
: AsyncExecutor(ioc, std::move(data), std::move(onComplete))
{
}
};
auto ptr = std::make_shared<EnableMakeShared>(ioc, std::move(data), std::move(onComplete));
ptr->execute(handle);
}
private:
AsyncExecutor(boost::asio::io_context& ioc, StatementType&& data, CallbackType&& onComplete)
: data_{std::move(data)}, retryPolicy_{ioc}, onComplete_{std::move(onComplete)}
{
}
void
execute(HandleType const& handle)
{
auto self = this->shared_from_this();
// lifetime is extended by capturing self ptr
auto handler = [this, &handle, self](auto&& res) mutable {
if (res)
{
onComplete_(std::move(res));
}
else
{
if (retryPolicy_.shouldRetry(res.error()))
retryPolicy_.retry([self, &handle]() { self->execute(handle); });
else
onComplete_(std::move(res)); // report error
}
self = nullptr; // explicitly decrement refcount
};
std::scoped_lock lck{mtx_};
future_.emplace(handle.asyncExecute(data_, std::move(handler)));
}
};
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,55 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <data/cassandra/Error.h>
#include <data/cassandra/impl/Batch.h>
#include <data/cassandra/impl/Statement.h>
#include <util/Expected.h>
#include <exception>
#include <vector>
namespace {
static constexpr auto batchDeleter = [](CassBatch* ptr) { cass_batch_free(ptr); };
};
namespace data::cassandra::detail {
// TODO: Use an appropriate value instead of CASS_BATCH_TYPE_LOGGED for different use cases
Batch::Batch(std::vector<Statement> const& statements)
: ManagedObject{cass_batch_new(CASS_BATCH_TYPE_LOGGED), batchDeleter}
{
cass_batch_set_is_idempotent(*this, cass_true);
for (auto const& statement : statements)
if (auto const res = add(statement); not res)
throw std::runtime_error("Failed to add statement to batch: " + res.error());
}
MaybeError
Batch::add(Statement const& statement)
{
if (auto const rc = cass_batch_add_statement(*this, statement); rc != CASS_OK)
{
return Error{CassandraError{cass_error_desc(rc), rc}};
}
return {};
}
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,37 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/Types.h>
#include <data/cassandra/impl/ManagedObject.h>
#include <cassandra.h>
namespace data::cassandra::detail {
struct Batch : public ManagedObject<CassBatch>
{
Batch(std::vector<Statement> const& statements);
MaybeError
add(Statement const& statement);
};
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,224 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <data/cassandra/impl/Cluster.h>
#include <data/cassandra/impl/SslContext.h>
#include <data/cassandra/impl/Statement.h>
#include <util/Expected.h>
#include <fmt/core.h>
#include <exception>
#include <vector>
namespace {
static constexpr auto clusterDeleter = [](CassCluster* ptr) { cass_cluster_free(ptr); };
template <class... Ts>
struct overloadSet : Ts...
{
using Ts::operator()...;
};
// explicit deduction guide (not needed as of C++20, but clang be clang)
template <class... Ts>
overloadSet(Ts...) -> overloadSet<Ts...>;
}; // namespace
namespace data::cassandra::detail {
Cluster::Cluster(Settings const& settings) : ManagedObject{cass_cluster_new(), clusterDeleter}
{
using std::to_string;
cass_cluster_set_token_aware_routing(*this, cass_true);
if (auto const rc = cass_cluster_set_protocol_version(*this, CASS_PROTOCOL_VERSION_V4); rc != CASS_OK)
{
throw std::runtime_error(
fmt::format("Error setting cassandra protocol version to v4: {}", cass_error_desc(rc)));
}
if (auto const rc = cass_cluster_set_num_threads_io(*this, settings.threads); rc != CASS_OK)
{
throw std::runtime_error(
fmt::format("Error setting cassandra io threads to {}: {}", settings.threads, cass_error_desc(rc)));
}
cass_log_set_level(settings.enableLog ? CASS_LOG_TRACE : CASS_LOG_DISABLED);
cass_cluster_set_connect_timeout(*this, settings.connectionTimeout.count());
cass_cluster_set_request_timeout(*this, settings.requestTimeout.count());
if (auto const rc =
cass_cluster_set_max_concurrent_requests_threshold(*this, settings.maxConcurrentRequestsThreshold);
rc != CASS_OK)
{
throw std::runtime_error(
fmt::format("Could not set max concurrent requests per host threshold: {}", cass_error_desc(rc)));
}
if (auto const rc = cass_cluster_set_max_connections_per_host(*this, settings.maxConnectionsPerHost); rc != CASS_OK)
{
throw std::runtime_error(fmt::format("Could not set max connections per host: {}", cass_error_desc(rc)));
}
if (auto const rc = cass_cluster_set_core_connections_per_host(*this, settings.coreConnectionsPerHost);
rc != CASS_OK)
{
throw std::runtime_error(fmt::format("Could not set core connections per host: {}", cass_error_desc(rc)));
}
auto const queueSize =
settings.queueSizeIO.value_or(settings.maxWriteRequestsOutstanding + settings.maxReadRequestsOutstanding);
if (auto const rc = cass_cluster_set_queue_size_io(*this, queueSize); rc != CASS_OK)
{
throw std::runtime_error(fmt::format("Could not set queue size for IO per host: {}", cass_error_desc(rc)));
}
auto apply = []<typename ValueType, typename Fn>(
std::optional<ValueType> const& maybeValue, Fn&& fn) requires std::is_object_v<Fn>
{
if (maybeValue)
std::invoke(fn, maybeValue.value());
};
apply(settings.queueSizeEvent, [this](auto value) {
if (auto const rc = cass_cluster_set_queue_size_event(*this, value); rc != CASS_OK)
throw std::runtime_error(
fmt::format("Could not set queue size for events per host: {}", cass_error_desc(rc)));
});
apply(settings.writeBytesHighWatermark, [this](auto value) {
if (auto const rc = cass_cluster_set_write_bytes_high_water_mark(*this, value); rc != CASS_OK)
throw std::runtime_error(fmt::format("Could not set write bytes high water_mark: {}", cass_error_desc(rc)));
});
apply(settings.writeBytesLowWatermark, [this](auto value) {
if (auto const rc = cass_cluster_set_write_bytes_low_water_mark(*this, value); rc != CASS_OK)
throw std::runtime_error(fmt::format("Could not set write bytes low water mark: {}", cass_error_desc(rc)));
});
apply(settings.pendingRequestsHighWatermark, [this](auto value) {
if (auto const rc = cass_cluster_set_pending_requests_high_water_mark(*this, value); rc != CASS_OK)
throw std::runtime_error(
fmt::format("Could not set pending requests high water mark: {}", cass_error_desc(rc)));
});
apply(settings.pendingRequestsLowWatermark, [this](auto value) {
if (auto const rc = cass_cluster_set_pending_requests_low_water_mark(*this, value); rc != CASS_OK)
throw std::runtime_error(
fmt::format("Could not set pending requests low water mark: {}", cass_error_desc(rc)));
});
apply(settings.maxRequestsPerFlush, [this](auto value) {
if (auto const rc = cass_cluster_set_max_requests_per_flush(*this, value); rc != CASS_OK)
throw std::runtime_error(fmt::format("Could not set max requests per flush: {}", cass_error_desc(rc)));
});
apply(settings.maxConcurrentCreation, [this](auto value) {
if (auto const rc = cass_cluster_set_max_concurrent_creation(*this, value); rc != CASS_OK)
throw std::runtime_error(fmt::format("Could not set max concurrent creation: {}", cass_error_desc(rc)));
});
setupConnection(settings);
setupCertificate(settings);
setupCredentials(settings);
auto valueOrDefault = []<typename T>(std::optional<T> const& maybeValue) -> std::string {
return maybeValue ? to_string(*maybeValue) : "default";
};
LOG(log_.info()) << "Threads: " << settings.threads;
LOG(log_.info()) << "Max concurrent requests per host: " << settings.maxConcurrentRequestsThreshold;
LOG(log_.info()) << "Max connections per host: " << settings.maxConnectionsPerHost;
LOG(log_.info()) << "Core connections per host: " << settings.coreConnectionsPerHost;
LOG(log_.info()) << "IO queue size: " << queueSize;
LOG(log_.info()) << "Event queue size: " << valueOrDefault(settings.queueSizeEvent);
LOG(log_.info()) << "Write bytes high watermark: " << valueOrDefault(settings.writeBytesHighWatermark);
LOG(log_.info()) << "Write bytes low watermark: " << valueOrDefault(settings.writeBytesLowWatermark);
LOG(log_.info()) << "Pending requests high watermark: " << valueOrDefault(settings.pendingRequestsHighWatermark);
LOG(log_.info()) << "Pending requests low watermark: " << valueOrDefault(settings.pendingRequestsLowWatermark);
LOG(log_.info()) << "Max requests per flush: " << valueOrDefault(settings.maxRequestsPerFlush);
LOG(log_.info()) << "Max concurrent creation: " << valueOrDefault(settings.maxConcurrentCreation);
}
void
Cluster::setupConnection(Settings const& settings)
{
std::visit(
overloadSet{
[this](Settings::ContactPoints const& points) { setupContactPoints(points); },
[this](Settings::SecureConnectionBundle const& bundle) { setupSecureBundle(bundle); }},
settings.connectionInfo);
}
void
Cluster::setupContactPoints(Settings::ContactPoints const& points)
{
using std::to_string;
auto throwErrorIfNeeded = [](CassError rc, std::string const& label, std::string const& value) {
if (rc != CASS_OK)
throw std::runtime_error(
fmt::format("Cassandra: Error setting {} [{}]: {}", label, value, cass_error_desc(rc)));
};
{
LOG(log_.debug()) << "Attempt connection using contact points: " << points.contactPoints;
auto const rc = cass_cluster_set_contact_points(*this, points.contactPoints.data());
throwErrorIfNeeded(rc, "contact_points", points.contactPoints);
}
if (points.port)
{
auto const rc = cass_cluster_set_port(*this, points.port.value());
throwErrorIfNeeded(rc, "port", to_string(points.port.value()));
}
}
void
Cluster::setupSecureBundle(Settings::SecureConnectionBundle const& bundle)
{
LOG(log_.debug()) << "Attempt connection using secure bundle";
if (auto const rc = cass_cluster_set_cloud_secure_connection_bundle(*this, bundle.bundle.data()); rc != CASS_OK)
{
throw std::runtime_error("Failed to connect using secure connection bundle " + bundle.bundle);
}
}
void
Cluster::setupCertificate(Settings const& settings)
{
if (not settings.certificate)
return;
LOG(log_.debug()) << "Configure SSL context";
SslContext context = SslContext(*settings.certificate);
cass_cluster_set_ssl(*this, context);
}
void
Cluster::setupCredentials(Settings const& settings)
{
if (not settings.username || not settings.password)
return;
LOG(log_.debug()) << "Set credentials; username: " << settings.username.value();
cass_cluster_set_credentials(*this, settings.username.value().c_str(), settings.password.value().c_str());
}
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,169 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/impl/ManagedObject.h>
#include <util/log/Logger.h>
#include <cassandra.h>
#include <chrono>
#include <optional>
#include <string>
#include <string_view>
#include <thread>
#include <variant>
namespace data::cassandra::detail {
// TODO: move Settings to public interface, not detail
/**
* @brief Bundles all cassandra settings in one place.
*/
struct Settings
{
/**
* @brief Represents the configuration of contact points for cassandra.
*/
struct ContactPoints
{
std::string contactPoints = "127.0.0.1"; // defaults to localhost
std::optional<uint16_t> port;
};
/**
* @brief Represents the configuration of a secure connection bundle.
*/
struct SecureConnectionBundle
{
std::string bundle; // no meaningful default
};
/** @brief Enables or disables cassandra driver logger */
bool enableLog = false;
/** @brief Connect timeout specified in milliseconds */
std::chrono::milliseconds connectionTimeout = std::chrono::milliseconds{10000};
/** @brief Request timeout specified in milliseconds */
std::chrono::milliseconds requestTimeout = std::chrono::milliseconds{0}; // no timeout at all
/** @brief Connection information; either ContactPoints or SecureConnectionBundle */
std::variant<ContactPoints, SecureConnectionBundle> connectionInfo = ContactPoints{};
/** @brief The number of threads for the driver to pool */
uint32_t threads = std::thread::hardware_concurrency();
/** @brief The maximum number of outstanding write requests at any given moment */
uint32_t maxWriteRequestsOutstanding = 10'000;
/** @brief The maximum number of outstanding read requests at any given moment */
uint32_t maxReadRequestsOutstanding = 100'000;
/** @brief The maximum number of connections per host */
uint32_t maxConnectionsPerHost = 2u;
/** @brief The number of connection per host to always have active */
uint32_t coreConnectionsPerHost = 2u;
/** @brief The maximum concurrent requests per connection; new connections will be created when reached */
uint32_t maxConcurrentRequestsThreshold =
(maxWriteRequestsOutstanding + maxReadRequestsOutstanding) / coreConnectionsPerHost;
/** @brief Size of the event queue */
std::optional<uint32_t> queueSizeEvent;
/** @brief Size of the IO queue */
std::optional<uint32_t> queueSizeIO;
/** @brief High watermark for bytes written */
std::optional<uint32_t> writeBytesHighWatermark;
/** @brief Low watermark for bytes written */
std::optional<uint32_t> writeBytesLowWatermark;
/** @brief High watermark for pending requests */
std::optional<uint32_t> pendingRequestsHighWatermark;
/** @brief Low watermark for pending requests */
std::optional<uint32_t> pendingRequestsLowWatermark;
/** @brief Maximum number of requests per flush */
std::optional<uint32_t> maxRequestsPerFlush;
/** @brief Maximum number of connections that will be created concurrently */
std::optional<uint32_t> maxConcurrentCreation;
/** @brief SSL certificate */
std::optional<std::string> certificate; // ssl context
/** @brief Username/login */
std::optional<std::string> username;
/** @brief Password to match the `username` */
std::optional<std::string> password;
/**
* @brief Creates a new Settings object as a copy of the current one with overridden contact points.
*/
Settings
withContactPoints(std::string_view contactPoints)
{
auto tmp = *this;
tmp.connectionInfo = ContactPoints{std::string{contactPoints}};
return tmp;
}
/**
* @brief Returns the default settings.
*/
static Settings
defaultSettings()
{
return Settings();
}
};
class Cluster : public ManagedObject<CassCluster>
{
util::Logger log_{"Backend"};
public:
Cluster(Settings const& settings);
private:
void
setupConnection(Settings const& settings);
void
setupContactPoints(Settings::ContactPoints const& points);
void
setupSecureBundle(Settings::SecureConnectionBundle const& bundle);
void
setupCertificate(Settings const& settings);
void
setupCredentials(Settings const& settings);
};
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,459 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/Handle.h>
#include <data/cassandra/Types.h>
#include <data/cassandra/impl/AsyncExecutor.h>
#include <util/Expected.h>
#include <util/log/Logger.h>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <atomic>
#include <condition_variable>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <thread>
namespace data::cassandra::detail {
// TODO: this could probably be also moved out of detail and into the main cassandra namespace.
/**
* @brief Implements async and sync querying against the cassandra DB with support for throttling.
*
* Note: A lot of the code that uses yield is repeated below.
* This is ok for now because we are hopefully going to be getting rid of it entirely later on.
*/
template <typename HandleType = Handle>
class DefaultExecutionStrategy
{
util::Logger log_{"Backend"};
std::uint32_t maxWriteRequestsOutstanding_;
std::atomic_uint32_t numWriteRequestsOutstanding_ = 0;
std::uint32_t maxReadRequestsOutstanding_;
std::atomic_uint32_t numReadRequestsOutstanding_ = 0;
std::mutex throttleMutex_;
std::condition_variable throttleCv_;
std::mutex syncMutex_;
std::condition_variable syncCv_;
boost::asio::io_context ioc_;
std::optional<boost::asio::io_service::work> work_;
std::reference_wrapper<HandleType const> handle_;
std::thread thread_;
public:
using ResultOrErrorType = typename HandleType::ResultOrErrorType;
using StatementType = typename HandleType::StatementType;
using PreparedStatementType = typename HandleType::PreparedStatementType;
using FutureType = typename HandleType::FutureType;
using FutureWithCallbackType = typename HandleType::FutureWithCallbackType;
using ResultType = typename HandleType::ResultType;
using CompletionTokenType = boost::asio::yield_context;
/**
* @param settings The settings to use
* @param handle A handle to the cassandra database
*/
DefaultExecutionStrategy(Settings const& settings, HandleType const& handle)
: maxWriteRequestsOutstanding_{settings.maxWriteRequestsOutstanding}
, maxReadRequestsOutstanding_{settings.maxReadRequestsOutstanding}
, work_{ioc_}
, handle_{std::cref(handle)}
, thread_{[this]() { ioc_.run(); }}
{
LOG(log_.info()) << "Max write requests outstanding is " << maxWriteRequestsOutstanding_
<< "; Max read requests outstanding is " << maxReadRequestsOutstanding_;
}
~DefaultExecutionStrategy()
{
work_.reset();
ioc_.stop();
thread_.join();
}
/**
* @brief Wait for all async writes to finish before unblocking.
*/
void
sync()
{
LOG(log_.debug()) << "Waiting to sync all writes...";
std::unique_lock<std::mutex> lck(syncMutex_);
syncCv_.wait(lck, [this]() { return finishedAllWriteRequests(); });
LOG(log_.debug()) << "Sync done.";
}
/**
* @return true if outstanding read requests allowance is exhausted; false otherwise
*/
bool
isTooBusy() const
{
return numReadRequestsOutstanding_ >= maxReadRequestsOutstanding_;
}
/**
* @brief Blocking query execution used for writing data.
*
* Retries forever sleeping for 5 milliseconds between attempts.
*/
ResultOrErrorType
writeSync(StatementType const& statement)
{
while (true)
{
if (auto res = handle_.get().execute(statement); res)
{
return res;
}
else
{
LOG(log_.warn()) << "Cassandra sync write error, retrying: " << res.error();
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
}
/**
* @brief Blocking query execution used for writing data.
*
* Retries forever sleeping for 5 milliseconds between attempts.
*/
template <typename... Args>
ResultOrErrorType
writeSync(PreparedStatementType const& preparedStatement, Args&&... args)
{
return writeSync(preparedStatement.bind(std::forward<Args>(args)...));
}
/**
* @brief Non-blocking query execution used for writing data.
*
* Retries forever with retry policy specified by @ref AsyncExecutor
*
* @param preparedStatement Statement to prepare and execute
* @param args Args to bind to the prepared statement
* @throw DatabaseTimeout on timeout
*/
template <typename... Args>
void
write(PreparedStatementType const& preparedStatement, Args&&... args)
{
auto statement = preparedStatement.bind(std::forward<Args>(args)...);
incrementOutstandingRequestCount();
// Note: lifetime is controlled by std::shared_from_this internally
AsyncExecutor<std::decay_t<decltype(statement)>, HandleType>::run(
ioc_, handle_, std::move(statement), [this](auto const&) { decrementOutstandingRequestCount(); });
}
/**
* @brief Non-blocking batched query execution used for writing data.
*
* Retries forever with retry policy specified by @ref AsyncExecutor.
*
* @param statements Vector of statements to execute as a batch
* @throw DatabaseTimeout on timeout
*/
void
write(std::vector<StatementType>&& statements)
{
if (statements.empty())
return;
incrementOutstandingRequestCount();
// Note: lifetime is controlled by std::shared_from_this internally
AsyncExecutor<std::decay_t<decltype(statements)>, HandleType>::run(
ioc_, handle_, std::move(statements), [this](auto const&) { decrementOutstandingRequestCount(); });
}
/**
* @brief Coroutine-based query execution used for reading data.
*
* Retries forever until successful or throws an exception on timeout.
*
* @param token Completion token (yield_context)
* @param preparedStatement Statement to prepare and execute
* @param args Args to bind to the prepared statement
* @throw DatabaseTimeout on timeout
* @return ResultType or error wrapped in Expected
*/
template <typename... Args>
[[maybe_unused]] ResultOrErrorType
read(CompletionTokenType token, PreparedStatementType const& preparedStatement, Args&&... args)
{
return read(token, preparedStatement.bind(std::forward<Args>(args)...));
}
/**
* @brief Coroutine-based query execution used for reading data.
*
* Retries forever until successful or throws an exception on timeout.
*
* @param token Completion token (yield_context)
* @param statements Statements to execute in a batch
* @throw DatabaseTimeout on timeout
* @return ResultType or error wrapped in Expected
*/
[[maybe_unused]] ResultOrErrorType
read(CompletionTokenType token, std::vector<StatementType> const& statements)
{
auto const numStatements = statements.size();
std::optional<FutureWithCallbackType> future;
// todo: perhaps use policy instead
while (true)
{
numReadRequestsOutstanding_ += numStatements;
// TODO: see if we can avoid using shared_ptr for self here
auto init = [this, &statements, &future]<typename Self>(Self& self) {
future.emplace(handle_.get().asyncExecute(
statements, [sself = std::make_shared<Self>(std::move(self))](auto&& res) mutable {
// Note: explicit work below needed on linux/gcc11
auto executor = boost::asio::get_associated_executor(*sself);
boost::asio::post(
executor,
[sself = std::move(sself),
res = std::move(res),
_ = boost::asio::make_work_guard(executor)]() mutable {
sself->complete(std::move(res));
sself.reset();
});
}));
};
auto res = boost::asio::async_compose<CompletionTokenType, void(ResultOrErrorType)>(
init, token, boost::asio::get_associated_executor(token));
numReadRequestsOutstanding_ -= numStatements;
if (res)
{
return res;
}
else
{
LOG(log_.error()) << "Failed batch read in coroutine: " << res.error();
throwErrorIfNeeded(res.error());
}
}
}
/**
* @brief Coroutine-based query execution used for reading data.
*
* Retries forever until successful or throws an exception on timeout.
*
* @param token Completion token (yield_context)
* @param statement Statement to execute
* @throw DatabaseTimeout on timeout
* @return ResultType or error wrapped in Expected
*/
[[maybe_unused]] ResultOrErrorType
read(CompletionTokenType token, StatementType const& statement)
{
std::optional<FutureWithCallbackType> future;
// todo: perhaps use policy instead
while (true)
{
++numReadRequestsOutstanding_;
// TODO: see if we can avoid using shared_ptr for self here
auto init = [this, &statement, &future]<typename Self>(Self& self) {
future.emplace(handle_.get().asyncExecute(
statement, [sself = std::make_shared<Self>(std::move(self))](auto&&) mutable {
// Note: explicit work below needed on linux/gcc11
auto executor = boost::asio::get_associated_executor(*sself);
boost::asio::post(
executor, [sself = std::move(sself), _ = boost::asio::make_work_guard(executor)]() mutable {
sself->complete();
sself.reset();
});
}));
};
boost::asio::async_compose<CompletionTokenType, void()>(
init, token, boost::asio::get_associated_executor(token));
--numReadRequestsOutstanding_;
if (auto res = future->get(); res)
{
return res;
}
else
{
LOG(log_.error()) << "Failed read in coroutine: " << res.error();
throwErrorIfNeeded(res.error());
}
}
}
/**
* @brief Coroutine-based query execution used for reading data.
*
* Attempts to execute each statement. On any error the whole vector will be
* discarded and exception will be thrown.
*
* @param token Completion token (yield_context)
* @param statements Statements to execute
* @throw DatabaseTimeout on db error
* @return Vector of results
*/
std::vector<ResultType>
readEach(CompletionTokenType token, std::vector<StatementType> const& statements)
{
std::atomic_bool hadError = false;
std::atomic_int numOutstanding = statements.size();
numReadRequestsOutstanding_ += statements.size();
auto futures = std::vector<FutureWithCallbackType>{};
futures.reserve(numOutstanding);
auto init = [this, &statements, &futures, &hadError, &numOutstanding]<typename Self>(Self& self) {
auto sself = std::make_shared<Self>(std::move(self)); // TODO: see if we can avoid this
auto executionHandler = [&hadError, &numOutstanding, sself = std::move(sself)](auto const& res) mutable {
if (not res)
hadError = true;
// when all async operations complete unblock the result
if (--numOutstanding == 0)
{
// Note: explicit work below needed on linux/gcc11
auto executor = boost::asio::get_associated_executor(*sself);
boost::asio::post(
executor, [sself = std::move(sself), _ = boost::asio::make_work_guard(executor)]() mutable {
sself->complete();
sself.reset();
});
}
};
std::transform(
std::cbegin(statements),
std::cend(statements),
std::back_inserter(futures),
[this, &executionHandler](auto const& statement) {
return handle_.get().asyncExecute(statement, executionHandler);
});
};
boost::asio::async_compose<CompletionTokenType, void()>(
init, token, boost::asio::get_associated_executor(token));
numReadRequestsOutstanding_ -= statements.size();
if (hadError)
throw DatabaseTimeout{};
std::vector<ResultType> results;
results.reserve(futures.size());
// it's safe to call blocking get on futures here as we already waited for the coroutine to resume above.
std::transform(
std::make_move_iterator(std::begin(futures)),
std::make_move_iterator(std::end(futures)),
std::back_inserter(results),
[](auto&& future) {
auto entry = future.get();
auto&& res = entry.value();
return std::move(res);
});
assert(futures.size() == statements.size());
assert(results.size() == statements.size());
return results;
}
private:
void
incrementOutstandingRequestCount()
{
{
std::unique_lock<std::mutex> lck(throttleMutex_);
if (!canAddWriteRequest())
{
LOG(log_.trace()) << "Max outstanding requests reached. "
<< "Waiting for other requests to finish";
throttleCv_.wait(lck, [this]() { return canAddWriteRequest(); });
}
}
++numWriteRequestsOutstanding_;
}
void
decrementOutstandingRequestCount()
{
// sanity check
if (numWriteRequestsOutstanding_ == 0)
{
assert(false);
throw std::runtime_error("decrementing num outstanding below 0");
}
size_t cur = (--numWriteRequestsOutstanding_);
{
// mutex lock required to prevent race condition around spurious
// wakeup
std::lock_guard lck(throttleMutex_);
throttleCv_.notify_one();
}
if (cur == 0)
{
// mutex lock required to prevent race condition around spurious
// wakeup
std::lock_guard lck(syncMutex_);
syncCv_.notify_one();
}
}
bool
canAddWriteRequest() const
{
return numWriteRequestsOutstanding_ < maxWriteRequestsOutstanding_;
}
bool
finishedAllWriteRequests() const
{
return numWriteRequestsOutstanding_ == 0;
}
void
throwErrorIfNeeded(CassandraError err) const
{
if (err.isTimeout())
throw DatabaseTimeout();
if (err.isInvalidQuery())
throw std::runtime_error("Invalid query");
}
};
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,102 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <data/cassandra/Error.h>
#include <data/cassandra/impl/Future.h>
#include <data/cassandra/impl/Result.h>
#include <exception>
#include <vector>
namespace {
static constexpr auto futureDeleter = [](CassFuture* ptr) { cass_future_free(ptr); };
} // namespace
namespace data::cassandra::detail {
/* implicit */ Future::Future(CassFuture* ptr) : ManagedObject{ptr, futureDeleter}
{
}
MaybeError
Future::await() const
{
if (auto const rc = cass_future_error_code(*this); rc)
{
auto errMsg = [this](std::string const& label) {
char const* message;
std::size_t len;
cass_future_error_message(*this, &message, &len);
return label + ": " + std::string{message, len};
}(cass_error_desc(rc));
return Error{CassandraError{errMsg, rc}};
}
return {};
}
ResultOrError
Future::get() const
{
if (auto const rc = cass_future_error_code(*this); rc)
{
auto const errMsg = [this](std::string const& label) {
char const* message;
std::size_t len;
cass_future_error_message(*this, &message, &len);
return label + ": " + std::string{message, len};
}("future::get()");
return Error{CassandraError{errMsg, rc}};
}
else
{
return Result{cass_future_get_result(*this)};
}
}
void
invokeHelper(CassFuture* ptr, void* cbPtr)
{
// Note: can't use Future{ptr}.get() because double free will occur :/
auto* cb = static_cast<FutureWithCallback::FnType*>(cbPtr);
if (auto const rc = cass_future_error_code(ptr); rc)
{
auto const errMsg = [&ptr](std::string const& label) {
char const* message;
std::size_t len;
cass_future_error_message(ptr, &message, &len);
return label + ": " + std::string{message, len};
}("invokeHelper");
(*cb)(Error{CassandraError{errMsg, rc}});
}
else
{
(*cb)(Result{cass_future_get_result(ptr)});
}
}
/* implicit */ FutureWithCallback::FutureWithCallback(CassFuture* ptr, FnType&& cb)
: Future{ptr}, cb_{std::make_unique<FnType>(std::move(cb))}
{
// Instead of passing `this` as the userdata void*, we pass the address of
// the callback itself which will survive std::move of the
// FutureWithCallback parent. Not ideal but I have no better solution atm.
cass_future_set_callback(*this, &invokeHelper, cb_.get());
}
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,58 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/Types.h>
#include <data/cassandra/impl/ManagedObject.h>
#include <cassandra.h>
namespace data::cassandra::detail {
struct Future : public ManagedObject<CassFuture>
{
/* implicit */ Future(CassFuture* ptr);
MaybeError
await() const;
ResultOrError
get() const;
};
void
invokeHelper(CassFuture* ptr, void* self);
class FutureWithCallback : public Future
{
public:
using FnType = std::function<void(ResultOrError)>;
using FnPtrType = std::unique_ptr<FnType>;
/* implicit */ FutureWithCallback(CassFuture* ptr, FnType&& cb);
FutureWithCallback(FutureWithCallback const&) = delete;
FutureWithCallback(FutureWithCallback&&) = default;
private:
/** Wrapped in a unique_ptr so it can survive std::move :/ */
FnPtrType cb_;
};
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,47 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <memory>
namespace data::cassandra::detail {
template <typename Managed>
class ManagedObject
{
protected:
std::unique_ptr<Managed, void (*)(Managed*)> ptr_;
public:
template <typename deleterCallable>
ManagedObject(Managed* rawPtr, deleterCallable deleter) : ptr_{rawPtr, deleter}
{
if (rawPtr == nullptr)
throw std::runtime_error("Could not create DB object - got nullptr");
}
ManagedObject(ManagedObject&&) = default;
operator Managed* const() const
{
return ptr_.get();
}
};
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,69 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <data/cassandra/impl/Result.h>
namespace {
static constexpr auto resultDeleter = [](CassResult const* ptr) { cass_result_free(ptr); };
static constexpr auto resultIteratorDeleter = [](CassIterator* ptr) { cass_iterator_free(ptr); };
} // namespace
namespace data::cassandra::detail {
/* implicit */ Result::Result(CassResult const* ptr) : ManagedObject{ptr, resultDeleter}
{
}
[[nodiscard]] std::size_t
Result::numRows() const
{
return cass_result_row_count(*this);
}
[[nodiscard]] bool
Result::hasRows() const
{
return numRows() > 0;
}
/* implicit */ ResultIterator::ResultIterator(CassIterator* ptr)
: ManagedObject{ptr, resultIteratorDeleter}, hasMore_{cass_iterator_next(ptr)}
{
}
[[nodiscard]] ResultIterator
ResultIterator::fromResult(Result const& result)
{
return {cass_iterator_from_result(result)};
}
[[maybe_unused]] bool
ResultIterator::moveForward()
{
hasMore_ = cass_iterator_next(*this);
return hasMore_;
}
[[nodiscard]] bool
ResultIterator::hasMore() const
{
return hasMore_;
}
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,257 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/impl/ManagedObject.h>
#include <data/cassandra/impl/Tuple.h>
#include <util/Expected.h>
#include <ripple/basics/base_uint.h>
#include <ripple/protocol/AccountID.h>
#include <cassandra.h>
#include <compare>
#include <iterator>
#include <tuple>
namespace data::cassandra::detail {
template <typename>
static constexpr bool unsupported_v = false;
template <typename Type>
inline Type
extractColumn(CassRow const* row, std::size_t idx)
{
using std::to_string;
Type output;
auto throwErrorIfNeeded = [](CassError rc, std::string_view label) {
if (rc != CASS_OK)
{
auto const tag = '[' + std::string{label} + ']';
throw std::logic_error(tag + ": " + cass_error_desc(rc));
}
};
using DecayedType = std::decay_t<Type>;
using UintTupleType = std::tuple<uint32_t, uint32_t>;
using UCharVectorType = std::vector<unsigned char>;
if constexpr (std::is_same_v<DecayedType, ripple::uint256>)
{
cass_byte_t const* buf;
std::size_t bufSize;
auto const rc = cass_value_get_bytes(cass_row_get_column(row, idx), &buf, &bufSize);
throwErrorIfNeeded(rc, "Extract ripple::uint256");
output = ripple::uint256::fromVoid(buf);
}
else if constexpr (std::is_same_v<DecayedType, ripple::AccountID>)
{
cass_byte_t const* buf;
std::size_t bufSize;
auto const rc = cass_value_get_bytes(cass_row_get_column(row, idx), &buf, &bufSize);
throwErrorIfNeeded(rc, "Extract ripple::AccountID");
output = ripple::AccountID::fromVoid(buf);
}
else if constexpr (std::is_same_v<DecayedType, UCharVectorType>)
{
cass_byte_t const* buf;
std::size_t bufSize;
auto const rc = cass_value_get_bytes(cass_row_get_column(row, idx), &buf, &bufSize);
throwErrorIfNeeded(rc, "Extract vector<unsigned char>");
output = UCharVectorType{buf, buf + bufSize};
}
else if constexpr (std::is_same_v<DecayedType, UintTupleType>)
{
auto const* tuple = cass_row_get_column(row, idx);
output = TupleIterator::fromTuple(tuple).extract<uint32_t, uint32_t>();
}
else if constexpr (std::is_convertible_v<DecayedType, std::string>)
{
char const* value;
std::size_t len;
auto const rc = cass_value_get_string(cass_row_get_column(row, idx), &value, &len);
throwErrorIfNeeded(rc, "Extract string");
output = std::string{value, len};
}
else if constexpr (std::is_same_v<DecayedType, bool>)
{
cass_bool_t flag;
auto const rc = cass_value_get_bool(cass_row_get_column(row, idx), &flag);
throwErrorIfNeeded(rc, "Extract bool");
output = flag ? true : false;
}
// clio only uses bigint (int64_t) so we convert any incoming type
else if constexpr (std::is_convertible_v<DecayedType, int64_t>)
{
int64_t out;
auto const rc = cass_value_get_int64(cass_row_get_column(row, idx), &out);
throwErrorIfNeeded(rc, "Extract int64");
output = static_cast<DecayedType>(out);
}
else
{
// type not supported for extraction
static_assert(unsupported_v<DecayedType>);
}
return output;
}
struct Result : public ManagedObject<CassResult const>
{
/* implicit */ Result(CassResult const* ptr);
[[nodiscard]] std::size_t
numRows() const;
[[nodiscard]] bool
hasRows() const;
template <typename... RowTypes>
std::optional<std::tuple<RowTypes...>>
get() const requires(std::tuple_size<std::tuple<RowTypes...>>{} > 1)
{
// row managed internally by cassandra driver, hence no ManagedObject.
auto const* row = cass_result_first_row(*this);
if (row == nullptr)
return std::nullopt;
std::size_t idx = 0;
auto advanceId = [&idx]() { return idx++; };
return std::make_optional<std::tuple<RowTypes...>>({extractColumn<RowTypes>(row, advanceId())...});
}
template <typename RowType>
std::optional<RowType>
get() const
{
// row managed internally by cassandra driver, hence no ManagedObject.
auto const* row = cass_result_first_row(*this);
if (row == nullptr)
return std::nullopt;
return std::make_optional<RowType>(extractColumn<RowType>(row, 0));
}
};
class ResultIterator : public ManagedObject<CassIterator>
{
bool hasMore_ = false;
public:
/* implicit */ ResultIterator(CassIterator* ptr);
[[nodiscard]] static ResultIterator
fromResult(Result const& result);
[[maybe_unused]] bool
moveForward();
[[nodiscard]] bool
hasMore() const;
template <typename... RowTypes>
std::tuple<RowTypes...>
extractCurrentRow() const
{
// note: row is invalidated on each iteration.
// managed internally by cassandra driver, hence no ManagedObject.
auto const* row = cass_iterator_get_row(*this);
std::size_t idx = 0;
auto advanceId = [&idx]() { return idx++; };
return {extractColumn<RowTypes>(row, advanceId())...};
}
};
template <typename... Types>
class ResultExtractor
{
std::reference_wrapper<Result const> ref_;
public:
struct Sentinel
{
};
struct Iterator
{
using iterator_category = std::input_iterator_tag;
using difference_type = std::size_t; // rows count
using value_type = std::tuple<Types...>;
/* implicit */ Iterator(ResultIterator iterator) : iterator_{std::move(iterator)}
{
}
Iterator(Iterator const&) = delete;
Iterator&
operator=(Iterator const&) = delete;
value_type
operator*() const
{
return iterator_.extractCurrentRow<Types...>();
}
value_type
operator->()
{
return iterator_.extractCurrentRow<Types...>();
}
Iterator&
operator++()
{
iterator_.moveForward();
return *this;
}
bool
operator==(Sentinel const&) const
{
return not iterator_.hasMore();
}
private:
ResultIterator iterator_;
};
ResultExtractor(Result const& result) : ref_{std::cref(result)}
{
}
Iterator
begin()
{
return ResultIterator::fromResult(ref_);
}
Sentinel
end()
{
return {};
}
};
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,94 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/Handle.h>
#include <data/cassandra/Types.h>
#include <util/Expected.h>
#include <util/log/Logger.h>
#include <boost/asio.hpp>
#include <algorithm>
#include <chrono>
#include <cmath>
namespace data::cassandra::detail {
/**
* @brief A retry policy that employs exponential backoff
*/
class ExponentialBackoffRetryPolicy
{
util::Logger log_{"Backend"};
boost::asio::steady_timer timer_;
uint32_t attempt_ = 0u;
public:
/**
* @brief Create a new retry policy instance with the io_context provided
*/
ExponentialBackoffRetryPolicy(boost::asio::io_context& ioc) : timer_{boost::asio::make_strand(ioc)}
{
}
/**
* @brief Computes next retry delay and returns true unconditionally
*
* @param err The cassandra error that triggered the retry
*/
[[nodiscard]] bool
shouldRetry([[maybe_unused]] CassandraError err)
{
auto const delay = calculateDelay(attempt_);
LOG(log_.error()) << "Cassandra write error: " << err << ", current retries " << attempt_ << ", retrying in "
<< delay.count() << " milliseconds";
return true; // keep retrying forever
}
/**
* @brief Schedules next retry
*
* @param fn The callable to execute
*/
template <typename Fn>
void
retry(Fn&& fn)
{
timer_.expires_after(calculateDelay(attempt_++));
timer_.async_wait([fn = std::forward<Fn>(fn)]([[maybe_unused]] const auto& err) {
// todo: deal with cancellation (thru err)
fn();
});
}
/**
* @brief Calculates the wait time before attempting another retry
*/
std::chrono::milliseconds
calculateDelay(uint32_t attempt)
{
return std::chrono::milliseconds{lround(std::pow(2, std::min(10u, attempt)))};
}
};
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,38 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/impl/ManagedObject.h>
#include <cassandra.h>
namespace data::cassandra::detail {
class Session : public ManagedObject<CassSession>
{
static constexpr auto deleter = [](CassSession* ptr) { cass_session_free(ptr); };
public:
Session() : ManagedObject{cass_session_new(), deleter}
{
}
};
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,37 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <data/cassandra/impl/SslContext.h>
namespace {
static constexpr auto contextDeleter = [](CassSsl* ptr) { cass_ssl_free(ptr); };
} // namespace
namespace data::cassandra::detail {
SslContext::SslContext(std::string const& certificate) : ManagedObject{cass_ssl_new(), contextDeleter}
{
cass_ssl_set_verify_flags(*this, CASS_SSL_VERIFY_NONE);
if (auto const rc = cass_ssl_add_trusted_cert(*this, certificate.c_str()); rc != CASS_OK)
{
throw std::runtime_error(std::string{"Error setting Cassandra SSL Context: "} + cass_error_desc(rc));
}
}
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,35 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/impl/ManagedObject.h>
#include <cassandra.h>
#include <string>
namespace data::cassandra::detail {
struct SslContext : public ManagedObject<CassSsl>
{
explicit SslContext(std::string const& certificate);
};
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,186 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/Types.h>
#include <data/cassandra/impl/ManagedObject.h>
#include <data/cassandra/impl/Tuple.h>
#include <util/Expected.h>
#include <ripple/basics/base_uint.h>
#include <ripple/protocol/STAccount.h>
#include <cassandra.h>
#include <fmt/core.h>
#include <chrono>
#include <compare>
#include <iterator>
namespace data::cassandra::detail {
class Statement : public ManagedObject<CassStatement>
{
static constexpr auto deleter = [](CassStatement* ptr) { cass_statement_free(ptr); };
template <typename>
static constexpr bool unsupported_v = false;
public:
/**
* @brief Construct a new statement with optionally provided arguments.
*
* Note: it's up to the user to make sure the bound parameters match
* the format of the query (e.g. amount of '?' matches count of args).
*/
template <typename... Args>
explicit Statement(std::string_view query, Args&&... args)
: ManagedObject{cass_statement_new(query.data(), sizeof...(args)), deleter}
{
cass_statement_set_consistency(*this, CASS_CONSISTENCY_QUORUM);
cass_statement_set_is_idempotent(*this, cass_true);
bind<Args...>(std::forward<Args>(args)...);
}
/* implicit */ Statement(CassStatement* ptr) : ManagedObject{ptr, deleter}
{
cass_statement_set_consistency(*this, CASS_CONSISTENCY_QUORUM);
cass_statement_set_is_idempotent(*this, cass_true);
}
Statement(Statement&&) = default;
/**
* @brief Binds the given arguments to the statement.
*
* @param args Arguments to bind
*/
template <typename... Args>
void
bind(Args&&... args) const
{
std::size_t idx = 0;
(this->bindAt<Args>(idx++, std::forward<Args>(args)), ...);
}
/**
* @brief Binds an argument to a specific index.
*
* @param idx The index of the argument
* @param value The value to bind it to
*/
template <typename Type>
void
bindAt(std::size_t const idx, Type&& value) const
{
using std::to_string;
auto throwErrorIfNeeded = [idx](CassError rc, std::string_view label) {
if (rc != CASS_OK)
throw std::logic_error(fmt::format("[{}] at idx {}: {}", label, idx, cass_error_desc(rc)));
};
auto bindBytes = [this, idx](auto const* data, size_t size) {
return cass_statement_bind_bytes(*this, idx, static_cast<cass_byte_t const*>(data), size);
};
using DecayedType = std::decay_t<Type>;
using UCharVectorType = std::vector<unsigned char>;
using UintTupleType = std::tuple<uint32_t, uint32_t>;
if constexpr (std::is_same_v<DecayedType, ripple::uint256>)
{
auto const rc = bindBytes(value.data(), value.size());
throwErrorIfNeeded(rc, "Bind ripple::uint256");
}
else if constexpr (std::is_same_v<DecayedType, ripple::AccountID>)
{
auto const rc = bindBytes(value.data(), value.size());
throwErrorIfNeeded(rc, "Bind ripple::AccountID");
}
else if constexpr (std::is_same_v<DecayedType, UCharVectorType>)
{
auto const rc = bindBytes(value.data(), value.size());
throwErrorIfNeeded(rc, "Bind vector<unsigned char>");
}
else if constexpr (std::is_convertible_v<DecayedType, std::string>)
{
// reinterpret_cast is needed here :'(
auto const rc = bindBytes(reinterpret_cast<unsigned char const*>(value.data()), value.size());
throwErrorIfNeeded(rc, "Bind string (as bytes)");
}
else if constexpr (std::is_same_v<DecayedType, UintTupleType>)
{
auto const rc = cass_statement_bind_tuple(*this, idx, Tuple{std::move(value)});
throwErrorIfNeeded(rc, "Bind tuple<uint32, uint32>");
}
else if constexpr (std::is_same_v<DecayedType, bool>)
{
auto const rc = cass_statement_bind_bool(*this, idx, value ? cass_true : cass_false);
throwErrorIfNeeded(rc, "Bind bool");
}
else if constexpr (std::is_same_v<DecayedType, Limit>)
{
auto const rc = cass_statement_bind_int32(*this, idx, value.limit);
throwErrorIfNeeded(rc, "Bind limit (int32)");
}
// clio only uses bigint (int64_t) so we convert any incoming type
else if constexpr (std::is_convertible_v<DecayedType, int64_t>)
{
auto const rc = cass_statement_bind_int64(*this, idx, value);
throwErrorIfNeeded(rc, "Bind int64");
}
else
{
// type not supported for binding
static_assert(unsupported_v<DecayedType>);
}
}
};
/**
* @brief Represents a prepared statement on the DB side.
*
* This is used to produce Statement objects that can be executed.
*/
class PreparedStatement : public ManagedObject<CassPrepared const>
{
static constexpr auto deleter = [](CassPrepared const* ptr) { cass_prepared_free(ptr); };
public:
/* implicit */ PreparedStatement(CassPrepared const* ptr) : ManagedObject{ptr, deleter}
{
}
/**
* @brief Bind the given arguments and produce a ready to execute Statement.
*
* @param args The arguments to bind
* @return A bound and ready to execute Statement object
*/
template <typename... Args>
Statement
bind(Args&&... args) const
{
Statement statement = cass_prepared_bind(*this);
statement.bind<Args...>(std::forward<Args>(args)...);
return statement;
}
};
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,43 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <data/cassandra/impl/Tuple.h>
namespace {
static constexpr auto tupleDeleter = [](CassTuple* ptr) { cass_tuple_free(ptr); };
static constexpr auto tupleIteratorDeleter = [](CassIterator* ptr) { cass_iterator_free(ptr); };
} // namespace
namespace data::cassandra::detail {
/* implicit */ Tuple::Tuple(CassTuple* ptr) : ManagedObject{ptr, tupleDeleter}
{
}
/* implicit */ TupleIterator::TupleIterator(CassIterator* ptr) : ManagedObject{ptr, tupleIteratorDeleter}
{
}
[[nodiscard]] TupleIterator
TupleIterator::fromTuple(CassValue const* value)
{
return {cass_iterator_from_tuple(value)};
}
} // namespace data::cassandra::detail

View File

@@ -0,0 +1,149 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <data/cassandra/impl/ManagedObject.h>
#include <cassandra.h>
#include <functional>
#include <string>
#include <string_view>
#include <tuple>
namespace data::cassandra::detail {
class Tuple : public ManagedObject<CassTuple>
{
static constexpr auto deleter = [](CassTuple* ptr) { cass_tuple_free(ptr); };
template <typename>
static constexpr bool unsupported_v = false;
public:
/* implicit */ Tuple(CassTuple* ptr);
template <typename... Types>
explicit Tuple(std::tuple<Types...>&& value)
: ManagedObject{cass_tuple_new(std::tuple_size<std::tuple<Types...>>{}), deleter}
{
std::apply(std::bind_front(&Tuple::bind<Types...>, this), std::move(value));
}
template <typename... Args>
void
bind(Args&&... args) const
{
std::size_t idx = 0;
(this->bindAt<Args>(idx++, std::forward<Args>(args)), ...);
}
template <typename Type>
void
bindAt(std::size_t const idx, Type&& value) const
{
using std::to_string;
auto throwErrorIfNeeded = [idx](CassError rc, std::string_view label) {
if (rc != CASS_OK)
{
auto const tag = '[' + std::string{label} + ']';
throw std::logic_error(tag + " at idx " + to_string(idx) + ": " + cass_error_desc(rc));
}
};
using DecayedType = std::decay_t<Type>;
if constexpr (std::is_same_v<DecayedType, bool>)
{
auto const rc = cass_tuple_set_bool(*this, idx, value ? cass_true : cass_false);
throwErrorIfNeeded(rc, "Bind bool");
}
// clio only uses bigint (int64_t) so we convert any incoming type
else if constexpr (std::is_convertible_v<DecayedType, int64_t>)
{
auto const rc = cass_tuple_set_int64(*this, idx, value);
throwErrorIfNeeded(rc, "Bind int64");
}
else
{
// type not supported for binding
static_assert(unsupported_v<DecayedType>);
}
}
};
class TupleIterator : public ManagedObject<CassIterator>
{
template <typename>
static constexpr bool unsupported_v = false;
public:
/* implicit */ TupleIterator(CassIterator* ptr);
[[nodiscard]] static TupleIterator
fromTuple(CassValue const* value);
template <typename... Types>
[[nodiscard]] std::tuple<Types...>
extract() const
{
return {extractNext<Types>()...};
}
private:
template <typename Type>
Type
extractNext() const
{
using std::to_string;
Type output;
if (not cass_iterator_next(*this))
throw std::logic_error("Could not extract next value from tuple iterator");
auto throwErrorIfNeeded = [](CassError rc, std::string_view label) {
if (rc != CASS_OK)
{
auto const tag = '[' + std::string{label} + ']';
throw std::logic_error(tag + ": " + cass_error_desc(rc));
}
};
using DecayedType = std::decay_t<Type>;
// clio only uses bigint (int64_t) so we convert any incoming type
if constexpr (std::is_convertible_v<DecayedType, int64_t>)
{
int64_t out;
auto const rc = cass_value_get_int64(cass_iterator_get_value(*this), &out);
throwErrorIfNeeded(rc, "Extract int64 from tuple");
output = static_cast<DecayedType>(out);
}
else
{
// type not supported for extraction
static_assert(unsupported_v<DecayedType>);
}
return output;
}
};
} // namespace data::cassandra::detail

Some files were not shown because too many files have changed in this diff Show More