diff --git a/.github/agents/committer.agent.md b/.github/agents/committer.agent.md index a8ef84b04..eca557373 100644 --- a/.github/agents/committer.agent.md +++ b/.github/agents/committer.agent.md @@ -41,6 +41,19 @@ Treat every commit request as a review-and-verify workflow, not as a blind reque - Do not mix skill/workflow documentation changes with implementation changes — always create separate commits. +## Splitting Commits + +When the requested work spans multiple logical commits and `project-words.txt` has been +modified with new entries that belong to different commits, do not try to split the +dictionary additions across those commits. Instead: + +1. Commit all `project-words.txt` changes first as a single `chore(cspell): add ` + commit (or fold them into the first logical commit when that is more natural). +2. Then create the remaining focused commits for the actual implementation/docs changes. + +This keeps the spell-check linter green at every commit and keeps the substantive commits +focused on their real intent rather than on dictionary churn. + ## Output Format When handling a commit task, respond in this order: diff --git a/.github/skills/dev/git-workflow/open-pull-request/SKILL.md b/.github/skills/dev/git-workflow/open-pull-request/SKILL.md index eca0fae3b..04074a383 100644 --- a/.github/skills/dev/git-workflow/open-pull-request/SKILL.md +++ b/.github/skills/dev/git-workflow/open-pull-request/SKILL.md @@ -18,10 +18,14 @@ metadata: Before opening a PR: - [ ] Working tree is clean (`git status`) +- [ ] Upstream target repository confirmed from workspace metadata (`Cargo.toml` → `repository`) - [ ] Branch is pushed to your fork remote - [ ] Commits are GPG signed (`git log --show-signature -n 1`) - [ ] All pre-commit checks passed (`linter all`, `cargo machete`, tests) +> Important: always open the PR in the **upstream repository**, not in your fork. +> Resolve upstream from `Cargo.toml` (`repository = "https://github.com/torrust/torrust-tracker"`) and use that value for `gh pr create --repo ...`. + ## Title and Description Convention PR title: use Conventional Commit style, include issue reference. @@ -42,13 +46,20 @@ PR body must include: ```bash gh pr create \ - --repo torrust/torrust-tracker \ + --repo / \ --base develop \ --head : \ --title "" \ --body "<body>" ``` +Example upstream resolution from `Cargo.toml`: + +```bash +UPSTREAM_REPO=$(grep '^repository\s*=\s*"https://github.com/' Cargo.toml | sed -E 's#.*github.com/([^\"]+).*#\1#') +gh pr create --repo "$UPSTREAM_REPO" --base develop --head <fork-owner>:<branch-name> --title "<title>" --body "<body>" +``` + If successful, `gh` prints the PR URL. ## Option B: GitHub MCP Tools diff --git a/Cargo.lock b/Cargo.lock index e4dc3041e..c6ed71ced 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,17 +17,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -182,12 +171,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - [[package]] name = "astral-tokio-tar" version = "0.6.0" @@ -374,6 +357,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic" version = "0.6.1" @@ -576,17 +568,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bigdecimal" -version = "0.4.10" +name = "base64ct" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" -dependencies = [ - "autocfg", - "libm", - "num-bigint", - "num-integer", - "num-traits", -] +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "binascii" @@ -594,24 +579,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.117", -] - [[package]] name = "bit-vec" version = "0.4.4" @@ -623,6 +590,9 @@ name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] [[package]] name = "bittorrent-http-tracker-core" @@ -714,18 +684,17 @@ version = "3.0.0-develop" dependencies = [ "anyhow", "aquatic_udp_protocol", + "async-trait", "bittorrent-primitives", "chrono", "clap", "derive_more", "local-ip-address", "mockall", - "r2d2", - "r2d2_mysql", - "r2d2_sqlite", "rand 0.10.1", "serde", "serde_json", + "sqlx", "testcontainers", "thiserror 2.0.18", "tokio", @@ -783,18 +752,6 @@ dependencies = [ "torrust-tracker-primitives", ] -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -919,30 +876,6 @@ dependencies = [ "time", ] -[[package]] -name = "borsh" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" -dependencies = [ - "borsh-derive", - "bytes", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "brotli" version = "8.0.2" @@ -964,49 +897,12 @@ dependencies = [ "alloc-stdlib", ] -[[package]] -name = "btoi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad" -dependencies = [ - "num-traits", -] - -[[package]] -name = "bufstream" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" - [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "bytemuck" version = "1.25.0" @@ -1067,15 +963,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -1148,17 +1035,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.6.1" @@ -1272,6 +1148,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-oid" version = "0.10.2" @@ -1331,6 +1213,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.0" @@ -1414,28 +1311,6 @@ dependencies = [ "itertools 0.13.0", ] -[[package]] -name = "crossbeam" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1597,6 +1472,17 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -1661,17 +1547,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "derive_utils" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "362f47930db19fe7735f527e6595e4900316b893ebf6d48ad3d31be928d57dd6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "diff" version = "0.1.13" @@ -1685,7 +1560,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid 0.9.6", "crypto-common 0.1.7", + "subtle", ] [[package]] @@ -1695,7 +1572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ "block-buffer 0.12.0", - "const-oid", + "const-oid 0.10.2", "crypto-common 0.2.1", "ctutils", ] @@ -1722,6 +1599,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast" version = "0.11.0" @@ -1745,6 +1628,9 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "encoding_rs" @@ -1791,6 +1677,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "etcetera" version = "0.11.0" @@ -1828,18 +1725,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - [[package]] name = "fastrand" version = "2.4.1" @@ -1897,10 +1782,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "libz-sys", "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1913,12 +1808,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "foreign-types" version = "0.3.2" @@ -1972,62 +1861,6 @@ dependencies = [ "futures-core", ] -[[package]] -name = "frunk" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28aef0f9aa070bce60767c12ba9cb41efeaf1a2bc6427f87b7d83f11239a16d7" -dependencies = [ - "frunk_core", - "frunk_derives", - "frunk_proc_macros", - "serde", -] - -[[package]] -name = "frunk_core" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "476eeaa382e3462b84da5d6ba3da97b5786823c2d0d3a0d04ef088d073da225c" -dependencies = [ - "serde", -] - -[[package]] -name = "frunk_derives" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b4095fc99e1d858e5b8c7125d2638372ec85aa0fe6c807105cf10b0265ca6c" -dependencies = [ - "frunk_proc_macro_helpers", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "frunk_proc_macro_helpers" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1952b802269f2db12ab7c0bd328d0ae8feaabf19f352a7b0af7bb0c5693abfce" -dependencies = [ - "frunk_core", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "frunk_proc_macros" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3462f590fa236005bd7ca4847f81438bd6fe0febd4d04e11968d4c2e96437e78" -dependencies = [ - "frunk_core", - "frunk_proc_macro_helpers", - "quote", - "syn 2.0.117", -] - [[package]] name = "fs-err" version = "3.3.0" @@ -2044,12 +1877,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures" version = "0.3.32" @@ -2092,6 +1919,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -2279,9 +2117,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] [[package]] name = "hashbrown" @@ -2297,16 +2132,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -2317,11 +2143,11 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "hashlink" -version = "0.11.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.16.1", + "hashbrown 0.15.5", ] [[package]] @@ -2349,17 +2175,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" [[package]] -name = "hmac" -version = "0.13.0" +name = "hkdf" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "digest 0.11.2", + "hmac 0.12.1", ] [[package]] -name = "home" -version = "0.5.12" +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", +] + +[[package]] +name = "home" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ @@ -2502,7 +2346,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2", "system-configuration", "tokio", "tower-service", @@ -2702,15 +2546,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "io-enum" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de9008599afe8527a8c9d70423437363b321649161e98473f433de802d76107" -dependencies = [ - "derive_utils", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -2863,6 +2698,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -2876,16 +2714,6 @@ version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - [[package]] name = "libm" version = "0.2.16" @@ -2906,20 +2734,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.37.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libz-sys" -version = "1.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -2967,15 +2784,6 @@ dependencies = [ "value-bag", ] -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "lru-slab" version = "0.1.2" @@ -2988,6 +2796,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.8.0" @@ -3040,12 +2858,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3102,97 +2914,6 @@ dependencies = [ "serde", ] -[[package]] -name = "mysql" -version = "25.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ad644efb545e459029b1ffa7c969d830975bd76906820913247620df10050b" -dependencies = [ - "bufstream", - "bytes", - "crossbeam", - "flate2", - "io-enum", - "libc", - "lru", - "mysql_common", - "named_pipe", - "native-tls", - "pem", - "percent-encoding", - "serde", - "serde_json", - "socket2 0.5.10", - "twox-hash", - "url", -] - -[[package]] -name = "mysql-common-derive" -version = "0.31.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c3512cf11487168e0e9db7157801bf5273be13055a9cc95356dc9e0035e49c" -dependencies = [ - "darling 0.20.11", - "heck", - "num-bigint", - "proc-macro-crate", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", - "termcolor", - "thiserror 1.0.69", -] - -[[package]] -name = "mysql_common" -version = "0.32.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478b0ff3f7d67b79da2b96f56f334431aef65e15ba4b29dd74a4236e29582bdc" -dependencies = [ - "base64 0.21.7", - "bigdecimal", - "bindgen", - "bitflags", - "bitvec", - "btoi", - "byteorder", - "bytes", - "cc", - "cmake", - "crc32fast", - "flate2", - "frunk", - "lazy_static", - "mysql-common-derive", - "num-bigint", - "num-traits", - "rand 0.8.6", - "regex", - "rust_decimal", - "saturating", - "serde", - "serde_json", - "sha1 0.10.6", - "sha2 0.10.9", - "smallvec", - "subprocess", - "thiserror 1.0.69", - "time", - "uuid", - "zstd", -] - -[[package]] -name = "named_pipe" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9c443cce91fc3e12f017290db75dde490d685cdaaf508d7159d7cf41f0eb2b" -dependencies = [ - "winapi", -] - [[package]] name = "native-tls" version = "0.2.18" @@ -3239,16 +2960,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nonempty" version = "0.7.0" @@ -3288,6 +2999,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -3341,6 +3068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3491,7 +3219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ "digest 0.11.2", - "hmac", + "hmac 0.13.0", ] [[package]] @@ -3518,13 +3246,12 @@ dependencies = [ ] [[package]] -name = "pem" -version = "3.0.6" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ - "base64 0.22.1", - "serde_core", + "base64ct", ] [[package]] @@ -3614,6 +3341,27 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -3838,26 +3586,6 @@ dependencies = [ "prost", ] -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "quickcheck" version = "1.1.0" @@ -3882,7 +3610,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.3", + "socket2", "thiserror 2.0.18", "tokio", "tracing", @@ -3920,7 +3648,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -3946,44 +3674,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "r2d2" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" -dependencies = [ - "log", - "parking_lot", - "scheduled-thread-pool", -] - -[[package]] -name = "r2d2_mysql" -version = "25.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93963fe09ca35b0311d089439e944e42a6cb39bf8ea323782ddb31240ba2ae87" -dependencies = [ - "mysql", - "r2d2", -] - -[[package]] -name = "r2d2_sqlite" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5576df16239e4e422c4835c8ed00be806d4491855c7847dba60b7aa8408b469b" -dependencies = [ - "r2d2", - "rusqlite", - "uuid", -] - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.6" @@ -4153,15 +3843,6 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - [[package]] name = "reqwest" version = "0.13.2" @@ -4231,42 +3912,23 @@ dependencies = [ ] [[package]] -name = "rkyv" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rsqlite-vfs" -version = "0.1.0" +name = "rsa" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "hashbrown 0.16.1", - "thiserror 2.0.18", + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", ] [[package]] @@ -4328,38 +3990,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "rusqlite" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", - "sqlite-wasm-rs", -] - -[[package]] -name = "rust_decimal" -version = "1.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand 0.8.6", - "rkyv", - "serde", - "serde_json", - "wasm-bindgen", -] - [[package]] name = "rustc-demangle" version = "0.1.27" @@ -4492,12 +4122,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "saturating" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" - [[package]] name = "schannel" version = "0.1.29" @@ -4507,15 +4131,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "scheduled-thread-pool" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" -dependencies = [ - "parking_lot", -] - [[package]] name = "schemars" version = "0.9.0" @@ -4546,12 +4161,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - [[package]] name = "security-framework" version = "3.7.0" @@ -4801,75 +4410,268 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "signal-hook-registry" -version = "1.4.8" +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ - "errno", "libc", + "windows-sys 0.61.2", ] [[package]] -name = "simd-adler32" -version = "0.3.9" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] -name = "simdutf8" -version = "0.1.5" +name = "spki" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] [[package]] -name = "siphasher" -version = "1.0.2" +name = "sqlx" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] [[package]] -name = "slab" -version = "0.4.12" +name = "sqlx-core" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener 5.4.1", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "native-tls", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", +] [[package]] -name = "smallvec" -version = "1.15.1" +name = "sqlx-macros" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.117", +] [[package]] -name = "socket2" -version = "0.5.10" +name = "sqlx-macros-core" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ - "libc", - "windows-sys 0.52.0", + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.117", + "tokio", + "url", ] [[package]] -name = "socket2" -version = "0.6.3" +name = "sqlx-mysql" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ - "libc", - "windows-sys 0.61.2", + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac 0.12.1", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.6", + "rsa", + "serde", + "sha1 0.10.6", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", ] [[package]] -name = "sqlite-wasm-rs" -version = "0.5.3" +name = "sqlx-postgres" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ - "cc", - "js-sys", - "rsqlite-vfs", - "wasm-bindgen", + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera 0.8.0", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac 0.12.1", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", ] [[package]] @@ -4884,6 +4686,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -4913,16 +4726,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "subprocess" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c56e8662b206b9892d7a5a3f2ecdbcb455d3d6b259111373b7e08b8055158a8" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "subtle" version = "2.6.1" @@ -5013,12 +4816,6 @@ dependencies = [ "libc", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "tdyne-peer-id" version = "1.0.2" @@ -5049,15 +4846,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" version = "0.4.4" @@ -5086,7 +4874,7 @@ dependencies = [ "bytes", "docker_credential", "either", - "etcetera", + "etcetera 0.11.0", "ferroid", "futures", "http", @@ -5241,7 +5029,7 @@ dependencies = [ "mio", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -5411,7 +5199,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2 0.6.3", + "socket2", "sync_wrapper", "tokio", "tokio-stream", @@ -5985,17 +5773,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "twox-hash" -version = "1.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" -dependencies = [ - "cfg-if", - "rand 0.8.6", - "static_assertions", -] - [[package]] name = "typenum" version = "1.20.0" @@ -6017,6 +5794,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -6029,6 +5812,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.13.2" @@ -6125,7 +5923,6 @@ checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", - "rand 0.10.1", "wasm-bindgen", ] @@ -6196,6 +5993,12 @@ dependencies = [ "wit-bindgen 0.51.0", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.118" @@ -6205,7 +6008,6 @@ dependencies = [ "cfg-if", "once_cell", "rustversion", - "serde", "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -6315,6 +6117,16 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -6425,6 +6237,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -6467,6 +6288,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -6506,6 +6342,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6524,6 +6366,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6542,6 +6390,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6572,6 +6426,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6590,6 +6450,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6608,6 +6474,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6626,6 +6498,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6756,15 +6634,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "xattr" version = "1.6.1" diff --git a/docs/issues/1525-05-migrate-sqlite-and-mysql-to-sqlx.md b/docs/issues/1525-05-migrate-sqlite-and-mysql-to-sqlx.md deleted file mode 100644 index 079866502..000000000 --- a/docs/issues/1525-05-migrate-sqlite-and-mysql-to-sqlx.md +++ /dev/null @@ -1,280 +0,0 @@ -# Subissue Draft for #1525-05: Migrate SQLite and MySQL Drivers to sqlx - -## Goal - -Move the existing SQL backends to a shared async `sqlx` substrate before adding PostgreSQL. - -## Why - -PostgreSQL should not be added as a special case. The existing SQL backends need to follow the same -async persistence model first so PostgreSQL can land on a common foundation. - -## Proposed Branch - -- `1525-05-migrate-sqlite-and-mysql-to-sqlx` - -## Background - -### Starting point - -By the time this subissue is implemented, subissue `1525-04` will have split the monolithic -`Database` trait into four narrow sync traits (`SchemaMigrator`, `TorrentMetricsStore`, -`WhitelistStore`, `AuthKeyStore`) plus a `Database` aggregate supertrait with a blanket impl. -Consumers still hold `Arc<Box<dyn Database>>`. - -The existing drivers (`Sqlite` in `driver/sqlite.rs`, `Mysql` in `driver/mysql.rs`) use -synchronous connection pools (`r2d2_sqlite`/`r2d2` for SQLite, the `mysql` crate for MySQL). -`build()` in `driver/mod.rs` calls `create_database_tables()` eagerly on startup. - -### Migration strategy: green parallel → single switch commit - -Rewriting both drivers at once while simultaneously making all four traits async would keep the -branch in a broken ("red") state for an extended period. Instead, this subissue uses a -**green parallel approach**: - -1. Build the async infrastructure and new driver implementations alongside the existing sync code - (Tasks 1–3). The branch compiles and all tests pass throughout these tasks. -2. Wire everything up and remove the old code in a single focused switch commit (Task 4). The - branch is briefly in a red state only during this commit. - -The technique is to put the async traits and new drivers in a temporary `databases/sqlx/` -submodule during Tasks 1–3. Task 4 moves them into place, updates consumers, and removes the sync -code. - -### What changes in the drivers - -The current drivers use blocking I/O and create the schema eagerly on construction. The new -`sqlx`-backed drivers: - -- Use `SqlitePool` / `MySqlPool` with lazy `connect_lazy_with()`. -- Manage the schema with raw `sqlx::query()` DDL statements (`CREATE TABLE IF NOT EXISTS ...`), - exactly mirroring what the current sync drivers do. `sqlx::migrate!()` and migration files are - **not** introduced here — that is subissue `1525-06`. -- Run `create_database_tables()` lazily the first time any operation is called, protected by an - `AtomicBool` + `Mutex` double-checked latch (`ensure_schema()`). -- All trait methods become `async fn` (via `async_trait`). - -## Tasks - -### Task 1 — Add sqlx infrastructure (no behavior change, stays green) - -Add the async substrate without touching the existing drivers or traits. - -#### Dependencies - -In `packages/tracker-core/Cargo.toml`, add: - -```toml -async-trait = "..." -sqlx = { version = "...", features = ["sqlite", "mysql", "runtime-tokio-native-tls"] } -tokio = { version = "...", features = ["full"] } # if not already present with needed features -``` - -Keep `r2d2`, `r2d2_sqlite`, `rusqlite`, and the `mysql` crate — they are still needed by the old -drivers until Task 4. - -#### Error handling - -Update `databases/error.rs` so that `sqlx::Error` can be converted into the existing `Error` type. -Add the following constructor methods and their corresponding enum variants. Do not add -`Error::migration_error()` — that belongs to `1525-06`: - -- `Error::connection_error()` — wraps connection failures (`sqlx::Error::Io`, pool errors, - etc.). Introduce the `ConnectionError` variant. -- `Error::invalid_query()` — wraps type-decoding and encoding failures. Used by - `decode_info_hash`, `decode_key`, `decode_valid_until`, and counter conversion helpers in - the async drivers. Also used by the `decode_counter`/`encode_counter` helpers introduced in - `1525-07` — introduce the variant here so `1525-07` requires no additional `error.rs` - changes. Introduce the `InvalidQuery` variant. -- `Error::query_returned_no_rows()` — for `sqlx::Error::RowNotFound`. Introduce the - `QueryReturnedNoRows` variant. -- `From<(sqlx::Error, Driver)>` — maps `sqlx::Error` variants to `ConnectionError`, - `QueryReturnedNoRows`, or `InvalidQuery` based on error kind (see reference `error.rs`). - -Do not change existing variants. - -**Outcome**: `cargo test --workspace --all-targets` still passes. No behavior change. - -### Task 2 — Implement async SQLite driver (stays green) - -Create a new async SQLite driver in a parallel `databases/sqlx/` submodule without touching the -existing `databases/driver/sqlite.rs`. - -#### New files - -```text -packages/tracker-core/src/databases/sqlx/mod.rs ← async trait definitions + AsyncDatabase aggregate -packages/tracker-core/src/databases/sqlx/sqlite.rs ← SqliteSqlx struct -``` - -#### Async trait definitions (`databases/sqlx/mod.rs`) - -Define async versions of the four narrow traits. Use `async_trait` for object safety: - -```rust -use async_trait::async_trait; - -#[async_trait] -pub trait AsyncSchemaMigrator: Send + Sync { - async fn create_database_tables(&self) -> Result<(), Error>; - async fn drop_database_tables(&self) -> Result<(), Error>; -} - -// ... AsyncTorrentMetricsStore, AsyncWhitelistStore, AsyncAuthKeyStore (same method -// signatures as their sync counterparts but with async fn) - -pub trait AsyncDatabase: - AsyncSchemaMigrator + AsyncTorrentMetricsStore + AsyncWhitelistStore + AsyncAuthKeyStore -{ -} - -impl<T> AsyncDatabase for T where - T: AsyncSchemaMigrator + AsyncTorrentMetricsStore + AsyncWhitelistStore + AsyncAuthKeyStore -{ -} -``` - -#### `SqliteSqlx` struct (`databases/sqlx/sqlite.rs`) - -Mirrors the reference `Sqlite` in `driver/sqlite.rs` (PR branch): - -```rust -use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; -use sqlx::SqlitePool; -use std::sync::atomic::{AtomicBool, Ordering}; -use tokio::sync::Mutex; - -pub(crate) struct SqliteSqlx { - pool: SqlitePool, - schema_ready: AtomicBool, - schema_lock: Mutex<()>, -} -``` - -Implement `AsyncSchemaMigrator`, `AsyncTorrentMetricsStore`, `AsyncWhitelistStore`, and -`AsyncAuthKeyStore` for `SqliteSqlx`. All SQL queries use `sqlx::query(...)`. Schema -initialization in `create_database_tables()` executes raw `CREATE TABLE IF NOT EXISTS ...` -statements via `sqlx::query()` — no `sqlx::migrate!()` in this step. - -#### Tests - -Add an inline `#[cfg(test)]` module in `databases/sqlx/sqlite.rs`. Use the shared -`databases/driver/tests::run_tests()` helper (or a new async equivalent) to run all behavioral -tests against `SqliteSqlx`. Use `torrust_tracker_test_helpers::configuration::ephemeral_sqlite_database` -for the in-memory/temp-file path. - -**Outcome**: `cargo test --workspace --all-targets` still passes. Old sync `Sqlite` driver -untouched. - -### Task 3 — Implement async MySQL driver (stays green) - -Create `packages/tracker-core/src/databases/sqlx/mysql.rs` with a `MysqlSqlx` struct mirroring -the same structure as `SqliteSqlx` but using `MySqlPool`. Schema initialization uses raw -`sqlx::query()` DDL — no `sqlx::migrate!()` in this step. - -Implement the same four async traits. Add an inline `#[cfg(test)]` module that runs the shared -behavioral test suite against a real MySQL instance (via environment variable guard -`TORRUST_TRACKER_CORE_RUN_MYSQL_DRIVER_TEST=true`, consistent with existing MySQL test gating). - -**Outcome**: `cargo test --workspace --all-targets` still passes. Old sync `Mysql` driver -untouched. - -### Task 4 — Switch: replace sync traits with async, update consumers (brief red) - -This task is a single focused commit. Steps within the commit: - -1. **Rename async traits to canonical names**: rename `AsyncSchemaMigrator` → `SchemaMigrator`, - `AsyncTorrentMetricsStore` → `TorrentMetricsStore`, etc. in `databases/sqlx/mod.rs`. Rename - `AsyncDatabase` → `Database`. Move the trait definitions from `databases/sqlx/mod.rs` into - `databases/mod.rs` (replacing the sync trait definitions). Move the driver files into the - existing driver directory, overwriting the old sync drivers: - `databases/sqlx/sqlite.rs` → `databases/driver/sqlite.rs` and - `databases/sqlx/mysql.rs` → `databases/driver/mysql.rs`. Remove the now-empty - `databases/sqlx/` submodule. - -2. **Rename driver structs**: rename `SqliteSqlx` → `Sqlite`, `MysqlSqlx` → `Mysql`. - -3. **Clean up old driver module helpers**: remove the sync test helpers from - `databases/driver/mod.rs` that reference `Arc<Box<dyn Database>>` with sync methods; replace - with async equivalents. (The old sync driver files at `databases/driver/sqlite.rs` and - `databases/driver/mysql.rs` were already overwritten by the async drivers in step 1.) - -4. **Update `databases/driver/mod.rs` `build()`**: the function no longer calls - `create_database_tables()` eagerly (schema is now lazy). Update the return type if needed. - -5. **Update `databases/setup.rs`**: `initialize_database()` constructs the new async `Sqlite` or - `Mysql` and wraps in `Arc<Box<dyn Database>>` (type stays the same, traits are now async). - -6. **Add `.await` at all consumer call sites**: every location that called a `Database` method - synchronously now needs `.await`. The affected files are: - - `statistics/persisted/downloads.rs` (`DatabaseDownloadsMetricRepository`) - - `whitelist/repository/persisted.rs` (`DatabaseWhitelist`) - - `whitelist/setup.rs` - - `authentication/key/repository/persisted.rs` (`DatabaseKeyRepository`) - - `authentication/handler.rs` (test helpers) - - Any integration tests in `tests/` - -7. **Remove unused dependencies**: remove `r2d2`, `r2d2_sqlite`, `rusqlite`, and the `mysql` crate - from `tracker-core/Cargo.toml`. Run `cargo machete` to verify. - -8. **Update mock usage**: `#[automock]` on the narrow traits generates async mocks via `mockall`. - Note that `MockDatabase` was already removed in `1525-04` (the aggregate supertrait has no - methods). The actual breakage surface in this switch commit is the four narrow-trait mocks: - `MockSchemaMigrator`, `MockTorrentMetricsStore`, `MockWhitelistStore`, and `MockAuthKeyStore`. - Any tests written against the **sync** versions of these mocks (from `1525-04`) will fail to - compile after the switch because async `mockall` mocks use - `.returning(|| Box::pin(async { Ok(()) }))` rather than `.returning(|| Ok(()))`. Find and - update all such tests before declaring this task complete. - -**Outcome**: `cargo test --workspace --all-targets` passes. `linter all` exits `0`. Sync drivers -and all `r2d2`/`rusqlite`/`mysql` dependencies are gone. - -## Constraints - -- Do not add PostgreSQL in this step. -- Do not introduce `sqlx::migrate!()`, migration files, or the `sqlx` `macros` feature — those - are introduced in subissue `1525-06`. -- Do not change the SQL schema in this step (schema evolution is `1525-06`). -- Keep `Arc<Box<dyn Database>>` as the consumer-facing type; do not introduce the `Persistence` - struct from the reference implementation (that is a separate concern). -- The lazy `ensure_schema()` latch must be correct under concurrent async access: use - `AtomicBool` (Acquire/Release) + `Mutex` double-checked pattern as in the reference. - -## Acceptance Criteria - -- [ ] SQLite and MySQL drivers use `sqlx` with async trait methods. -- [ ] Schema initialization is lazy (`ensure_schema()` pattern) — no eager call in `build()`. -- [ ] Schema management uses raw `sqlx::query()` DDL; `sqlx::migrate!()` is not used. -- [ ] `r2d2`, `r2d2_sqlite`, `rusqlite`, and the `mysql` crate are removed from - `tracker-core/Cargo.toml`. -- [ ] Existing behavior is preserved end-to-end. -- [ ] The branch compiles and all tests pass after each of Tasks 1–3 individually (verified by CI - or manual `cargo test` run after each task). -- [ ] Persistence benchmarking (see subissue `1525-03`) shows no regression against the committed - baseline. -- [ ] `cargo test --workspace --all-targets` passes. -- [ ] `linter all` exits with code `0`. -- [ ] `cargo machete` reports no unused dependencies. - -## Out of Scope - -- PostgreSQL driver — that is subissue `1525-08`. -- `sqlx::migrate!()` and migration files — that is subissue `1525-06`. -- `async_trait` removal — the `async_trait` crate is required at MSRV 1.72 because - async-fn-in-traits was stabilized in Rust 1.75. When the MSRV is raised to 1.75+, remove - `async_trait` and replace `#[async_trait]` attribute usage with native async trait syntax. - Track this as a follow-up when the MSRV is next bumped. - -## References - -- EPIC: `#1525` -- Subissue `1525-04`: `docs/issues/1713-1525-04-split-persistence-traits.md` — must be completed first -- Subissue `1525-03`: `docs/issues/1525-03-persistence-benchmarking.md` — benchmark baseline -- Reference PR: `#1695` -- Reference implementation branch: `josecelano:pr-1684-review` — see EPIC for checkout - instructions (`docs/issues/1525-overhaul-persistence.md`) -- Reference files (async driver implementations — note: the reference uses `sqlx::migrate!()` - which is not adopted in this step; use raw DDL instead): - - `packages/tracker-core/src/databases/driver/sqlite.rs` - - `packages/tracker-core/src/databases/driver/mysql.rs` - - `packages/tracker-core/src/databases/driver/mod.rs` diff --git a/docs/issues/1717-1525-05-migrate-sqlite-and-mysql-to-sqlx.md b/docs/issues/1717-1525-05-migrate-sqlite-and-mysql-to-sqlx.md new file mode 100644 index 000000000..c4977cd89 --- /dev/null +++ b/docs/issues/1717-1525-05-migrate-sqlite-and-mysql-to-sqlx.md @@ -0,0 +1,411 @@ +# Subissue Draft for #1525-05: Migrate SQLite and MySQL Drivers to sqlx + +## Goal + +Move the existing SQL backends to a shared async `sqlx` substrate before adding PostgreSQL. + +## Why + +PostgreSQL should not be added as a special case. The existing SQL backends need to follow the same +async persistence model first so PostgreSQL can land on a common foundation. + +## Proposed Branch + +- `1525-05-migrate-sqlite-and-mysql-to-sqlx` + +## Background + +### Starting point + +Subissue `1525-04` has already been merged into `develop` (it is included in this branch). +It split the monolithic `Database` trait into four narrow sync traits (`SchemaMigrator`, +`TorrentMetricsStore`, `WhitelistStore`, `AuthKeyStore`) plus a `Database` aggregate supertrait +with a blanket impl. Consumers still hold `Arc<Box<dyn Database>>`. + +The existing drivers (`Sqlite` in `driver/sqlite.rs`, `Mysql` in `driver/mysql.rs`) use +synchronous connection pools (`r2d2_sqlite`/`r2d2` for SQLite, the `mysql` crate for MySQL). +`build()` in `driver/mod.rs` calls `create_database_tables()` eagerly on startup. + +### Migration strategy: green parallel → single switch commit + +Rewriting both drivers at once while simultaneously making all four traits async would keep the +branch in a broken ("red") state for an extended period. Instead, this subissue uses a +**green parallel approach**: + +1. Build the async infrastructure and new driver implementations alongside the existing sync code + (Tasks 1–3). The branch compiles and all tests pass throughout these tasks. +2. Wire everything up and remove the old code in a single focused switch commit (Task 4). The + branch is briefly in a red state only during this commit. + +The technique is to put the async traits and new drivers in a temporary `databases/sqlx/` +submodule during Tasks 1–3. Task 4 moves them into place, updates consumers, and removes the sync +code. + +### Decision update (2026-04-29) + +After implementation review, we decided to keep **eager schema initialization** in this subissue +for operational clarity and parity with the existing sync drivers: + +- Do **not** use per-method lazy schema checks (`ensure_schema()`). +- Keep explicit startup initialization (`create_database_tables()`) in setup/factory wiring. +- Keep using raw `sqlx::query()` DDL in this subissue; migration tooling stays in `1525-06`. + +This decision also applies to Task 4 (switch commit): keep eager initialization there as well. + +### What changes in the drivers + +The current drivers use blocking I/O and create the schema eagerly on construction. The new +`sqlx`-backed drivers: + +- Use `SqlitePool` / `MySqlPool` with lazy `connect_lazy_with()`. +- Manage the schema with raw `sqlx::query()` DDL statements (`CREATE TABLE IF NOT EXISTS ...`), + exactly mirroring what the current sync drivers do. `sqlx::migrate!()` and migration files are + **not** introduced here — that is subissue `1525-06`. +- Keep schema initialization eager via setup/factory initialization (`create_database_tables()`). +- All trait methods become `async fn` (via `async_trait`). + +## Tasks + +### Task 1 — Add sqlx infrastructure (no behavior change, stays green) + +Add the async substrate without touching the existing drivers or traits. + +#### Dependencies + +In `packages/tracker-core/Cargo.toml`, add: + +```toml +async-trait = "*" # latest compatible with MSRV 1.72 +sqlx = { version = "*", features = ["sqlite", "mysql", "runtime-tokio-native-tls"] } # latest compatible +tokio = { version = "*", features = ["full"] } # latest compatible; if not already present with needed features +``` + +Use the latest crate versions compatible with MSRV 1.72. + +Keep `r2d2`, `r2d2_sqlite`, `rusqlite`, and the `mysql` crate — they are still needed by the old +drivers until Task 4. + +#### Error handling + +Update `databases/error.rs` so that `sqlx::Error` can be converted into the existing `Error` +type. The variants `ConnectionError`, `InvalidQuery`, and `QueryReturnedNoRows` **already exist** +in `error.rs`; do not re-introduce them. The only required change is: + +- Broaden `ConnectionError`: its `source` field currently wraps `LocatedError<'static, UrlError>` + (MySQL-specific). Generalize it to `LocatedError<'static, dyn std::error::Error + Send + Sync>` + so it can hold any connection-level error from sqlx as well. +- Add `From<(sqlx::Error, Driver)>` — maps `sqlx::Error` variants to `ConnectionError`, + `QueryReturnedNoRows`, or `InvalidQuery` based on error kind (see reference `error.rs`). Do not + add `Error::migration_error()` — that belongs to `1525-06`. + +Do not change any other existing variants. The `ConnectionPool` variant (wraps `r2d2::Error`) is +removed in Task 4 together with the `r2d2` dependency. + +**Outcome**: `cargo test --workspace --all-targets` still passes. No behavior change. + +### Task 2 — Implement async SQLite driver (stays green) + +Create a new async SQLite driver in a parallel `databases/sqlx/` submodule without touching the +existing `databases/driver/sqlite/` subdirectory. + +> **Note**: post-1525-04 the sync drivers are already split into per-trait files. The actual +> existing layout is: +> +> ```text +> databases/driver/sqlite/mod.rs +> databases/driver/sqlite/schema_migrator.rs +> databases/driver/sqlite/torrent_metrics_store.rs +> databases/driver/sqlite/whitelist_store.rs +> databases/driver/sqlite/auth_key_store.rs +> ``` +> +> The async parallel module must mirror this layout. + +#### New files + +```text +packages/tracker-core/src/databases/sqlx/mod.rs ← async trait definitions + AsyncDatabase aggregate +packages/tracker-core/src/databases/sqlx/sqlite/mod.rs ← SqliteSqlx struct + pool/latch +packages/tracker-core/src/databases/sqlx/sqlite/schema_migrator.rs +packages/tracker-core/src/databases/sqlx/sqlite/torrent_metrics_store.rs +packages/tracker-core/src/databases/sqlx/sqlite/whitelist_store.rs +packages/tracker-core/src/databases/sqlx/sqlite/auth_key_store.rs +``` + +#### Async trait definitions (`databases/sqlx/mod.rs`) + +Define async versions of the four narrow traits. Use `async_trait` for object safety: + +```rust +use async_trait::async_trait; + +#[async_trait] +pub trait AsyncSchemaMigrator: Send + Sync { + async fn create_database_tables(&self) -> Result<(), Error>; + async fn drop_database_tables(&self) -> Result<(), Error>; +} + +// ... AsyncTorrentMetricsStore, AsyncWhitelistStore, AsyncAuthKeyStore (same method +// signatures as their sync counterparts but with async fn) + +pub trait AsyncDatabase: + AsyncSchemaMigrator + AsyncTorrentMetricsStore + AsyncWhitelistStore + AsyncAuthKeyStore +{ +} + +impl<T> AsyncDatabase for T where + T: AsyncSchemaMigrator + AsyncTorrentMetricsStore + AsyncWhitelistStore + AsyncAuthKeyStore +{ +} +``` + +#### `SqliteSqlx` struct (`databases/sqlx/sqlite.rs`) + +Mirrors the reference `Sqlite` in `driver/sqlite.rs` (PR branch): + +```rust +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; +use sqlx::SqlitePool; +use std::sync::atomic::{AtomicBool, Ordering}; +use tokio::sync::Mutex; + +pub(crate) struct SqliteSqlx { + pool: SqlitePool, + schema_ready: AtomicBool, + schema_lock: Mutex<()>, +} +``` + +Implement `AsyncSchemaMigrator`, `AsyncTorrentMetricsStore`, `AsyncWhitelistStore`, and +`AsyncAuthKeyStore` for `SqliteSqlx`. All SQL queries use `sqlx::query(...)`. Schema +initialization in `create_database_tables()` executes raw `CREATE TABLE IF NOT EXISTS ...` +statements via `sqlx::query()` — no `sqlx::migrate!()` in this step. + +#### Tests + +Add an inline `#[cfg(test)]` module in `databases/sqlx/sqlite.rs`. Use the shared +`databases/driver/tests::run_tests()` helper (or a new async equivalent) to run all behavioral +tests against `SqliteSqlx`. Use `torrust_tracker_test_helpers::configuration::ephemeral_sqlite_database` +for the in-memory/temp-file path. + +**Outcome**: `cargo test --workspace --all-targets` still passes. Old sync `Sqlite` driver +untouched. + +### Task 3 — Implement async MySQL driver (stays green) + +Create a `packages/tracker-core/src/databases/sqlx/mysql/` subdirectory mirroring the same +per-trait file layout as `databases/sqlx/sqlite/` (i.e. `mod.rs`, `schema_migrator.rs`, +`torrent_metrics_store.rs`, `whitelist_store.rs`, `auth_key_store.rs`) but using `MySqlPool`. Schema initialization uses raw +`sqlx::query()` DDL — no `sqlx::migrate!()` in this step. + +Implement the same four async traits. Add an inline `#[cfg(test)]` module that runs the shared +behavioral test suite against a real MySQL instance (via environment variable guard +`TORRUST_TRACKER_CORE_RUN_MYSQL_DRIVER_TEST=true`, consistent with existing MySQL test gating). + +**Outcome**: `cargo test --workspace --all-targets` still passes. Old sync `Mysql` driver +untouched. + +### Task 4 — Switch: replace sync traits with async, update consumers (brief red) + +This task is a single focused commit. Steps within the commit: + +1. **Rename async traits to canonical names**: rename `AsyncSchemaMigrator` → `SchemaMigrator`, + `AsyncTorrentMetricsStore` → `TorrentMetricsStore`, etc. in `databases/sqlx/mod.rs`. Rename + `AsyncDatabase` → `Database`. Move the trait definitions from `databases/sqlx/mod.rs` into + `databases/traits/` (replacing the sync trait definitions in + `databases/traits/schema.rs`, `databases/traits/torrent_metrics.rs`, + `databases/traits/whitelist.rs`, `databases/traits/auth_keys.rs`). + Move the driver subdirectories, overwriting the old sync drivers: + `databases/sqlx/sqlite/` → `databases/driver/sqlite/` and + `databases/sqlx/mysql/` → `databases/driver/mysql/`. + Remove the now-empty `databases/sqlx/` submodule. + +2. **Rename driver structs**: rename `SqliteSqlx` → `Sqlite`, `MysqlSqlx` → `Mysql`. + +3. **Clean up `databases/driver/mod.rs`**: remove the sync test helpers that call trait methods + without `.await`; replace with async equivalents. + +4. **Update `databases/setup.rs` — `initialize_database()`**: this function already returns + `DatabaseStores` (a struct of four `Arc<dyn XxxStore>` fields, one per narrow trait — not + `Arc<Box<dyn Database>>`). Keep eager `create_database_tables()` during initialization. + No return-type change is needed. + +5. **Add `.await` at all consumer call sites**: every location that called a narrow-trait method + synchronously now needs `.await`. The affected files are: + - `statistics/persisted/downloads.rs` (`DatabaseDownloadsMetricRepository`) + - `whitelist/repository/persisted.rs` (`DatabaseWhitelist`) + - `whitelist/setup.rs` + - `authentication/key/repository/persisted.rs` (`DatabaseKeyRepository`) + - `authentication/handler.rs` (test helpers) + - `src/bin/persistence_benchmark/driver_bench/` and + `src/bin/persistence_benchmark/driver_bench/operations/` (benchmark binary) + - Any integration tests in `tests/` + +6. **Remove unused dependencies**: remove `r2d2`, `r2d2_sqlite`, `rusqlite`, and `r2d2_mysql` + from `tracker-core/Cargo.toml`. Also remove the `ConnectionPool` error variant and its + `From<(r2d2::Error, Driver)>` impl from `databases/error.rs`. Run `cargo machete` to verify. + +7. **Update mock usage**: `#[automock]` on the narrow traits generates async mocks via `mockall`. + Note that `MockDatabase` was already removed in `1525-04` (the aggregate supertrait has no + methods). The actual breakage surface in this switch commit is the four narrow-trait mocks: + `MockSchemaMigrator`, `MockTorrentMetricsStore`, `MockWhitelistStore`, and `MockAuthKeyStore`. + Any tests written against the **sync** versions of these mocks (from `1525-04`) will fail to + compile after the switch because async `mockall` mocks use + `.returning(|| Box::pin(async { Ok(()) }))` rather than `.returning(|| Ok(()))`. Find and + update all such tests before declaring this task complete. + +**Outcome**: `cargo test --workspace --all-targets` passes. `linter all` exits `0`. Sync drivers +and all `r2d2`/`rusqlite`/`mysql` dependencies are gone. + +### Task 5 — Remove sync-to-async runtime bridges (cleanup follow-up) + +During Task 4, some sync wrappers were introduced to keep existing sync consumers working +while trait methods became async (helpers named `block_on_current_or_new_runtime`). +These wrappers are a transitional compatibility mechanism and should be removed. + +This task migrates remaining sync call paths to native async end-to-end: + +1. Make repository/service methods async where they call async persistence traits. +2. Propagate `.await` through callers instead of blocking at lower layers. +3. Remove all `block_on_current_or_new_runtime` helpers from tracker-core modules. +4. Keep runtime ownership at application boundaries only (no nested runtime creation). +5. Preserve eager schema initialization behavior while using async initialization paths. + +**Outcome**: no `block_on_current_or_new_runtime` helper remains; persistence interactions +are fully async from call sites to drivers; tests, linters, and benchmarks still pass. + +### Task 6 — Remove legacy persistence surface and temporary sqlx staging tree + +The branch still contains a mixed layout: + +- canonical runtime code under `packages/tracker-core/src/databases/driver/` and + `packages/tracker-core/src/databases/traits/` +- temporary migration staging code under `packages/tracker-core/src/databases/sqlx/` +- legacy compatibility dependencies and error conversions that were expected to disappear in the + switch commit + +This task finishes the structural cleanup so the repository reflects a single persistence model. + +1. Remove the temporary staging subtree under `packages/tracker-core/src/databases/sqlx/`, + including its nested `driver/` and `traits/` directories. +2. Ensure `packages/tracker-core/src/databases/driver/` contains only the canonical sqlx-backed + implementations that remain in use. +3. Ensure `packages/tracker-core/src/databases/traits/` contains only the canonical async trait + definitions that remain in use. +4. Remove leftover legacy compatibility code tied to the pre-sqlx drivers, including obsolete + error conversions and type references. +5. Remove obsolete dependencies from `packages/tracker-core/Cargo.toml`: `r2d2`, `r2d2_sqlite`, + `rusqlite`, and `r2d2_mysql`. +6. Regenerate lockfile state as needed and confirm `cargo machete` still passes. + +**Outcome**: there is one canonical async persistence surface only; the temporary `databases/sqlx/` +tree is gone; legacy sync-driver compatibility code and dependencies are gone. + +### Task 7 — Record final validation and benchmark status + +Once the structural cleanup is complete, record the remaining evidence needed to close the +subissue cleanly. + +Benchmark entrypoints and docs for the implementer: + +- Binary entrypoint: `packages/tracker-core/src/bin/persistence_benchmark_runner.rs` +- Binary-private implementation modules: `packages/tracker-core/src/bin/persistence_benchmark/` +- Benchmark artifact index and workflow notes: `packages/tracker-core/docs/benchmarking/README.md` +- Baseline benchmark spec and command examples: `docs/issues/1710-1525-03-persistence-benchmarking.md` +- Current committed baseline artifacts: `packages/tracker-core/docs/benchmarking/runs/2026-04-28/` + +Typical commands: + +```text +cargo run -p bittorrent-tracker-core --bin persistence_benchmark_runner -- \ + --driver sqlite3 + +cargo run -p bittorrent-tracker-core --bin persistence_benchmark_runner -- \ + --driver mysql \ + --db-version 8.4 +``` + +1. Run and record focused validation for the final cleanup work. +2. Run `cargo test --workspace --all-targets` and `linter all` on the final state. +3. Run the persistence benchmark comparison against the committed baseline from subissue `1525-03`, + or explicitly document why that comparison is still deferred. +4. Update the acceptance criteria in this spec to match the final verified state. + +**Outcome**: the spec contains closure-quality evidence for remaining acceptance criteria instead +of inferred status. + +## Constraints + +- Do not add PostgreSQL in this step. +- Do not introduce `sqlx::migrate!()`, migration files, or the `sqlx` `macros` feature — those + are introduced in subissue `1525-06`. +- Do not change the SQL schema in this step (schema evolution is `1525-06`). +- `DatabaseStores` (four `Arc<dyn XxxStore>` fields, one per narrow trait) is already the + consumer-facing type returned by `initialize_database()`; do not change this. Do not introduce + `Arc<Box<dyn Database>>` or the `Persistence` struct from the reference implementation. +- Keep startup schema initialization eager in this subissue and in Task 4. + +## Acceptance Criteria + +### Progress Review (2026-04-30) + +Status: structural cleanup and benchmark validation complete. + +What is done: + +- SQLite and MySQL driver implementations use `sqlx` pools and async trait methods. +- Schema initialization is still eager in `initialize_database()`. +- Schema creation still uses raw `sqlx::query()` DDL, and `sqlx::migrate!()` is not used. +- Sync-to-async bridge helpers introduced during the migration have been removed, and async initialization has been propagated through current call paths. +- The temporary staging subtree under `packages/tracker-core/src/databases/sqlx/` has been removed; the canonical `databases/driver/` and `databases/traits/` directories are the single persistence surface. +- Legacy `r2d2`, `r2d2_sqlite`, and `r2d2_mysql` dependencies have been removed from `packages/tracker-core/Cargo.toml` (the `rusqlite` symbol was only re-exported through `r2d2_sqlite`; no separate direct dep existed). +- Legacy compatibility/error plumbing has been removed from `packages/tracker-core/src/databases/error.rs` (no more `ConnectionPool` variant or `r2d2`/`rusqlite`/`mysql` `From` impls) and from `packages/tracker-core/src/authentication/key/mod.rs` (the `From<rusqlite::Error>` impl is now `From<sqlx::Error>`). +- Stale `r2d2_*` references in driver doc comments have been replaced with accurate `sqlx`-based wording. +- Current validation passed: `cargo machete`, `linter all`, doc tests, and full workspace tests on the cleaned-up state. +- Persistence benchmark comparison against the `2026-04-28` baseline recorded under `packages/tracker-core/docs/benchmarking/runs/2026-04-30/`. No regression: MySQL totals are 13–16% faster and SQLite per-operation medians stay within run-to-run variance. The bench harness was updated to wait for the MySQL container's TCP listener (sqlx no longer hides this race the way r2d2 did); production code paths are unchanged. + +What is still not done: + +- There is no recorded evidence in this branch that Tasks 1 to 3 were each validated independently at the time they were completed. + +- [x] SQLite and MySQL drivers use `sqlx` with async trait methods. +- [x] Schema initialization remains eager via setup/factory initialization. +- [x] Schema management uses raw `sqlx::query()` DDL; `sqlx::migrate!()` is not used. +- [x] `r2d2`, `r2d2_sqlite`, `rusqlite`, and the `mysql` crate are removed from + `tracker-core/Cargo.toml`. +- [x] Existing behavior is preserved end-to-end. +- [x] All temporary sync-to-async runtime bridge helpers (e.g. `block_on_current_or_new_runtime`) are removed and replaced with native async call paths. +- [ ] The branch compiles and all tests pass after each of Tasks 1–3 individually (verified by CI + or manual `cargo test` run after each task). +- [x] Persistence benchmarking (see subissue `1525-03`) shows no regression against the committed + baseline. — See `packages/tracker-core/docs/benchmarking/runs/2026-04-30/REPORT.md` for the + full comparison; MySQL totals improved by 13–16% and SQLite per-op medians remained within + run-to-run variance. +- [x] `cargo test --workspace --all-targets` passes. +- [x] `linter all` exits with code `0`. +- [x] `cargo machete` reports no unused dependencies. + +## Out of Scope + +- PostgreSQL driver — that is subissue `1525-08`. +- `sqlx::migrate!()` and migration files — that is subissue `1525-06`. +- `async_trait` removal — the `async_trait` crate is required at MSRV 1.72 because + async-fn-in-traits was stabilized in Rust 1.75. When the MSRV is raised to 1.75+, remove + `async_trait` and replace `#[async_trait]` attribute usage with native async trait syntax. + Track this as a follow-up when the MSRV is next bumped. + +## References + +- EPIC: `#1525` +- Subissue `1525-04`: `docs/issues/1713-1525-04-split-persistence-traits.md` — **already merged + into `develop`** +- Subissue `1525-03`: `docs/issues/1525-03-persistence-benchmarking.md` — benchmark baseline +- Reference PR: `#1695` +- Reference implementation branch: `josecelano:pr-1684-review` — local checkout at + `/home/josecelano/Documents/git/committer/me/github/torrust/torrust-tracker-pr-1700`; + consult only if blocked during implementation +- Reference files (async driver implementations — note: the reference uses `sqlx::migrate!()` + which is not adopted in this step; use raw DDL instead): + - `packages/tracker-core/src/databases/driver/sqlite.rs` + - `packages/tracker-core/src/databases/driver/mysql.rs` + - `packages/tracker-core/src/databases/driver/mod.rs` diff --git a/packages/axum-http-tracker-server/src/environment.rs b/packages/axum-http-tracker-server/src/environment.rs index 616973a0f..57f64bd15 100644 --- a/packages/axum-http-tracker-server/src/environment.rs +++ b/packages/axum-http-tracker-server/src/environment.rs @@ -4,7 +4,6 @@ use bittorrent_http_tracker_core::container::HttpTrackerCoreContainer; use bittorrent_http_tracker_core::statistics::event::listener::run_event_listener; use bittorrent_primitives::info_hash::InfoHash; use bittorrent_tracker_core::container::TrackerCoreContainer; -use futures::executor::block_on; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use torrust_axum_server::tsl::make_rust_tls; @@ -42,17 +41,16 @@ impl Environment<Stopped> { /// Will panic if it fails to make the TSL config from the configuration. #[allow(dead_code)] #[must_use] - pub fn new(configuration: &Arc<Configuration>) -> Self { + pub async fn new(configuration: &Arc<Configuration>) -> Self { initialize_global_services(configuration); - let container = Arc::new(EnvContainer::initialize(configuration)); + let container = Arc::new(EnvContainer::initialize(configuration).await); let bind_to = container.http_tracker_core_container.http_tracker_config.bind_address; - let tls = block_on(make_rust_tls( - &container.http_tracker_core_container.http_tracker_config.tsl_config, - )) - .map(|tls| tls.expect("tls config failed")); + let tls = make_rust_tls(&container.http_tracker_core_container.http_tracker_config.tsl_config) + .await + .map(|tls| tls.expect("tls config failed")); let server = HttpServer::new(Launcher::new(bind_to, tls)); @@ -98,7 +96,7 @@ impl Environment<Stopped> { impl Environment<Running> { pub async fn new(configuration: &Arc<Configuration>) -> Self { - Environment::<Stopped>::new(configuration).start().await + Environment::<Stopped>::new(configuration).await.start().await } /// Stops the test environment and return a stopped environment. @@ -142,7 +140,7 @@ impl EnvContainer { /// /// Will panic if the configuration is missing the HTTP tracker configuration. #[must_use] - pub fn initialize(configuration: &Configuration) -> Self { + pub async fn initialize(configuration: &Configuration) -> Self { let core_config = Arc::new(configuration.core.clone()); let http_tracker_config = configuration .http_trackers @@ -154,10 +152,8 @@ impl EnvContainer { configuration.core.tracker_usage_statistics.into(), )); - let tracker_core_container = Arc::new(TrackerCoreContainer::initialize_from( - &core_config, - &swarm_coordination_registry_container, - )); + let tracker_core_container = + Arc::new(TrackerCoreContainer::initialize_from(&core_config, &swarm_coordination_registry_container).await); let http_tracker_container = HttpTrackerCoreContainer::initialize_from_tracker_core(&tracker_core_container, &http_tracker_config); diff --git a/packages/axum-http-tracker-server/src/server.rs b/packages/axum-http-tracker-server/src/server.rs index 69f9cb72e..f3ec3b8c7 100644 --- a/packages/axum-http-tracker-server/src/server.rs +++ b/packages/axum-http-tracker-server/src/server.rs @@ -270,7 +270,7 @@ mod tests { use crate::server::{HttpServer, Launcher}; - pub fn initialize_container(configuration: &Configuration) -> HttpTrackerCoreContainer { + pub async fn initialize_container(configuration: &Configuration) -> HttpTrackerCoreContainer { let cancellation_token = CancellationToken::new(); let core_config = Arc::new(configuration.core.clone()); @@ -302,10 +302,8 @@ mod tests { configuration.core.tracker_usage_statistics.into(), )); - let tracker_core_container = Arc::new(TrackerCoreContainer::initialize_from( - &core_config, - &swarm_coordination_registry_container, - )); + let tracker_core_container = + Arc::new(TrackerCoreContainer::initialize_from(&core_config, &swarm_coordination_registry_container).await); let announce_service = Arc::new(AnnounceService::new( tracker_core_container.core_config.clone(), @@ -355,7 +353,7 @@ mod tests { initialize_global_services(&configuration); - let http_tracker_container = Arc::new(initialize_container(&configuration)); + let http_tracker_container = Arc::new(initialize_container(&configuration).await); let bind_to = http_tracker_config.bind_address; diff --git a/packages/axum-http-tracker-server/src/v1/handlers/announce.rs b/packages/axum-http-tracker-server/src/v1/handlers/announce.rs index 59fdc5b34..155f6893e 100644 --- a/packages/axum-http-tracker-server/src/v1/handlers/announce.rs +++ b/packages/axum-http-tracker-server/src/v1/handlers/announce.rs @@ -133,28 +133,28 @@ mod tests { pub announce_service: Arc<AnnounceService>, } - fn initialize_private_tracker() -> CoreHttpTrackerServices { - initialize_core_tracker_services(&configuration::ephemeral_private()) + async fn initialize_private_tracker() -> CoreHttpTrackerServices { + initialize_core_tracker_services(&configuration::ephemeral_private()).await } - fn initialize_listed_tracker() -> CoreHttpTrackerServices { - initialize_core_tracker_services(&configuration::ephemeral_listed()) + async fn initialize_listed_tracker() -> CoreHttpTrackerServices { + initialize_core_tracker_services(&configuration::ephemeral_listed()).await } - fn initialize_tracker_on_reverse_proxy() -> CoreHttpTrackerServices { - initialize_core_tracker_services(&configuration::ephemeral_with_reverse_proxy()) + async fn initialize_tracker_on_reverse_proxy() -> CoreHttpTrackerServices { + initialize_core_tracker_services(&configuration::ephemeral_with_reverse_proxy()).await } - fn initialize_tracker_not_on_reverse_proxy() -> CoreHttpTrackerServices { - initialize_core_tracker_services(&configuration::ephemeral_without_reverse_proxy()) + async fn initialize_tracker_not_on_reverse_proxy() -> CoreHttpTrackerServices { + initialize_core_tracker_services(&configuration::ephemeral_without_reverse_proxy()).await } - fn initialize_core_tracker_services(config: &Configuration) -> CoreHttpTrackerServices { + async fn initialize_core_tracker_services(config: &Configuration) -> CoreHttpTrackerServices { let cancellation_token = CancellationToken::new(); // Initialize the core tracker services with the provided configuration. let core_config = Arc::new(config.core.clone()); - let database = initialize_database(&config.core); + let database = initialize_database(&config.core).await; let in_memory_whitelist = Arc::new(InMemoryWhitelist::default()); let whitelist_authorization = Arc::new(WhitelistAuthorization::new(&config.core, &in_memory_whitelist.clone())); let in_memory_key_repository = Arc::new(InMemoryKeyRepository::default()); @@ -236,7 +236,7 @@ mod tests { #[tokio::test] async fn it_should_fail_when_the_authentication_key_is_missing() { - let http_core_tracker_services = initialize_private_tracker(); + let http_core_tracker_services = initialize_private_tracker().await; let server_socket_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 7070); let server_service_binding = ServiceBinding::new(Protocol::HTTP, server_socket_addr).unwrap(); @@ -265,7 +265,7 @@ mod tests { #[tokio::test] async fn it_should_fail_when_the_authentication_key_is_invalid() { - let http_core_tracker_services = initialize_private_tracker(); + let http_core_tracker_services = initialize_private_tracker().await; let unregistered_key = authentication::Key::from_str("YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ").unwrap(); @@ -308,7 +308,7 @@ mod tests { #[tokio::test] async fn it_should_fail_when_the_announced_torrent_is_not_whitelisted() { - let http_core_tracker_services = initialize_listed_tracker(); + let http_core_tracker_services = initialize_listed_tracker().await; let announce_request = sample_announce_request(); @@ -353,7 +353,7 @@ mod tests { #[tokio::test] async fn it_should_fail_when_the_right_most_x_forwarded_for_header_ip_is_not_available() { - let http_core_tracker_services = initialize_tracker_on_reverse_proxy(); + let http_core_tracker_services = initialize_tracker_on_reverse_proxy().await; let client_ip_sources = ClientIpSources { right_most_x_forwarded_for: None, @@ -398,7 +398,7 @@ mod tests { #[tokio::test] async fn it_should_fail_when_the_client_ip_from_the_connection_info_is_not_available() { - let http_core_tracker_services = initialize_tracker_not_on_reverse_proxy(); + let http_core_tracker_services = initialize_tracker_not_on_reverse_proxy().await; let client_ip_sources = ClientIpSources { right_most_x_forwarded_for: None, diff --git a/packages/axum-rest-tracker-api-server/src/environment.rs b/packages/axum-rest-tracker-api-server/src/environment.rs index cddb45277..2c138ad50 100644 --- a/packages/axum-rest-tracker-api-server/src/environment.rs +++ b/packages/axum-rest-tracker-api-server/src/environment.rs @@ -5,7 +5,6 @@ use bittorrent_http_tracker_core::container::HttpTrackerCoreContainer; use bittorrent_primitives::info_hash::InfoHash; use bittorrent_tracker_core::container::TrackerCoreContainer; use bittorrent_udp_tracker_core::container::UdpTrackerCoreContainer; -use futures::executor::block_on; use torrust_axum_server::tsl::make_rust_tls; use torrust_rest_tracker_api_client::connection_info::{ConnectionInfo, Origin}; use torrust_rest_tracker_api_core::container::TrackerHttpApiCoreContainer; @@ -48,17 +47,16 @@ impl Environment<Stopped> { /// Will panic if it cannot make the TSL configuration from the provided /// configuration. #[must_use] - pub fn new(configuration: &Arc<Configuration>) -> Self { + pub async fn new(configuration: &Arc<Configuration>) -> Self { initialize_global_services(configuration); - let container = Arc::new(EnvContainer::initialize(configuration)); + let container = Arc::new(EnvContainer::initialize(configuration).await); let bind_to = container.tracker_http_api_core_container.http_api_config.bind_address; - let tls = block_on(make_rust_tls( - &container.tracker_http_api_core_container.http_api_config.tsl_config, - )) - .map(|tls| tls.expect("tls config failed")); + let tls = make_rust_tls(&container.tracker_http_api_core_container.http_api_config.tsl_config) + .await + .map(|tls| tls.expect("tls config failed")); let server = ApiServer::new(Launcher::new(bind_to, tls)); @@ -99,7 +97,7 @@ impl Environment<Stopped> { impl Environment<Running> { pub async fn new(configuration: &Arc<Configuration>) -> Self { - Environment::<Stopped>::new(configuration).start().await + Environment::<Stopped>::new(configuration).await.start().await } /// # Panics @@ -153,7 +151,7 @@ impl EnvContainer { /// - The configuration does not contain a UDP tracker configuration. /// - The configuration does not contain a HTTP API configuration. #[must_use] - pub fn initialize(configuration: &Configuration) -> Self { + pub async fn initialize(configuration: &Configuration) -> Self { let core_config = Arc::new(configuration.core.clone()); let http_tracker_config = configuration @@ -177,10 +175,8 @@ impl EnvContainer { core_config.tracker_usage_statistics.into(), )); - let tracker_core_container = Arc::new(TrackerCoreContainer::initialize_from( - &core_config, - &swarm_coordination_registry_container, - )); + let tracker_core_container = + Arc::new(TrackerCoreContainer::initialize_from(&core_config, &swarm_coordination_registry_container).await); let http_tracker_core_container = HttpTrackerCoreContainer::initialize_from_tracker_core(&tracker_core_container, &http_tracker_config); diff --git a/packages/axum-rest-tracker-api-server/src/server.rs b/packages/axum-rest-tracker-api-server/src/server.rs index 9eef6b71a..460bdefc0 100644 --- a/packages/axum-rest-tracker-api-server/src/server.rs +++ b/packages/axum-rest-tracker-api-server/src/server.rs @@ -350,7 +350,8 @@ mod tests { let register = &Registar::default(); let http_api_container = - TrackerHttpApiCoreContainer::initialize(&core_config, &http_tracker_config, &udp_tracker_config, &http_api_config); + TrackerHttpApiCoreContainer::initialize(&core_config, &http_tracker_config, &udp_tracker_config, &http_api_config) + .await; let started = stopped .start(http_api_container, register.give_form(), access_tokens) diff --git a/packages/axum-rest-tracker-api-server/tests/server/mod.rs b/packages/axum-rest-tracker-api-server/tests/server/mod.rs index 80fd9d9b2..2808c27f9 100644 --- a/packages/axum-rest-tracker-api-server/tests/server/mod.rs +++ b/packages/axum-rest-tracker-api-server/tests/server/mod.rs @@ -14,6 +14,6 @@ use bittorrent_tracker_core::databases::SchemaMigrator; /// /// - Inject a database mock in the future. /// - Inject directly the database reference passed to the Tracker type. -pub fn force_database_error(schema_migrator: &Arc<dyn SchemaMigrator>) { - schema_migrator.drop_database_tables().unwrap(); +pub async fn force_database_error(schema_migrator: &Arc<dyn SchemaMigrator>) { + schema_migrator.drop_database_tables().await.unwrap(); } diff --git a/packages/axum-rest-tracker-api-server/tests/server/v1/contract/context/auth_key.rs b/packages/axum-rest-tracker-api-server/tests/server/v1/contract/context/auth_key.rs index fd78791d3..20865370d 100644 --- a/packages/axum-rest-tracker-api-server/tests/server/v1/contract/context/auth_key.rs +++ b/packages/axum-rest-tracker-api-server/tests/server/v1/contract/context/auth_key.rs @@ -135,7 +135,7 @@ async fn should_fail_when_the_auth_key_cannot_be_generated() { let env = Started::new(&configuration::ephemeral().into()).await; - force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator); + force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator).await; let request_id = Uuid::new_v4(); @@ -315,7 +315,7 @@ async fn should_fail_when_the_auth_key_cannot_be_deleted() { .await .unwrap(); - force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator); + force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator).await; let request_id = Uuid::new_v4(); @@ -433,7 +433,7 @@ async fn should_fail_when_keys_cannot_be_reloaded() { .await .unwrap(); - force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator); + force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator).await; let response = Client::new(env.get_connection_info()) .unwrap() @@ -598,7 +598,7 @@ mod deprecated_generate_key_endpoint { let env = Started::new(&configuration::ephemeral().into()).await; - force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator); + force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator).await; let request_id = Uuid::new_v4(); let seconds_valid = 60; diff --git a/packages/axum-rest-tracker-api-server/tests/server/v1/contract/context/whitelist.rs b/packages/axum-rest-tracker-api-server/tests/server/v1/contract/context/whitelist.rs index 0bee10881..019628a97 100644 --- a/packages/axum-rest-tracker-api-server/tests/server/v1/contract/context/whitelist.rs +++ b/packages/axum-rest-tracker-api-server/tests/server/v1/contract/context/whitelist.rs @@ -115,7 +115,7 @@ async fn should_fail_when_the_torrent_cannot_be_whitelisted() { let info_hash = "9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d".to_owned(); // DevSkim: ignore DS173237 - force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator); + force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator).await; let request_id = Uuid::new_v4(); @@ -266,7 +266,7 @@ async fn should_fail_when_the_torrent_cannot_be_removed_from_the_whitelist() { .await .unwrap(); - force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator); + force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator).await; let request_id = Uuid::new_v4(); @@ -392,7 +392,7 @@ async fn should_fail_when_the_whitelist_cannot_be_reloaded_from_the_database() { .await .unwrap(); - force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator); + force_database_error(&env.container.tracker_core_container.database_stores.schema_migrator).await; let request_id = Uuid::new_v4(); diff --git a/packages/http-tracker-core/benches/helpers/sync.rs b/packages/http-tracker-core/benches/helpers/sync.rs index dbf0dac83..f77c9bc5b 100644 --- a/packages/http-tracker-core/benches/helpers/sync.rs +++ b/packages/http-tracker-core/benches/helpers/sync.rs @@ -8,7 +8,7 @@ use crate::helpers::util::{initialize_core_tracker_services, sample_announce_req #[must_use] pub async fn return_announce_data_once(samples: u64) -> Duration { - let (core_tracker_services, core_http_tracker_services) = initialize_core_tracker_services(); + let (core_tracker_services, core_http_tracker_services) = initialize_core_tracker_services().await; let peer = sample_peer(); diff --git a/packages/http-tracker-core/benches/helpers/util.rs b/packages/http-tracker-core/benches/helpers/util.rs index 5c703929c..4f2f96459 100644 --- a/packages/http-tracker-core/benches/helpers/util.rs +++ b/packages/http-tracker-core/benches/helpers/util.rs @@ -38,15 +38,17 @@ pub struct CoreHttpTrackerServices { pub http_stats_event_sender: bittorrent_http_tracker_core::event::sender::Sender, } -pub fn initialize_core_tracker_services() -> (CoreTrackerServices, CoreHttpTrackerServices) { - initialize_core_tracker_services_with_config(&configuration::ephemeral_public()) +pub async fn initialize_core_tracker_services() -> (CoreTrackerServices, CoreHttpTrackerServices) { + initialize_core_tracker_services_with_config(&configuration::ephemeral_public()).await } -pub fn initialize_core_tracker_services_with_config(config: &Configuration) -> (CoreTrackerServices, CoreHttpTrackerServices) { +pub async fn initialize_core_tracker_services_with_config( + config: &Configuration, +) -> (CoreTrackerServices, CoreHttpTrackerServices) { let cancellation_token = CancellationToken::new(); let core_config = Arc::new(config.core.clone()); - let database = initialize_database(&config.core); + let database = initialize_database(&config.core).await; let in_memory_torrent_repository = Arc::new(InMemoryTorrentRepository::default()); let db_downloads_metric_repository = Arc::new(DatabaseDownloadsMetricRepository::new(&database.torrent_metrics_store)); let in_memory_whitelist = Arc::new(InMemoryWhitelist::default()); diff --git a/packages/http-tracker-core/src/container.rs b/packages/http-tracker-core/src/container.rs index ed0aaf8b0..cc4e69a49 100644 --- a/packages/http-tracker-core/src/container.rs +++ b/packages/http-tracker-core/src/container.rs @@ -26,15 +26,13 @@ pub struct HttpTrackerCoreContainer { impl HttpTrackerCoreContainer { #[must_use] - pub fn initialize(core_config: &Arc<Core>, http_tracker_config: &Arc<HttpTracker>) -> Arc<Self> { + pub async fn initialize(core_config: &Arc<Core>, http_tracker_config: &Arc<HttpTracker>) -> Arc<Self> { let swarm_coordination_registry_container = Arc::new(SwarmCoordinationRegistryContainer::initialize( core_config.tracker_usage_statistics.into(), )); - let tracker_core_container = Arc::new(TrackerCoreContainer::initialize_from( - core_config, - &swarm_coordination_registry_container, - )); + let tracker_core_container = + Arc::new(TrackerCoreContainer::initialize_from(core_config, &swarm_coordination_registry_container).await); Self::initialize_from_tracker_core(&tracker_core_container, http_tracker_config) } diff --git a/packages/http-tracker-core/src/services/announce.rs b/packages/http-tracker-core/src/services/announce.rs index 5b1cce6f0..e6ace18b1 100644 --- a/packages/http-tracker-core/src/services/announce.rs +++ b/packages/http-tracker-core/src/services/announce.rs @@ -232,15 +232,17 @@ mod tests { pub http_stats_event_sender: crate::event::sender::Sender, } - fn initialize_core_tracker_services() -> (CoreTrackerServices, CoreHttpTrackerServices) { - initialize_core_tracker_services_with_config(&configuration::ephemeral_public()) + async fn initialize_core_tracker_services() -> (CoreTrackerServices, CoreHttpTrackerServices) { + initialize_core_tracker_services_with_config(&configuration::ephemeral_public()).await } - fn initialize_core_tracker_services_with_config(config: &Configuration) -> (CoreTrackerServices, CoreHttpTrackerServices) { + async fn initialize_core_tracker_services_with_config( + config: &Configuration, + ) -> (CoreTrackerServices, CoreHttpTrackerServices) { let cancellation_token = CancellationToken::new(); let core_config = Arc::new(config.core.clone()); - let database = initialize_database(&config.core); + let database = initialize_database(&config.core).await; let in_memory_torrent_repository = Arc::new(InMemoryTorrentRepository::default()); let db_downloads_metric_repository = Arc::new(DatabaseDownloadsMetricRepository::new(&database.torrent_metrics_store)); let in_memory_whitelist = Arc::new(InMemoryWhitelist::default()); @@ -346,7 +348,7 @@ mod tests { #[tokio::test] async fn it_should_return_the_announce_data() { - let (core_tracker_services, core_http_tracker_services) = initialize_core_tracker_services(); + let (core_tracker_services, core_http_tracker_services) = initialize_core_tracker_services().await; let peer = sample_peer(); @@ -412,7 +414,7 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(1))))); let http_stats_event_sender: crate::event::sender::Sender = Some(Arc::new(http_stats_event_sender_mock)); - let (core_tracker_services, mut core_http_tracker_services) = initialize_core_tracker_services(); + let (core_tracker_services, mut core_http_tracker_services) = initialize_core_tracker_services().await; core_http_tracker_services.http_stats_event_sender = http_stats_event_sender; @@ -486,7 +488,7 @@ mod tests { let http_stats_event_sender: crate::event::sender::Sender = Some(Arc::new(http_stats_event_sender_mock)); let (core_tracker_services, mut core_http_tracker_services) = - initialize_core_tracker_services_with_config(&tracker_with_an_ipv6_external_ip()); + initialize_core_tracker_services_with_config(&tracker_with_an_ipv6_external_ip()).await; core_http_tracker_services.http_stats_event_sender = http_stats_event_sender; @@ -532,7 +534,7 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(1))))); let http_stats_event_sender: crate::event::sender::Sender = Some(Arc::new(http_stats_event_sender_mock)); - let (core_tracker_services, mut core_http_tracker_services) = initialize_core_tracker_services(); + let (core_tracker_services, mut core_http_tracker_services) = initialize_core_tracker_services().await; core_http_tracker_services.http_stats_event_sender = http_stats_event_sender; let (announce_request, client_ip_sources) = sample_announce_request_for_peer(peer); diff --git a/packages/http-tracker-core/src/services/scrape.rs b/packages/http-tracker-core/src/services/scrape.rs index 9c5aad3e9..29fd424d3 100644 --- a/packages/http-tracker-core/src/services/scrape.rs +++ b/packages/http-tracker-core/src/services/scrape.rs @@ -195,8 +195,8 @@ mod tests { authentication_service: Arc<AuthenticationService>, } - fn initialize_services_with_configuration(config: &Configuration) -> Container { - let database = initialize_database(&config.core); + async fn initialize_services_with_configuration(config: &Configuration) -> Container { + let database = initialize_database(&config.core).await; let in_memory_whitelist = Arc::new(InMemoryWhitelist::default()); let whitelist_authorization = Arc::new(WhitelistAuthorization::new(&config.core, &in_memory_whitelist.clone())); let in_memory_torrent_repository = Arc::new(InMemoryTorrentRepository::default()); @@ -281,7 +281,7 @@ mod tests { let http_stats_event_sender = http_stats_event_bus.sender(); - let container = initialize_services_with_configuration(&configuration); + let container = initialize_services_with_configuration(&configuration).await; let info_hash = sample_info_hash(); let info_hashes = vec![info_hash]; @@ -352,7 +352,7 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(1))))); let http_stats_event_sender: crate::event::sender::Sender = Some(Arc::new(http_stats_event_sender_mock)); - let container = initialize_services_with_configuration(&config); + let container = initialize_services_with_configuration(&config).await; let peer_ip = IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)); @@ -406,7 +406,7 @@ mod tests { .returning(|_| Box::pin(future::ready(Some(Ok(1))))); let http_stats_event_sender: crate::event::sender::Sender = Some(Arc::new(http_stats_event_sender_mock)); - let container = initialize_services_with_configuration(&config); + let container = initialize_services_with_configuration(&config).await; let peer_ip = IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)); @@ -465,7 +465,7 @@ mod tests { ) { let config = configuration::ephemeral_private(); - let container = initialize_services_with_configuration(&config); + let container = initialize_services_with_configuration(&config).await; // HTTP core stats let http_core_broadcaster = Broadcaster::default(); @@ -518,7 +518,7 @@ mod tests { async fn it_should_send_the_tcp_4_scrape_event_when_the_peer_uses_ipv4() { let config = configuration::ephemeral(); - let container = initialize_services_with_configuration(&config); + let container = initialize_services_with_configuration(&config).await; let mut http_stats_event_sender_mock = MockHttpStatsEventSender::new(); http_stats_event_sender_mock @@ -570,7 +570,7 @@ mod tests { let config = configuration::ephemeral(); - let container = initialize_services_with_configuration(&config); + let container = initialize_services_with_configuration(&config).await; let mut http_stats_event_sender_mock = MockHttpStatsEventSender::new(); http_stats_event_sender_mock diff --git a/packages/rest-tracker-api-core/src/container.rs b/packages/rest-tracker-api-core/src/container.rs index bcc5a0186..9be6a5d00 100644 --- a/packages/rest-tracker-api-core/src/container.rs +++ b/packages/rest-tracker-api-core/src/container.rs @@ -30,7 +30,7 @@ pub struct TrackerHttpApiCoreContainer { impl TrackerHttpApiCoreContainer { #[must_use] - pub fn initialize( + pub async fn initialize( core_config: &Arc<Core>, http_tracker_config: &Arc<HttpTracker>, udp_tracker_config: &Arc<UdpTracker>, @@ -40,10 +40,8 @@ impl TrackerHttpApiCoreContainer { core_config.tracker_usage_statistics.into(), )); - let tracker_core_container = Arc::new(TrackerCoreContainer::initialize_from( - core_config, - &swarm_coordination_registry_container, - )); + let tracker_core_container = + Arc::new(TrackerCoreContainer::initialize_from(core_config, &swarm_coordination_registry_container).await); let http_tracker_core_container = HttpTrackerCoreContainer::initialize_from_tracker_core(&tracker_core_container, http_tracker_config); diff --git a/packages/rest-tracker-api-core/src/statistics/services.rs b/packages/rest-tracker-api-core/src/statistics/services.rs index f87cb8c76..bb397b74a 100644 --- a/packages/rest-tracker-api-core/src/statistics/services.rs +++ b/packages/rest-tracker-api-core/src/statistics/services.rs @@ -222,7 +222,7 @@ mod tests { Arc::new(SwarmCoordinationRegistryContainer::initialize(SenderStatus::Enabled)); let tracker_core_container = - TrackerCoreContainer::initialize_from(&core_config, &swarm_coordination_registry_container.clone()); + TrackerCoreContainer::initialize_from(&core_config, &swarm_coordination_registry_container.clone()).await; let _ban_service = Arc::new(RwLock::new(BanService::new(MAX_CONNECTION_ID_ERRORS_PER_IP))); diff --git a/packages/tracker-core/Cargo.toml b/packages/tracker-core/Cargo.toml index 3913283ff..03172fbb6 100644 --- a/packages/tracker-core/Cargo.toml +++ b/packages/tracker-core/Cargo.toml @@ -19,18 +19,17 @@ db-compatibility-tests = [ ] [dependencies] anyhow = "1" +async-trait = "0" aquatic_udp_protocol = "0" bittorrent-primitives = "0.1.0" chrono = { version = "0", default-features = false, features = [ "clock" ] } clap = { version = "4", features = [ "derive" ] } derive_more = { version = "2", features = [ "as_ref", "constructor", "from" ] } mockall = "0" -r2d2 = "0" -r2d2_mysql = "25" -r2d2_sqlite = { version = "0", features = [ "bundled" ] } rand = "0" serde = { version = "1", features = [ "derive" ] } serde_json = { version = "1", features = [ "preserve_order" ] } +sqlx = { version = "0.8", features = [ "mysql", "runtime-tokio-native-tls", "sqlite" ] } thiserror = "2" tokio = { version = "1", features = [ "macros", "net", "rt-multi-thread", "signal", "sync" ] } tokio-util = "0.7.15" @@ -50,3 +49,6 @@ mockall = "0" torrust-rest-tracker-api-client = { version = "3.0.0-develop", path = "../rest-tracker-api-client" } torrust-tracker-test-helpers = { version = "3.0.0-develop", path = "../test-helpers" } url = "2.5.4" + +[package.metadata.cargo-machete] +ignored = [ "async-trait" ] diff --git a/packages/tracker-core/docs/benchmarking/README.md b/packages/tracker-core/docs/benchmarking/README.md index e8fac458a..b3b5af704 100644 --- a/packages/tracker-core/docs/benchmarking/README.md +++ b/packages/tracker-core/docs/benchmarking/README.md @@ -28,6 +28,20 @@ Raw JSON artifacts: - `runs/2026-04-28/mysql-8.4.json` - `runs/2026-04-28/mysql-8.0.json` +## Post-SQLx run + +- Date: `2026-04-30` +- Commit (HEAD at run time): `a4dbc63a6c713e115bfc11374b72743aa51ebfb5` +- Issue context: `docs/issues/1717-1525-05-migrate-sqlite-and-mysql-to-sqlx.md` +- Run summary (with comparison vs `2026-04-28`): `runs/2026-04-30/REPORT.md` +- Machine profile: `machine/2026-04-30-josecelano-desktop.txt` + +Raw JSON artifacts: + +- `runs/2026-04-30/sqlite3.json` +- `runs/2026-04-30/mysql-8.4.json` +- `runs/2026-04-30/mysql-8.0.json` + ## How to add a new run 1. Create a new run folder: @@ -63,3 +77,5 @@ After implementing: - `docs/issues/1525-05-migrate-sqlite-and-mysql-to-sqlx.md` run the same benchmark commands again, store results in a new dated folder, and compare against `runs/2026-04-28`. + +The first such comparison was captured at `runs/2026-04-30/REPORT.md`. diff --git a/packages/tracker-core/docs/benchmarking/machine/2026-04-30-josecelano-desktop.txt b/packages/tracker-core/docs/benchmarking/machine/2026-04-30-josecelano-desktop.txt new file mode 100644 index 000000000..9c1daecd7 --- /dev/null +++ b/packages/tracker-core/docs/benchmarking/machine/2026-04-30-josecelano-desktop.txt @@ -0,0 +1,96 @@ +hostname: +josecelano-desktop + +date_utc: +2026-04-30T07:34:51Z + +uname -a: +Linux josecelano-desktop 6.17.0-22-generic #22-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 13 12:04:44 UTC 2026 x86_64 GNU/Linux + +/etc/os-release: +PRETTY_NAME="Ubuntu 25.10" +NAME="Ubuntu" +VERSION_ID="25.10" +VERSION="25.10 (Questing Quokka)" +VERSION_CODENAME=questing +ID=ubuntu +ID_LIKE=debian +HOME_URL="https://www.ubuntu.com/" +SUPPORT_URL="https://help.ubuntu.com/" +BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" +PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" +UBUNTU_CODENAME=questing +LOGO=ubuntu-logo + +lscpu: +Architecture: x86_64 +CPU op-mode(s): 32-bit, 64-bit +Address sizes: 48 bits physical, 48 bits virtual +Byte Order: Little Endian +CPU(s): 32 +On-line CPU(s) list: 0-31 +Vendor ID: AuthenticAMD +Model name: AMD Ryzen 9 7950X 16-Core Processor +CPU family: 25 +Model: 97 +Thread(s) per core: 2 +Core(s) per socket: 16 +Socket(s): 1 +Stepping: 2 +Frequency boost: enabled +CPU(s) scaling MHz: 79% +CPU max MHz: 5883,1968 +CPU min MHz: 425,2920 +BogoMIPS: 8982,52 +Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good amd_lbr_v2 nopl xtopology nonstop_tsc cpuid extd_apicid aperfmperf rapl pni pclmulqdq monitor ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw ibs skinit wdt tce topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx cpuid_fault cpb cat_l3 cdp_l3 hw_pstate ssbd mba perfmon_v2 ibrs ibpb stibp ibrs_enhanced vmmcall fsgsbase bmi1 avx2 smep bmi2 erms invpcid cqm rdt_a avx512f avx512dq rdseed adx smap avx512ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local user_shstk avx512_bf16 clzero irperf xsaveerptr rdpru wbnoinvd cppc arat npt lbrv svm_lock nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold avic vgif x2avic v_spec_ctrl vnmi avx512vbmi umip pku ospke avx512_vbmi2 gfni vaes vpclmulqdq avx512_vnni avx512_bitalg avx512_vpopcntdq rdpid overflow_recov succor smca fsrm flush_l1d amd_lbr_pmc_freeze +Virtualization: AMD-V +L1d cache: 512 KiB (16 instances) +L1i cache: 512 KiB (16 instances) +L2 cache: 16 MiB (16 instances) +L3 cache: 64 MiB (2 instances) +NUMA node(s): 1 +NUMA node0 CPU(s): 0-31 +Vulnerability Gather data sampling: Not affected +Vulnerability Ghostwrite: Not affected +Vulnerability Indirect target selection: Not affected +Vulnerability Itlb multihit: Not affected +Vulnerability L1tf: Not affected +Vulnerability Mds: Not affected +Vulnerability Meltdown: Not affected +Vulnerability Mmio stale data: Not affected +Vulnerability Old microcode: Not affected +Vulnerability Reg file data sampling: Not affected +Vulnerability Retbleed: Not affected +Vulnerability Spec rstack overflow: Mitigation; Safe RET +Vulnerability Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl +Vulnerability Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization +Vulnerability Spectre v2: Mitigation; Enhanced / Automatic IBRS; IBPB conditional; STIBP always-on; PBRSB-eIBRS Not affected; BHI Not affected +Vulnerability Srbds: Not affected +Vulnerability Tsa: Mitigation; Clear CPU buffers +Vulnerability Tsx async abort: Not affected +Vulnerability Vmscape: Mitigation; IBPB before exit to userspace + +free -h: + total used free shared buff/cache available +Mem: 61Gi 15Gi 28Gi 437Mi 18Gi 45Gi +Swap: 8,0Gi 3,7Gi 4,3Gi + +rustc -Vv: +rustc 1.97.0-nightly (37d85e592 2026-04-28) +binary: rustc +commit-hash: 37d85e592f9ae5f20f7d9a9f99785246fa7298da +commit-date: 2026-04-28 +host: x86_64-unknown-linux-gnu +release: 1.97.0-nightly +LLVM version: 22.1.4 + +cargo -V: +cargo 1.97.0-nightly (eb9b60f1f 2026-04-24) + +docker version: +Docker version 28.3.3, build 980b856 + +podman version: +Command 'podman' not found, but can be installed with: +sudo apt install podman +podman-not-available diff --git a/packages/tracker-core/docs/benchmarking/runs/2026-04-30/REPORT.md b/packages/tracker-core/docs/benchmarking/runs/2026-04-30/REPORT.md new file mode 100644 index 000000000..5a3343092 --- /dev/null +++ b/packages/tracker-core/docs/benchmarking/runs/2026-04-30/REPORT.md @@ -0,0 +1,115 @@ +# Benchmark Report - 2026-04-30 + +This run captures benchmark results after migrating the SQLite and MySQL +drivers from `r2d2` + `rusqlite` / `mysql` to `sqlx 0.8`: + +- `docs/issues/1717-1525-05-migrate-sqlite-and-mysql-to-sqlx.md` + +It is the post-SQLx counterpart of the `2026-04-28` baseline. + +## Run context + +- Commit (HEAD at run time): `a4dbc63a6c713e115bfc11374b72743aa51ebfb5` +- Ops per operation: `100` +- Benchmark runner: `cargo run -p bittorrent-tracker-core --bin persistence_benchmark_runner` +- Machine profile: `../../machine/2026-04-30-josecelano-desktop.txt` +- Same machine as the `2026-04-28` baseline (AMD Ryzen 9 7950X, Ubuntu 25.10). + +The `git_revision` recorded in the JSON artifacts is `a4dbc63a…`. A small +benchmark-harness change was applied locally on top of that commit to wait +for the MySQL container to fully accept TCP connections before running +DDL (see "Notes" below). The change does not touch any code path that +contributes to recorded operation timings, so the numbers remain +comparable. + +## Raw artifacts + +- `sqlite3.json` +- `mysql-8.4.json` +- `mysql-8.0.json` + +## High-level timing summary + +`meta.timings_ms.total`: + +| Driver | Baseline (2026-04-28) | New (2026-04-30) | Delta | +| --------- | --------------------: | ---------------: | -------: | +| sqlite3 | 75 ms | 118 ms | +43 ms | +| mysql 8.4 | 7381 ms | 6231 ms | −1150 ms | +| mysql 8.0 | 7633 ms | 6678 ms | −955 ms | + +Interpretation: + +- MySQL totals improve by ~13–16% on both 8.0 and 8.4, mostly driven by + much faster `remove_*` operations (see medians below). +- sqlite3 total rises by 43 ms. On a 75 ms baseline with only 100 ops per + operation and no warmup, this is well inside run-to-run noise; per-op + medians (next section) are within a handful of microseconds of the + baseline and the `remove_*` operations are actually faster. + +## Selected operation medians (microseconds) + +| Operation | sqlite3 (base → new) | mysql 8.4 (base → new) | mysql 8.0 (base → new) | +| ------------------------------- | -------------------: | ---------------------: | ---------------------: | +| save_torrent_downloads | 64 → 80 | 750 → 779 | 949 → 978 | +| load_torrent_downloads | 9 → 24 | 114 → 119 | 133 → 139 | +| increase_downloads_for_torrent | 50 → 73 | 759 → 824 | 1027 → 972 | +| save_global_downloads | 58 → 72 | 745 → 834 | 1020 → 1046 | +| increase_global_downloads | 49 → 65 | 748 → 820 | 1007 → 1053 | +| add_info_hash_to_whitelist | 61 → 82 | 715 → 739 | 998 → 1010 | +| remove_info_hash_from_whitelist | 116 → 73 | 1460 → 743 | 1902 → 982 | +| add_key_to_keys | 61 → 79 | 712 → 730 | 948 → 958 | +| remove_key_from_keys | 116 → 71 | 1476 → 739 | 1883 → 952 | + +Notable changes: + +- `remove_*` operations are roughly **2× faster** on MySQL 8.4 and 8.0, + and ~35% faster on SQLite. Likely sqlx prepared-statement reuse and + the absence of r2d2 connection-checkout overhead on these short + operations. +- `save_*` and simple `load_*` ops show small (~10–20 µs on SQLite, + ~10–80 µs on MySQL) regressions, well inside per-run variance. +- Overall MySQL throughput is meaningfully better; SQLite totals are + unchanged once you discount the dominant per-op variance contribution. + +## Regression assessment + +No regression. The largest single per-operation regression on either +driver is the SQLite `load_torrent_downloads` median going from 9 µs to +24 µs. That difference (15 µs) is the same order of magnitude as the +syscall jitter that sqlx adds for query execution, and is paid for many +times over by the `remove_*` improvements. End-to-end MySQL benchmark +time drops by 13–16%. + +## Machine characteristics (summary) + +From `../../machine/2026-04-30-josecelano-desktop.txt`: + +- Host: `josecelano-desktop` +- OS: `Ubuntu 25.10` +- Kernel: `Linux 6.17.0-22-generic` +- CPU: `AMD Ryzen 9 7950X` (16 cores / 32 threads) +- Container runtime used by benchmark: `Docker 28.3.3` + +Identical hardware to the `2026-04-28` baseline. + +## Notes + +`sqlx` opens connection pools lazily and does not retry the first query +on connect failure. With the `mysql:8.x` testcontainer image the very +first DDL statement issued by the benchmark harness occasionally raced +the TCP listener and failed with `UnexpectedEof`. The +`r2d2`-based driver previously masked this through implicit pool +checkout retries. + +The benchmark harness now waits for the second `ready for connections` +log line on the container's stderr (the official `mysql` image emits it +twice — first transiently on the unix socket during init, then again on +TCP port `3306`) and then performs a short `connect`+`SELECT 1` retry +loop before handing off to `initialize_database`. This is a bench-only +change in +`packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/mysql.rs` +and does not alter production code paths. + +Whether to introduce a similar startup-retry policy in production +should be considered separately. diff --git a/packages/tracker-core/docs/benchmarking/runs/2026-04-30/mysql-8.0.json b/packages/tracker-core/docs/benchmarking/runs/2026-04-30/mysql-8.0.json new file mode 100644 index 000000000..ecdb6f6d0 --- /dev/null +++ b/packages/tracker-core/docs/benchmarking/runs/2026-04-30/mysql-8.0.json @@ -0,0 +1,121 @@ +{ + "meta": { + "git_revision": "a4dbc63a6c713e115bfc11374b72743aa51ebfb5", + "driver": "mysql", + "db_version": "8.0", + "ops": 100, + "timestamp": "2026-04-30T08:10:56.811832134+00:00", + "timings_ms": { + "benchmark": 6678, + "report_build": 1, + "total": 6679 + } + }, + "operations": [ + { + "name": "save_torrent_downloads", + "count": 100, + "best_us": 720, + "median_us": 978, + "worst_us": 1565 + }, + { + "name": "load_torrent_downloads", + "count": 100, + "best_us": 115, + "median_us": 139, + "worst_us": 543 + }, + { + "name": "load_all_torrents_downloads", + "count": 100, + "best_us": 174, + "median_us": 198, + "worst_us": 291 + }, + { + "name": "increase_downloads_for_torrent", + "count": 100, + "best_us": 778, + "median_us": 972, + "worst_us": 1488 + }, + { + "name": "save_global_downloads", + "count": 100, + "best_us": 762, + "median_us": 1046, + "worst_us": 1482 + }, + { + "name": "load_global_downloads", + "count": 100, + "best_us": 113, + "median_us": 136, + "worst_us": 252 + }, + { + "name": "increase_global_downloads", + "count": 100, + "best_us": 731, + "median_us": 1053, + "worst_us": 1469 + }, + { + "name": "add_info_hash_to_whitelist", + "count": 100, + "best_us": 759, + "median_us": 1010, + "worst_us": 8684 + }, + { + "name": "get_info_hash_from_whitelist", + "count": 100, + "best_us": 104, + "median_us": 117, + "worst_us": 280 + }, + { + "name": "load_whitelist", + "count": 100, + "best_us": 161, + "median_us": 169, + "worst_us": 274 + }, + { + "name": "remove_info_hash_from_whitelist", + "count": 100, + "best_us": 802, + "median_us": 982, + "worst_us": 4835 + }, + { + "name": "add_key_to_keys", + "count": 100, + "best_us": 725, + "median_us": 958, + "worst_us": 1361 + }, + { + "name": "get_key_from_keys", + "count": 100, + "best_us": 103, + "median_us": 124, + "worst_us": 299 + }, + { + "name": "load_keys", + "count": 100, + "best_us": 166, + "median_us": 179, + "worst_us": 327 + }, + { + "name": "remove_key_from_keys", + "count": 100, + "best_us": 754, + "median_us": 952, + "worst_us": 1558 + } + ] +} diff --git a/packages/tracker-core/docs/benchmarking/runs/2026-04-30/mysql-8.4.json b/packages/tracker-core/docs/benchmarking/runs/2026-04-30/mysql-8.4.json new file mode 100644 index 000000000..d5c37ce30 --- /dev/null +++ b/packages/tracker-core/docs/benchmarking/runs/2026-04-30/mysql-8.4.json @@ -0,0 +1,121 @@ +{ + "meta": { + "git_revision": "a4dbc63a6c713e115bfc11374b72743aa51ebfb5", + "driver": "mysql", + "db_version": "8.4", + "ops": 100, + "timestamp": "2026-04-30T08:09:16.593106220+00:00", + "timings_ms": { + "benchmark": 6231, + "report_build": 1, + "total": 6232 + } + }, + "operations": [ + { + "name": "save_torrent_downloads", + "count": 100, + "best_us": 709, + "median_us": 779, + "worst_us": 1594 + }, + { + "name": "load_torrent_downloads", + "count": 100, + "best_us": 94, + "median_us": 119, + "worst_us": 240 + }, + { + "name": "load_all_torrents_downloads", + "count": 100, + "best_us": 153, + "median_us": 168, + "worst_us": 275 + }, + { + "name": "increase_downloads_for_torrent", + "count": 100, + "best_us": 711, + "median_us": 824, + "worst_us": 1266 + }, + { + "name": "save_global_downloads", + "count": 100, + "best_us": 718, + "median_us": 834, + "worst_us": 2425 + }, + { + "name": "load_global_downloads", + "count": 100, + "best_us": 97, + "median_us": 123, + "worst_us": 309 + }, + { + "name": "increase_global_downloads", + "count": 100, + "best_us": 729, + "median_us": 820, + "worst_us": 1431 + }, + { + "name": "add_info_hash_to_whitelist", + "count": 100, + "best_us": 703, + "median_us": 739, + "worst_us": 1591 + }, + { + "name": "get_info_hash_from_whitelist", + "count": 100, + "best_us": 93, + "median_us": 110, + "worst_us": 250 + }, + { + "name": "load_whitelist", + "count": 100, + "best_us": 150, + "median_us": 159, + "worst_us": 241 + }, + { + "name": "remove_info_hash_from_whitelist", + "count": 100, + "best_us": 708, + "median_us": 743, + "worst_us": 2117 + }, + { + "name": "add_key_to_keys", + "count": 100, + "best_us": 691, + "median_us": 730, + "worst_us": 1126 + }, + { + "name": "get_key_from_keys", + "count": 100, + "best_us": 95, + "median_us": 106, + "worst_us": 216 + }, + { + "name": "load_keys", + "count": 100, + "best_us": 161, + "median_us": 180, + "worst_us": 302 + }, + { + "name": "remove_key_from_keys", + "count": 100, + "best_us": 685, + "median_us": 739, + "worst_us": 1147 + } + ] +} diff --git a/packages/tracker-core/docs/benchmarking/runs/2026-04-30/sqlite3.json b/packages/tracker-core/docs/benchmarking/runs/2026-04-30/sqlite3.json new file mode 100644 index 000000000..45d920c81 --- /dev/null +++ b/packages/tracker-core/docs/benchmarking/runs/2026-04-30/sqlite3.json @@ -0,0 +1,121 @@ +{ + "meta": { + "git_revision": "a4dbc63a6c713e115bfc11374b72743aa51ebfb5", + "driver": "sqlite3", + "db_version": "-", + "ops": 100, + "timestamp": "2026-04-30T07:35:03.030593914+00:00", + "timings_ms": { + "benchmark": 116, + "report_build": 1, + "total": 118 + } + }, + "operations": [ + { + "name": "save_torrent_downloads", + "count": 100, + "best_us": 78, + "median_us": 80, + "worst_us": 104 + }, + { + "name": "load_torrent_downloads", + "count": 100, + "best_us": 23, + "median_us": 24, + "worst_us": 51 + }, + { + "name": "load_all_torrents_downloads", + "count": 100, + "best_us": 70, + "median_us": 80, + "worst_us": 198 + }, + { + "name": "increase_downloads_for_torrent", + "count": 100, + "best_us": 66, + "median_us": 73, + "worst_us": 134 + }, + { + "name": "save_global_downloads", + "count": 100, + "best_us": 70, + "median_us": 72, + "worst_us": 234 + }, + { + "name": "load_global_downloads", + "count": 100, + "best_us": 20, + "median_us": 21, + "worst_us": 40 + }, + { + "name": "increase_global_downloads", + "count": 100, + "best_us": 63, + "median_us": 65, + "worst_us": 79 + }, + { + "name": "add_info_hash_to_whitelist", + "count": 100, + "best_us": 76, + "median_us": 82, + "worst_us": 109 + }, + { + "name": "get_info_hash_from_whitelist", + "count": 100, + "best_us": 21, + "median_us": 23, + "worst_us": 53 + }, + { + "name": "load_whitelist", + "count": 100, + "best_us": 51, + "median_us": 60, + "worst_us": 87 + }, + { + "name": "remove_info_hash_from_whitelist", + "count": 100, + "best_us": 70, + "median_us": 73, + "worst_us": 118 + }, + { + "name": "add_key_to_keys", + "count": 100, + "best_us": 76, + "median_us": 79, + "worst_us": 128 + }, + { + "name": "get_key_from_keys", + "count": 100, + "best_us": 21, + "median_us": 21, + "worst_us": 41 + }, + { + "name": "load_keys", + "count": 100, + "best_us": 75, + "median_us": 82, + "worst_us": 121 + }, + { + "name": "remove_key_from_keys", + "count": 100, + "best_us": 69, + "median_us": 71, + "worst_us": 115 + } + ] +} diff --git a/packages/tracker-core/src/announce_handler.rs b/packages/tracker-core/src/announce_handler.rs index 0b6bffd31..150550f49 100644 --- a/packages/tracker-core/src/announce_handler.rs +++ b/packages/tracker-core/src/announce_handler.rs @@ -167,20 +167,20 @@ impl AnnounceHandler { peer.change_ip(&assign_ip_address_to_peer(remote_client_ip, self.config.net.external_ip)); self.in_memory_torrent_repository - .handle_announcement(info_hash, peer, self.load_downloads_metric_if_needed(info_hash)?) + .handle_announcement(info_hash, peer, self.load_downloads_metric_if_needed(info_hash).await?) .await; Ok(self.build_announce_data(info_hash, peer, peers_wanted).await) } /// Loads the number of downloads for a torrent if needed. - fn load_downloads_metric_if_needed( + async fn load_downloads_metric_if_needed( &self, info_hash: &InfoHash, ) -> Result<Option<NumberOfDownloads>, databases::error::Error> { if self.config.tracker_policy.persistent_torrent_completed_stat && !self.in_memory_torrent_repository.contains(info_hash) { - Ok(self.db_downloads_metric_repository.load_torrent_downloads(info_hash)?) + Ok(self.db_downloads_metric_repository.load_torrent_downloads(info_hash).await?) } else { Ok(None) } @@ -292,9 +292,9 @@ mod tests { use crate::scrape_handler::ScrapeHandler; use crate::test_helpers::tests::initialize_handlers; - fn public_tracker() -> (Arc<AnnounceHandler>, Arc<ScrapeHandler>) { + async fn public_tracker() -> (Arc<AnnounceHandler>, Arc<ScrapeHandler>) { let config = configuration::ephemeral_public(); - initialize_handlers(&config) + initialize_handlers(&config).await } // The client peer IP @@ -453,7 +453,7 @@ mod tests { #[tokio::test] async fn it_should_return_the_announce_data_with_an_empty_peer_list_when_it_is_the_first_announced_peer() { - let (announce_handler, _scrape_handler) = public_tracker(); + let (announce_handler, _scrape_handler) = public_tracker().await; let mut peer = sample_peer(); @@ -467,7 +467,7 @@ mod tests { #[tokio::test] async fn it_should_return_the_announce_data_with_the_previously_announced_peers() { - let (announce_handler, _scrape_handler) = public_tracker(); + let (announce_handler, _scrape_handler) = public_tracker().await; let mut previously_announced_peer = sample_peer_1(); announce_handler @@ -491,7 +491,7 @@ mod tests { #[tokio::test] async fn it_should_allow_peers_to_get_only_a_subset_of_the_peers_in_the_swarm() { - let (announce_handler, _scrape_handler) = public_tracker(); + let (announce_handler, _scrape_handler) = public_tracker().await; let mut previously_announced_peer_1 = sample_peer_1(); announce_handler @@ -537,7 +537,7 @@ mod tests { #[tokio::test] async fn when_the_peer_is_a_seeder() { - let (announce_handler, _scrape_handler) = public_tracker(); + let (announce_handler, _scrape_handler) = public_tracker().await; let mut peer = seeder(); @@ -551,7 +551,7 @@ mod tests { #[tokio::test] async fn when_the_peer_is_a_leecher() { - let (announce_handler, _scrape_handler) = public_tracker(); + let (announce_handler, _scrape_handler) = public_tracker().await; let mut peer = leecher(); @@ -565,7 +565,7 @@ mod tests { #[tokio::test] async fn when_a_previously_announced_started_peer_has_completed_downloading() { - let (announce_handler, _scrape_handler) = public_tracker(); + let (announce_handler, _scrape_handler) = public_tracker().await; // We have to announce with "started" event because peer does not count if peer was not previously known let mut started_peer = started_peer(); diff --git a/packages/tracker-core/src/authentication/handler.rs b/packages/tracker-core/src/authentication/handler.rs index 780837026..0c42e350c 100644 --- a/packages/tracker-core/src/authentication/handler.rs +++ b/packages/tracker-core/src/authentication/handler.rs @@ -182,7 +182,7 @@ impl KeysHandler { pub async fn generate_expiring_peer_key(&self, lifetime: Option<Duration>) -> Result<PeerKey, databases::error::Error> { let peer_key = key::generate_key(lifetime); - self.db_key_repository.add(&peer_key)?; + self.db_key_repository.add(&peer_key).await?; self.in_memory_key_repository.insert(&peer_key).await; @@ -229,7 +229,7 @@ impl KeysHandler { // code-review: should we return a friendly error instead of the DB // constrain error when the key already exist? For now, it's returning // the specif error for each DB driver when a UNIQUE constrain fails. - self.db_key_repository.add(&peer_key)?; + self.db_key_repository.add(&peer_key).await?; self.in_memory_key_repository.insert(&peer_key).await; @@ -249,7 +249,7 @@ impl KeysHandler { /// Returns a `databases::error::Error` if the key cannot be removed from /// the database. pub async fn remove_peer_key(&self, key: &Key) -> Result<(), databases::error::Error> { - self.db_key_repository.remove(key)?; + self.db_key_repository.remove(key).await?; self.remove_in_memory_auth_key(key).await; @@ -277,7 +277,7 @@ impl KeysHandler { /// /// Returns a `databases::error::Error` if there is an issue loading the keys from the database. pub async fn load_peer_keys_from_database(&self) -> Result<(), databases::error::Error> { - let keys_from_database = self.db_key_repository.load_keys()?; + let keys_from_database = self.db_key_repository.load_keys().await?; self.in_memory_key_repository.reset_with(keys_from_database).await; @@ -301,10 +301,10 @@ mod tests { use crate::databases::setup::initialize_database; use crate::databases::{AuthKeyStore, MockAuthKeyStore}; - fn instantiate_keys_handler() -> KeysHandler { + async fn instantiate_keys_handler() -> KeysHandler { let config = configuration::ephemeral_private(); - instantiate_keys_handler_with_configuration(&config) + instantiate_keys_handler_with_configuration(&config).await } fn instantiate_keys_handler_with_database(auth_key_store: &Arc<dyn AuthKeyStore>) -> KeysHandler { @@ -314,10 +314,10 @@ mod tests { KeysHandler::new(&db_key_repository, &in_memory_key_repository) } - fn instantiate_keys_handler_with_configuration(config: &Configuration) -> KeysHandler { + async fn instantiate_keys_handler_with_configuration(config: &Configuration) -> KeysHandler { // todo: pass only Core configuration - let stores = initialize_database(&config.core); + let stores = initialize_database(&config.core).await; let db_key_repository = Arc::new(DatabaseKeyRepository::new(&stores.auth_key_store)); let in_memory_key_repository = Arc::new(InMemoryKeyRepository::default()); @@ -338,7 +338,7 @@ mod tests { #[tokio::test] async fn it_should_generate_the_key() { - let keys_handler = instantiate_keys_handler(); + let keys_handler = instantiate_keys_handler().await; let peer_key = keys_handler .generate_expiring_peer_key(Some(Duration::from_secs(100))) @@ -372,7 +372,7 @@ mod tests { #[tokio::test] async fn it_should_add_a_randomly_generated_key() { - let keys_handler = instantiate_keys_handler(); + let keys_handler = instantiate_keys_handler().await; let peer_key = keys_handler .add_peer_key(AddKeyRequest { @@ -403,9 +403,11 @@ mod tests { })) .times(1) .returning(|_peer_key| { - Err(databases::error::Error::InsertFailed { - location: Location::caller(), - driver: Driver::Sqlite3, + Box::pin(async move { + Err(databases::error::Error::InsertFailed { + location: Location::caller(), + driver: Driver::Sqlite3, + }) }) }); let auth_key_store: Arc<dyn AuthKeyStore> = Arc::new(database_mock); @@ -444,7 +446,7 @@ mod tests { #[tokio::test] async fn it_should_add_a_pre_generated_key() { - let keys_handler = instantiate_keys_handler(); + let keys_handler = instantiate_keys_handler().await; let peer_key = keys_handler .add_peer_key(AddKeyRequest { @@ -465,7 +467,7 @@ mod tests { #[tokio::test] async fn it_should_fail_adding_a_pre_generated_key_when_the_key_duration_exceeds_the_maximum_duration() { - let keys_handler = instantiate_keys_handler(); + let keys_handler = instantiate_keys_handler().await; let result = keys_handler .add_peer_key(AddKeyRequest { @@ -479,7 +481,7 @@ mod tests { #[tokio::test] async fn it_should_fail_adding_a_pre_generated_key_when_the_key_is_invalid() { - let keys_handler = instantiate_keys_handler(); + let keys_handler = instantiate_keys_handler().await; let result = keys_handler .add_peer_key(AddKeyRequest { @@ -508,9 +510,11 @@ mod tests { .with(predicate::eq(expected_peer_key)) .times(1) .returning(|_peer_key| { - Err(databases::error::Error::InsertFailed { - location: Location::caller(), - driver: Driver::Sqlite3, + Box::pin(async move { + Err(databases::error::Error::InsertFailed { + location: Location::caller(), + driver: Driver::Sqlite3, + }) }) }); let auth_key_store: Arc<dyn AuthKeyStore> = Arc::new(database_mock); @@ -549,7 +553,7 @@ mod tests { #[tokio::test] async fn it_should_generate_the_key() { - let keys_handler = instantiate_keys_handler(); + let keys_handler = instantiate_keys_handler().await; let peer_key = keys_handler.generate_permanent_peer_key().await.unwrap(); @@ -558,7 +562,7 @@ mod tests { #[tokio::test] async fn it_should_add_a_randomly_generated_key() { - let keys_handler = instantiate_keys_handler(); + let keys_handler = instantiate_keys_handler().await; let peer_key = keys_handler .add_peer_key(AddKeyRequest { @@ -579,9 +583,11 @@ mod tests { .with(function(move |peer_key: &PeerKey| peer_key.valid_until.is_none())) .times(1) .returning(|_peer_key| { - Err(databases::error::Error::InsertFailed { - location: Location::caller(), - driver: Driver::Sqlite3, + Box::pin(async move { + Err(databases::error::Error::InsertFailed { + location: Location::caller(), + driver: Driver::Sqlite3, + }) }) }); let auth_key_store: Arc<dyn AuthKeyStore> = Arc::new(database_mock); @@ -617,7 +623,7 @@ mod tests { #[tokio::test] async fn it_should_add_a_pre_generated_key() { - let keys_handler = instantiate_keys_handler(); + let keys_handler = instantiate_keys_handler().await; let peer_key = keys_handler .add_peer_key(AddKeyRequest { @@ -638,7 +644,7 @@ mod tests { #[tokio::test] async fn it_should_fail_adding_a_pre_generated_key_when_the_key_is_invalid() { - let keys_handler = instantiate_keys_handler(); + let keys_handler = instantiate_keys_handler().await; let result = keys_handler .add_peer_key(AddKeyRequest { @@ -663,9 +669,11 @@ mod tests { .with(predicate::eq(expected_peer_key)) .times(1) .returning(|_peer_key| { - Err(databases::error::Error::InsertFailed { - location: Location::caller(), - driver: Driver::Sqlite3, + Box::pin(async move { + Err(databases::error::Error::InsertFailed { + location: Location::caller(), + driver: Driver::Sqlite3, + }) }) }); let auth_key_store: Arc<dyn AuthKeyStore> = Arc::new(database_mock); diff --git a/packages/tracker-core/src/authentication/key/mod.rs b/packages/tracker-core/src/authentication/key/mod.rs index 44bbd0688..ce65385ce 100644 --- a/packages/tracker-core/src/authentication/key/mod.rs +++ b/packages/tracker-core/src/authentication/key/mod.rs @@ -191,8 +191,8 @@ pub enum Error { MissingAuthKey { location: &'static Location<'static> }, } -impl From<r2d2_sqlite::rusqlite::Error> for Error { - fn from(e: r2d2_sqlite::rusqlite::Error) -> Self { +impl From<sqlx::Error> for Error { + fn from(e: sqlx::Error) -> Self { Error::KeyVerificationError { source: (Arc::new(e) as DynError).into(), } @@ -296,7 +296,7 @@ mod tests { #[test] fn could_be_a_database_error() { - let err = r2d2_sqlite::rusqlite::Error::InvalidQuery; + let err = sqlx::Error::RowNotFound; let err: key::Error = err.into(); diff --git a/packages/tracker-core/src/authentication/key/repository/persisted.rs b/packages/tracker-core/src/authentication/key/repository/persisted.rs index c0724f4e2..eed0026f2 100644 --- a/packages/tracker-core/src/authentication/key/repository/persisted.rs +++ b/packages/tracker-core/src/authentication/key/repository/persisted.rs @@ -39,8 +39,8 @@ impl DatabaseKeyRepository { /// # Errors /// /// Returns a [`databases::error::Error`] if the key cannot be added. - pub(crate) fn add(&self, peer_key: &PeerKey) -> Result<(), databases::error::Error> { - self.database.add_key_to_keys(peer_key)?; + pub(crate) async fn add(&self, peer_key: &PeerKey) -> Result<(), databases::error::Error> { + self.database.add_key_to_keys(peer_key).await?; Ok(()) } @@ -53,8 +53,8 @@ impl DatabaseKeyRepository { /// # Errors /// /// Returns a [`databases::error::Error`] if the key cannot be removed. - pub(crate) fn remove(&self, key: &Key) -> Result<(), databases::error::Error> { - self.database.remove_key_from_keys(key)?; + pub(crate) async fn remove(&self, key: &Key) -> Result<(), databases::error::Error> { + self.database.remove_key_from_keys(key).await?; Ok(()) } @@ -67,8 +67,8 @@ impl DatabaseKeyRepository { /// # Returns /// /// A vector containing all persisted [`PeerKey`] entries. - pub(crate) fn load_keys(&self) -> Result<Vec<PeerKey>, databases::error::Error> { - let keys = self.database.load_keys()?; + pub(crate) async fn load_keys(&self) -> Result<Vec<PeerKey>, databases::error::Error> { + let keys = self.database.load_keys().await?; Ok(keys) } } @@ -94,11 +94,11 @@ mod tests { config } - #[test] - fn persist_a_new_peer_key() { + #[tokio::test] + async fn persist_a_new_peer_key() { let configuration = ephemeral_configuration(); - let stores = initialize_database(&configuration); + let stores = initialize_database(&configuration).await; let repository = DatabaseKeyRepository::new(&stores.auth_key_store); @@ -107,18 +107,18 @@ mod tests { valid_until: Some(Duration::new(9999, 0)), }; - let result = repository.add(&peer_key); + let result = repository.add(&peer_key).await; assert!(result.is_ok()); - let keys = repository.load_keys().unwrap(); + let keys = repository.load_keys().await.unwrap(); assert_eq!(keys, vec!(peer_key)); } - #[test] - fn remove_a_persisted_peer_key() { + #[tokio::test] + async fn remove_a_persisted_peer_key() { let configuration = ephemeral_configuration(); - let stores = initialize_database(&configuration); + let stores = initialize_database(&configuration).await; let repository = DatabaseKeyRepository::new(&stores.auth_key_store); @@ -127,20 +127,20 @@ mod tests { valid_until: Some(Duration::new(9999, 0)), }; - let _unused = repository.add(&peer_key); + let _unused = repository.add(&peer_key).await; - let result = repository.remove(&peer_key.key); + let result = repository.remove(&peer_key.key).await; assert!(result.is_ok()); - let keys = repository.load_keys().unwrap(); + let keys = repository.load_keys().await.unwrap(); assert!(keys.is_empty()); } - #[test] - fn load_all_persisted_peer_keys() { + #[tokio::test] + async fn load_all_persisted_peer_keys() { let configuration = ephemeral_configuration(); - let stores = initialize_database(&configuration); + let stores = initialize_database(&configuration).await; let repository = DatabaseKeyRepository::new(&stores.auth_key_store); @@ -149,9 +149,9 @@ mod tests { valid_until: Some(Duration::new(9999, 0)), }; - let _unused = repository.add(&peer_key); + let _unused = repository.add(&peer_key).await; - let keys = repository.load_keys().unwrap(); + let keys = repository.load_keys().await.unwrap(); assert_eq!(keys, vec!(peer_key)); } diff --git a/packages/tracker-core/src/authentication/mod.rs b/packages/tracker-core/src/authentication/mod.rs index 6c3d39f29..ba793ecf0 100644 --- a/packages/tracker-core/src/authentication/mod.rs +++ b/packages/tracker-core/src/authentication/mod.rs @@ -44,13 +44,13 @@ mod tests { use crate::authentication::service::AuthenticationService; use crate::databases::setup::initialize_database; - fn instantiate_keys_manager_and_authentication() -> (Arc<KeysHandler>, Arc<AuthenticationService>) { + async fn instantiate_keys_manager_and_authentication() -> (Arc<KeysHandler>, Arc<AuthenticationService>) { let config = configuration::ephemeral_private(); - instantiate_keys_manager_and_authentication_with_configuration(&config) + instantiate_keys_manager_and_authentication_with_configuration(&config).await } - fn instantiate_keys_manager_and_authentication_with_checking_keys_expiration_disabled( + async fn instantiate_keys_manager_and_authentication_with_checking_keys_expiration_disabled( ) -> (Arc<KeysHandler>, Arc<AuthenticationService>) { let mut config = configuration::ephemeral_private(); @@ -58,13 +58,13 @@ mod tests { check_keys_expiration: false, }); - instantiate_keys_manager_and_authentication_with_configuration(&config) + instantiate_keys_manager_and_authentication_with_configuration(&config).await } - fn instantiate_keys_manager_and_authentication_with_configuration( + async fn instantiate_keys_manager_and_authentication_with_configuration( config: &Configuration, ) -> (Arc<KeysHandler>, Arc<AuthenticationService>) { - let stores = initialize_database(&config.core); + let stores = initialize_database(&config.core).await; let db_key_repository = Arc::new(DatabaseKeyRepository::new(&stores.auth_key_store)); let in_memory_key_repository = Arc::new(InMemoryKeyRepository::default()); let authentication_service = Arc::new(service::AuthenticationService::new(&config.core, &in_memory_key_repository)); @@ -78,7 +78,7 @@ mod tests { #[tokio::test] async fn it_should_remove_an_authentication_key() { - let (keys_manager, authentication_service) = instantiate_keys_manager_and_authentication(); + let (keys_manager, authentication_service) = instantiate_keys_manager_and_authentication().await; let expiring_key = keys_manager .generate_expiring_peer_key(Some(Duration::from_secs(100))) @@ -95,7 +95,7 @@ mod tests { #[tokio::test] async fn it_should_load_authentication_keys_from_the_database() { - let (keys_manager, authentication_service) = instantiate_keys_manager_and_authentication(); + let (keys_manager, authentication_service) = instantiate_keys_manager_and_authentication().await; let expiring_key = keys_manager .generate_expiring_peer_key(Some(Duration::from_secs(100))) @@ -126,7 +126,7 @@ mod tests { #[tokio::test] async fn it_should_authenticate_a_peer_with_the_key() { - let (keys_manager, authentication_service) = instantiate_keys_manager_and_authentication(); + let (keys_manager, authentication_service) = instantiate_keys_manager_and_authentication().await; let peer_key = keys_manager .generate_expiring_peer_key(Some(Duration::from_secs(100))) @@ -141,7 +141,7 @@ mod tests { #[tokio::test] async fn it_should_accept_an_expired_key_when_checking_expiration_is_disabled_in_configuration() { let (keys_manager, authentication_service) = - instantiate_keys_manager_and_authentication_with_checking_keys_expiration_disabled(); + instantiate_keys_manager_and_authentication_with_checking_keys_expiration_disabled().await; let past_timestamp = Duration::ZERO; @@ -165,7 +165,7 @@ mod tests { #[tokio::test] async fn it_should_authenticate_a_peer_with_the_key() { - let (keys_manager, authentication_service) = instantiate_keys_manager_and_authentication(); + let (keys_manager, authentication_service) = instantiate_keys_manager_and_authentication().await; let peer_key = keys_manager .add_peer_key(AddKeyRequest { @@ -183,7 +183,7 @@ mod tests { #[tokio::test] async fn it_should_accept_an_expired_key_when_checking_expiration_is_disabled_in_configuration() { let (keys_manager, authentication_service) = - instantiate_keys_manager_and_authentication_with_checking_keys_expiration_disabled(); + instantiate_keys_manager_and_authentication_with_checking_keys_expiration_disabled().await; let peer_key = keys_manager .add_peer_key(AddKeyRequest { @@ -205,7 +205,7 @@ mod tests { #[tokio::test] async fn it_should_authenticate_a_peer_with_the_key() { - let (keys_manager, authentication_service) = instantiate_keys_manager_and_authentication(); + let (keys_manager, authentication_service) = instantiate_keys_manager_and_authentication().await; let peer_key = keys_manager.generate_permanent_peer_key().await.unwrap(); @@ -222,7 +222,7 @@ mod tests { #[tokio::test] async fn it_should_authenticate_a_peer_with_the_key() { - let (keys_manager, authentication_service) = instantiate_keys_manager_and_authentication(); + let (keys_manager, authentication_service) = instantiate_keys_manager_and_authentication().await; let peer_key = keys_manager .add_peer_key(AddKeyRequest { diff --git a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/mod.rs b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/mod.rs index 02462a365..083d735a4 100644 --- a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/mod.rs +++ b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/mod.rs @@ -33,7 +33,7 @@ impl ActiveDatabase { /// connection details. pub(super) async fn new(driver: Driver, db_version: &str) -> Result<Self> { match driver { - Driver::Sqlite3 => Ok(sqlite::initialize()), + Driver::Sqlite3 => Ok(sqlite::initialize().await), Driver::MySQL => mysql::initialize(db_version).await, } } @@ -60,6 +60,7 @@ pub(super) async fn reset_database(schema_migrator: &dyn SchemaMigrator) -> Resu create_database_tables_with_retry(schema_migrator).await?; schema_migrator .drop_database_tables() + .await .context("failed to drop benchmark database tables")?; create_database_tables_with_retry(schema_migrator).await } @@ -76,7 +77,7 @@ async fn create_database_tables_with_retry(schema_migrator: &dyn SchemaMigrator) let mut last_error: Option<anyhow::Error> = None; for _ in 0..5 { - match schema_migrator.create_database_tables() { + match schema_migrator.create_database_tables().await { Ok(()) => return Ok(()), Err(error) => { last_error = Some(error.into()); diff --git a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/mysql.rs b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/mysql.rs index 4bbc332c7..a07cce287 100644 --- a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/mysql.rs +++ b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/mysql.rs @@ -1,15 +1,34 @@ +use std::str::FromStr; +use std::time::Duration; + use anyhow::{Context, Result}; use bittorrent_tracker_core::databases::setup::initialize_database; -use testcontainers::core::IntoContainerPort; +use sqlx::mysql::{MySqlConnectOptions, MySqlPoolOptions}; +use testcontainers::core::wait::LogWaitStrategy; +use testcontainers::core::{IntoContainerPort, WaitFor}; use testcontainers::runners::AsyncRunner; use testcontainers::{GenericImage, ImageExt}; use torrust_tracker_configuration as configuration; use super::{ActiveDatabase, BenchmarkResource}; +/// Maximum number of connect-and-ping attempts after the container is reported +/// ready. Belt-and-braces against a brief race between the second +/// `ready for connections` log line and TCP acceptance on port 3306. +const READINESS_PING_RETRIES: usize = 30; +/// Delay between readiness-ping attempts. +const READINESS_PING_INTERVAL: Duration = Duration::from_millis(500); + pub(super) async fn initialize(db_version: &str) -> Result<ActiveDatabase> { + // The official `mysql` image emits `ready for connections` twice on stderr: + // first transiently during init on the unix socket, then again once mysqld + // is actually accepting TCP clients on port 3306. We wait for the second + // occurrence so the first query (DDL via `initialize_database`) does not + // race the TCP listener and panic with `UnexpectedEof`. This is the same + // idiom the Java testcontainers MySQL module uses internally. let mysql_container = GenericImage::new("mysql", db_version) .with_exposed_port(3306.tcp()) + .with_wait_for(WaitFor::Log(LogWaitStrategy::stderr("ready for connections").with_times(2))) .with_env_var("MYSQL_ROOT_PASSWORD", "test") .with_env_var("MYSQL_DATABASE", "torrust_tracker_bench") .with_env_var("MYSQL_ROOT_HOST", "%") @@ -27,13 +46,53 @@ pub(super) async fn initialize(db_version: &str) -> Result<ActiveDatabase> { .context("failed to resolve mysql container host port")?; let mysql_database_url = format!("mysql://root:test@{host}:{port}/torrust_tracker_bench"); + + // Belt-and-braces: even after the readiness log message, the very first TCP + // connect can still hit `UnexpectedEof` while mysqld finalises bind/accept. + // Probe with a short connect-and-ping loop so the production + // `initialize_database` call below sees a steady server. This mirrors what + // the previous r2d2-based driver did implicitly through pool checkout + // retries. + wait_until_mysql_accepts_connections(&mysql_database_url) + .await + .context("mysql container did not accept connections in time")?; + let mut config = configuration::Core::default(); config.database.driver = configuration::Driver::MySQL; config.database.path = mysql_database_url; - let database = initialize_database(&config); + let database = initialize_database(&config).await; Ok(ActiveDatabase { database: Some(database), resource: Some(BenchmarkResource::Mysql(Box::new(mysql_container))), }) } + +async fn wait_until_mysql_accepts_connections(database_url: &str) -> Result<()> { + let options = MySqlConnectOptions::from_str(database_url).context("invalid mysql benchmark URL")?; + + let mut last_error: Option<sqlx::Error> = None; + + for _ in 0..READINESS_PING_RETRIES { + match MySqlPoolOptions::new().max_connections(1).connect_with(options.clone()).await { + Ok(pool) => { + if let Err(error) = sqlx::query("SELECT 1").execute(&pool).await { + last_error = Some(error); + } else { + pool.close().await; + return Ok(()); + } + } + Err(error) => { + last_error = Some(error); + } + } + + tokio::time::sleep(READINESS_PING_INTERVAL).await; + } + + Err(anyhow::anyhow!( + "mysql still not accepting connections after {READINESS_PING_RETRIES} attempts; last error: {error}", + error = last_error.map_or_else(|| "<none>".to_string(), |e| e.to_string()) + )) +} diff --git a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/sqlite.rs b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/sqlite.rs index 1ffa06198..c0dba09b6 100644 --- a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/sqlite.rs +++ b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/database/sqlite.rs @@ -3,7 +3,7 @@ use torrust_tracker_configuration as configuration; use super::{ActiveDatabase, BenchmarkResource}; -pub(super) fn initialize() -> ActiveDatabase { +pub(super) async fn initialize() -> ActiveDatabase { let sqlite_db_path = std::env::temp_dir().join(format!( "torrust-tracker-core-benchmark-{}.sqlite3", chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default() @@ -13,7 +13,7 @@ pub(super) fn initialize() -> ActiveDatabase { config.database.driver = configuration::Driver::Sqlite3; config.database.path = sqlite_db_path_as_string; - let database = initialize_database(&config); + let database = initialize_database(&config).await; ActiveDatabase { database: Some(database), diff --git a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/mod.rs b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/mod.rs index 33805a20d..792a76767 100644 --- a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/mod.rs +++ b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/mod.rs @@ -29,9 +29,9 @@ pub async fn run(driver: Driver, db_version: &str, ops: OpsCount) -> Result<Vec< let ops = ops.get(); let mut operations_samples = Vec::new(); - operations::benchmark_torrent_operations(&*stores.torrent_metrics_store, ops, &mut operations_samples)?; - operations::benchmark_whitelist_operations(&*stores.whitelist_store, ops, &mut operations_samples)?; - operations::benchmark_key_operations(&*stores.auth_key_store, ops, &mut operations_samples)?; + operations::benchmark_torrent_operations(&*stores.torrent_metrics_store, ops, &mut operations_samples).await?; + operations::benchmark_whitelist_operations(&*stores.whitelist_store, ops, &mut operations_samples).await?; + operations::benchmark_key_operations(&*stores.auth_key_store, ops, &mut operations_samples).await?; Ok(operations_samples) } diff --git a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/keys.rs b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/keys.rs index 02ed709e8..6e548aa0a 100644 --- a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/keys.rs +++ b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/keys.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result}; use bittorrent_tracker_core::authentication; use bittorrent_tracker_core::databases::AuthKeyStore; -use super::super::sampling::measure_operation; +use super::super::sampling::measure_operation_async; use super::super::RawOperationSamples; /// Benchmarks authentication-key persistence operations. @@ -10,65 +10,86 @@ use super::super::RawOperationSamples; /// # Errors /// /// Returns an error if any setup or measured database operation fails. -pub(super) fn benchmark_key_operations( +pub(super) async fn benchmark_key_operations( database: &dyn AuthKeyStore, ops: usize, operations: &mut Vec<RawOperationSamples>, ) -> Result<()> { - operations.push(measure_operation( - "add_key_to_keys", - ops, - |_| Ok(authentication::key::generate_key(None)), - |peer_key| { - let _added_rows = database.add_key_to_keys(&peer_key).context("add_key_to_keys failed")?; - Ok(()) - }, - )?); + operations.push( + measure_operation_async( + "add_key_to_keys", + ops, + |_| async move { Ok(authentication::key::generate_key(None)) }, + |peer_key| async move { + let _added_rows = database.add_key_to_keys(&peer_key).await.context("add_key_to_keys failed")?; + Ok(()) + }, + ) + .await?, + ); let persisted_peer_key = authentication::key::generate_key(None); let _added_rows = database .add_key_to_keys(&persisted_peer_key) + .await .context("failed to seed get_key_from_keys")?; let persisted_key = persisted_peer_key.key(); - operations.push(measure_operation( - "get_key_from_keys", - ops, - |_| Ok(()), - |()| { - let persisted_key_result = database - .get_key_from_keys(&persisted_key) - .context("get_key_from_keys failed")?; - drop(persisted_key_result); - Ok(()) - }, - )?); + operations.push( + measure_operation_async( + "get_key_from_keys", + ops, + |_| async move { Ok(()) }, + |()| { + let persisted_key = persisted_key.clone(); + async move { + let persisted_key_result = database + .get_key_from_keys(&persisted_key) + .await + .context("get_key_from_keys failed")?; + drop(persisted_key_result); + Ok(()) + } + }, + ) + .await?, + ); - operations.push(measure_operation( - "load_keys", - ops, - |_| Ok(()), - |()| { - let keys = database.load_keys().context("load_keys failed")?; - drop(keys); - Ok(()) - }, - )?); + operations.push( + measure_operation_async( + "load_keys", + ops, + |_| async move { Ok(()) }, + |()| async move { + let keys = database.load_keys().await.context("load_keys failed")?; + drop(keys); + Ok(()) + }, + ) + .await?, + ); - operations.push(measure_operation( - "remove_key_from_keys", - ops, - |_| { - let peer_key = authentication::key::generate_key(None); - let _added_rows = database - .add_key_to_keys(&peer_key) - .context("failed to seed remove_key_from_keys")?; - Ok(peer_key.key()) - }, - |key| { - let _removed_rows = database.remove_key_from_keys(&key).context("remove_key_from_keys failed")?; - Ok(()) - }, - )?); + operations.push( + measure_operation_async( + "remove_key_from_keys", + ops, + |_| async move { + let peer_key = authentication::key::generate_key(None); + let _added_rows = database + .add_key_to_keys(&peer_key) + .await + .context("failed to seed remove_key_from_keys")?; + Ok(peer_key.key()) + }, + |key| async move { + let _removed_rows = database + .remove_key_from_keys(&key) + .await + .context("remove_key_from_keys failed")?; + Ok(()) + }, + ) + .await?, + ); Ok(()) } diff --git a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/mod.rs b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/mod.rs index 962806a46..1b169682b 100644 --- a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/mod.rs +++ b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/mod.rs @@ -7,26 +7,26 @@ use bittorrent_tracker_core::databases::{AuthKeyStore, TorrentMetricsStore, Whit use super::RawOperationSamples; -pub(super) fn benchmark_torrent_operations( +pub(super) async fn benchmark_torrent_operations( database: &dyn TorrentMetricsStore, ops: usize, operations: &mut Vec<RawOperationSamples>, ) -> Result<()> { - torrent::benchmark_torrent_operations(database, ops, operations) + torrent::benchmark_torrent_operations(database, ops, operations).await } -pub(super) fn benchmark_whitelist_operations( +pub(super) async fn benchmark_whitelist_operations( database: &dyn WhitelistStore, ops: usize, operations: &mut Vec<RawOperationSamples>, ) -> Result<()> { - whitelist::benchmark_whitelist_operations(database, ops, operations) + whitelist::benchmark_whitelist_operations(database, ops, operations).await } -pub(super) fn benchmark_key_operations( +pub(super) async fn benchmark_key_operations( database: &dyn AuthKeyStore, ops: usize, operations: &mut Vec<RawOperationSamples>, ) -> Result<()> { - keys::benchmark_key_operations(database, ops, operations) + keys::benchmark_key_operations(database, ops, operations).await } diff --git a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/torrent.rs b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/torrent.rs index 38b6152f4..7c71624a1 100644 --- a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/torrent.rs +++ b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/torrent.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use bittorrent_tracker_core::databases::TorrentMetricsStore; -use super::super::sampling::{downloads_from_index, info_hash_from_index, measure_operation}; +use super::super::sampling::{downloads_from_index, info_hash_from_index, measure_operation_async}; use super::super::RawOperationSamples; /// Benchmarks torrent statistics persistence operations. @@ -12,103 +12,205 @@ use super::super::RawOperationSamples; /// # Errors /// /// Returns an error if any setup or measured database operation fails. -pub(super) fn benchmark_torrent_operations( +pub(super) async fn benchmark_torrent_operations( database: &dyn TorrentMetricsStore, ops: usize, operations: &mut Vec<RawOperationSamples>, ) -> Result<()> { - operations.push(measure_operation( - "save_torrent_downloads", - ops, - |index| Ok((info_hash_from_index(index + 1)?, downloads_from_index(index)?)), - |(info_hash, downloads)| { - database - .save_torrent_downloads(&info_hash, downloads) - .context("save_torrent_downloads failed") - }, - )?); + benchmark_save_torrent_downloads(database, ops, operations).await?; + benchmark_load_torrent_downloads(database, ops, operations).await?; + benchmark_load_all_torrents_downloads(database, ops, operations).await?; + benchmark_increase_downloads_for_torrent(database, ops, operations).await?; + benchmark_save_global_downloads(database, ops, operations).await?; + benchmark_load_global_downloads(database, ops, operations).await?; + benchmark_increase_global_downloads(database, ops, operations).await?; + Ok(()) +} + +async fn benchmark_save_torrent_downloads( + database: &dyn TorrentMetricsStore, + ops: usize, + operations: &mut Vec<RawOperationSamples>, +) -> Result<()> { + operations.push( + measure_operation_async( + "save_torrent_downloads", + ops, + |index| async move { Ok((info_hash_from_index(index + 1)?, downloads_from_index(index)?)) }, + |(info_hash, downloads)| async move { + database + .save_torrent_downloads(&info_hash, downloads) + .await + .context("save_torrent_downloads failed") + }, + ) + .await?, + ); + + Ok(()) +} + +async fn benchmark_load_torrent_downloads( + database: &dyn TorrentMetricsStore, + ops: usize, + operations: &mut Vec<RawOperationSamples>, +) -> Result<()> { let load_torrent_info_hash = info_hash_from_index(10_000)?; database .save_torrent_downloads(&load_torrent_info_hash, 123) + .await .context("failed to seed load_torrent_downloads")?; - operations.push(measure_operation( - "load_torrent_downloads", - ops, - |_| Ok(()), - |()| { - let _downloads_result = database - .load_torrent_downloads(&load_torrent_info_hash) - .context("load_torrent_downloads failed")?; - Ok(()) - }, - )?); - - operations.push(measure_operation( - "load_all_torrents_downloads", - ops, - |_| Ok(()), - |()| { - let all_downloads = database - .load_all_torrents_downloads() - .context("load_all_torrents_downloads failed")?; - drop(all_downloads); - Ok(()) - }, - )?); + operations.push( + measure_operation_async( + "load_torrent_downloads", + ops, + |_| async move { Ok(()) }, + |()| async move { + let _downloads_result = database + .load_torrent_downloads(&load_torrent_info_hash) + .await + .context("load_torrent_downloads failed")?; + Ok(()) + }, + ) + .await?, + ); + + Ok(()) +} + +async fn benchmark_load_all_torrents_downloads( + database: &dyn TorrentMetricsStore, + ops: usize, + operations: &mut Vec<RawOperationSamples>, +) -> Result<()> { + operations.push( + measure_operation_async( + "load_all_torrents_downloads", + ops, + |_| async move { Ok(()) }, + |()| async move { + let all_downloads = database + .load_all_torrents_downloads() + .await + .context("load_all_torrents_downloads failed")?; + drop(all_downloads); + Ok(()) + }, + ) + .await?, + ); + + Ok(()) +} + +async fn benchmark_increase_downloads_for_torrent( + database: &dyn TorrentMetricsStore, + ops: usize, + operations: &mut Vec<RawOperationSamples>, +) -> Result<()> { let increasing_downloads_info_hash = info_hash_from_index(20_000)?; database .save_torrent_downloads(&increasing_downloads_info_hash, 0) + .await .context("failed to seed increase_downloads_for_torrent")?; - operations.push(measure_operation( - "increase_downloads_for_torrent", - ops, - |_| Ok(()), - |()| { - database - .increase_downloads_for_torrent(&increasing_downloads_info_hash) - .context("increase_downloads_for_torrent failed") - }, - )?); - - operations.push(measure_operation( - "save_global_downloads", - ops, - downloads_from_index, - |downloads| { - database - .save_global_downloads(downloads) - .context("save_global_downloads failed") - }, - )?); + operations.push( + measure_operation_async( + "increase_downloads_for_torrent", + ops, + |_| async move { Ok(()) }, + |()| async move { + database + .increase_downloads_for_torrent(&increasing_downloads_info_hash) + .await + .context("increase_downloads_for_torrent failed") + }, + ) + .await?, + ); + + Ok(()) +} + +async fn benchmark_save_global_downloads( + database: &dyn TorrentMetricsStore, + ops: usize, + operations: &mut Vec<RawOperationSamples>, +) -> Result<()> { + operations.push( + measure_operation_async( + "save_global_downloads", + ops, + |index| async move { downloads_from_index(index) }, + |downloads| async move { + database + .save_global_downloads(downloads) + .await + .context("save_global_downloads failed") + }, + ) + .await?, + ); + + Ok(()) +} + +async fn benchmark_load_global_downloads( + database: &dyn TorrentMetricsStore, + ops: usize, + operations: &mut Vec<RawOperationSamples>, +) -> Result<()> { database .save_global_downloads(0) + .await .context("failed to seed load_global_downloads")?; - operations.push(measure_operation( - "load_global_downloads", - ops, - |_| Ok(()), - |()| { - let _downloads_result = database.load_global_downloads().context("load_global_downloads failed")?; - Ok(()) - }, - )?); + operations.push( + measure_operation_async( + "load_global_downloads", + ops, + |_| async move { Ok(()) }, + |()| async move { + let _downloads_result = database + .load_global_downloads() + .await + .context("load_global_downloads failed")?; + Ok(()) + }, + ) + .await?, + ); + + Ok(()) +} + +async fn benchmark_increase_global_downloads( + database: &dyn TorrentMetricsStore, + ops: usize, + operations: &mut Vec<RawOperationSamples>, +) -> Result<()> { database .save_global_downloads(0) + .await .context("failed to seed increase_global_downloads")?; - operations.push(measure_operation( - "increase_global_downloads", - ops, - |_| Ok(()), - |()| { - database - .increase_global_downloads() - .context("increase_global_downloads failed") - }, - )?); + + operations.push( + measure_operation_async( + "increase_global_downloads", + ops, + |_| async move { Ok(()) }, + |()| async move { + database + .increase_global_downloads() + .await + .context("increase_global_downloads failed") + }, + ) + .await?, + ); Ok(()) } diff --git a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/whitelist.rs b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/whitelist.rs index 44e77d3a5..bd9b780be 100644 --- a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/whitelist.rs +++ b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/operations/whitelist.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Result}; use bittorrent_tracker_core::databases::WhitelistStore; -use super::super::sampling::{info_hash_from_index, measure_operation}; +use super::super::sampling::{info_hash_from_index, measure_operation_async}; use super::super::RawOperationSamples; /// Benchmarks whitelist-related persistence operations. @@ -9,67 +9,84 @@ use super::super::RawOperationSamples; /// # Errors /// /// Returns an error if any setup or measured database operation fails. -pub(super) fn benchmark_whitelist_operations( +pub(super) async fn benchmark_whitelist_operations( database: &dyn WhitelistStore, ops: usize, operations: &mut Vec<RawOperationSamples>, ) -> Result<()> { - operations.push(measure_operation( - "add_info_hash_to_whitelist", - ops, - |index| info_hash_from_index(30_000 + index), - |info_hash| { - let _added_rows = database - .add_info_hash_to_whitelist(info_hash) - .context("add_info_hash_to_whitelist failed")?; - Ok(()) - }, - )?); + operations.push( + measure_operation_async( + "add_info_hash_to_whitelist", + ops, + |index| async move { info_hash_from_index(30_000 + index) }, + |info_hash| async move { + let _added_rows = database + .add_info_hash_to_whitelist(info_hash) + .await + .context("add_info_hash_to_whitelist failed")?; + Ok(()) + }, + ) + .await?, + ); let whitelisted_info_hash = info_hash_from_index(40_000)?; let _added_rows = database .add_info_hash_to_whitelist(whitelisted_info_hash) + .await .context("failed to seed get_info_hash_from_whitelist")?; - operations.push(measure_operation( - "get_info_hash_from_whitelist", - ops, - |_| Ok(()), - |()| { - let _info_hash_result = database - .get_info_hash_from_whitelist(whitelisted_info_hash) - .context("get_info_hash_from_whitelist failed")?; - Ok(()) - }, - )?); + operations.push( + measure_operation_async( + "get_info_hash_from_whitelist", + ops, + |_| async move { Ok(()) }, + |()| async move { + let _info_hash_result = database + .get_info_hash_from_whitelist(whitelisted_info_hash) + .await + .context("get_info_hash_from_whitelist failed")?; + Ok(()) + }, + ) + .await?, + ); - operations.push(measure_operation( - "load_whitelist", - ops, - |_| Ok(()), - |()| { - let whitelist = database.load_whitelist().context("load_whitelist failed")?; - drop(whitelist); - Ok(()) - }, - )?); + operations.push( + measure_operation_async( + "load_whitelist", + ops, + |_| async move { Ok(()) }, + |()| async move { + let whitelist = database.load_whitelist().await.context("load_whitelist failed")?; + drop(whitelist); + Ok(()) + }, + ) + .await?, + ); - operations.push(measure_operation( - "remove_info_hash_from_whitelist", - ops, - |index| { - let info_hash = info_hash_from_index(50_000 + index)?; - let _added_rows = database - .add_info_hash_to_whitelist(info_hash) - .context("failed to seed remove_info_hash_from_whitelist")?; - Ok(info_hash) - }, - |info_hash| { - let _removed_rows = database - .remove_info_hash_from_whitelist(info_hash) - .context("remove_info_hash_from_whitelist failed")?; - Ok(()) - }, - )?); + operations.push( + measure_operation_async( + "remove_info_hash_from_whitelist", + ops, + |index| async move { + let info_hash = info_hash_from_index(50_000 + index)?; + let _added_rows = database + .add_info_hash_to_whitelist(info_hash) + .await + .context("failed to seed remove_info_hash_from_whitelist")?; + Ok(info_hash) + }, + |info_hash| async move { + let _removed_rows = database + .remove_info_hash_from_whitelist(info_hash) + .await + .context("remove_info_hash_from_whitelist failed")?; + Ok(()) + }, + ) + .await?, + ); Ok(()) } diff --git a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/sampling.rs b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/sampling.rs index 1f39eb853..a0daf9b00 100644 --- a/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/sampling.rs +++ b/packages/tracker-core/src/bin/persistence_benchmark/driver_bench/sampling.rs @@ -6,31 +6,31 @@ use bittorrent_primitives::info_hash::InfoHash; use super::RawOperationSamples; -/// Measures one database operation `ops` times and records elapsed samples. -/// -/// Per-iteration fixture generation is performed by `setup` before timing -/// starts, so the recorded durations reflect only the database operation. +/// Async variant of operation measurement, for database operations requiring +/// `.await`. /// /// # Errors /// -/// Returns an error if setup or any operation invocation fails. -pub(super) fn measure_operation<S, F, T>( +/// Returns an error if setup or any async operation invocation fails. +pub(super) async fn measure_operation_async<S, SetupFut, F, T, OpFut>( name: impl Into<String>, ops: usize, mut setup: S, mut operation: F, ) -> Result<RawOperationSamples> where - S: FnMut(usize) -> Result<T>, - F: FnMut(T) -> Result<()>, + S: FnMut(usize) -> SetupFut, + SetupFut: std::future::Future<Output = Result<T>>, + F: FnMut(T) -> OpFut, + OpFut: std::future::Future<Output = Result<()>>, { let name = name.into(); let mut samples = Vec::with_capacity(ops); for index in 0..ops { - let prepared = setup(index)?; + let prepared = setup(index).await?; let start = Instant::now(); - operation(prepared)?; + operation(prepared).await?; samples.push(start.elapsed()); } diff --git a/packages/tracker-core/src/container.rs b/packages/tracker-core/src/container.rs index e849b723f..e52547c28 100644 --- a/packages/tracker-core/src/container.rs +++ b/packages/tracker-core/src/container.rs @@ -37,11 +37,11 @@ pub struct TrackerCoreContainer { impl TrackerCoreContainer { #[must_use] - pub fn initialize_from( + pub async fn initialize_from( core_config: &Arc<Core>, swarm_coordination_registry_container: &Arc<SwarmCoordinationRegistryContainer>, ) -> Self { - let db = initialize_database(core_config); + let db = initialize_database(core_config).await; let in_memory_whitelist = Arc::new(InMemoryWhitelist::default()); let whitelist_authorization = Arc::new(WhitelistAuthorization::new(core_config, &in_memory_whitelist.clone())); let whitelist_manager = initialize_whitelist_manager(db.whitelist_store.clone(), in_memory_whitelist.clone()); diff --git a/packages/tracker-core/src/databases/driver/mod.rs b/packages/tracker-core/src/databases/driver/mod.rs index bc84eef9c..147275f30 100644 --- a/packages/tracker-core/src/databases/driver/mod.rs +++ b/packages/tracker-core/src/databases/driver/mod.rs @@ -58,54 +58,33 @@ pub(crate) mod tests { use crate::databases::traits::Database; pub async fn run_tests(driver: &Arc<Box<dyn Database>>) { - // Since the interface is very simple and there are no conflicts between - // tests, we share the same database. If we want to isolate the tests in - // the future, we can create a new database for each test. - database_setup(driver).await; - // Persistent torrents (stats) - - // Torrent metrics - handling_torrent_persistence::it_should_save_and_load_persistent_torrents(driver); - handling_torrent_persistence::it_should_load_all_persistent_torrents(driver); - handling_torrent_persistence::it_should_increase_the_number_of_downloads_for_a_given_torrent(driver); - // Aggregate metrics for all torrents - handling_torrent_persistence::it_should_save_and_load_the_global_number_of_downloads(driver); - handling_torrent_persistence::it_should_load_the_global_number_of_downloads(driver); - handling_torrent_persistence::it_should_increase_the_global_number_of_downloads(driver); - - // Authentication keys (for private trackers) - - handling_authentication_keys::it_should_load_the_keys(driver); - - // Permanent keys - handling_authentication_keys::it_should_save_and_load_permanent_authentication_keys(driver); - handling_authentication_keys::it_should_remove_a_permanent_authentication_key(driver); - - // Expiring keys - handling_authentication_keys::it_should_save_and_load_expiring_authentication_keys(driver); - handling_authentication_keys::it_should_remove_an_expiring_authentication_key(driver); - - // Whitelist (for listed trackers) - - handling_the_whitelist::it_should_load_the_whitelist(driver); - handling_the_whitelist::it_should_add_and_get_infohashes(driver); - handling_the_whitelist::it_should_remove_an_infohash_from_the_whitelist(driver); - handling_the_whitelist::it_should_fail_trying_to_add_the_same_infohash_twice(driver); + handling_torrent_persistence::it_should_save_and_load_persistent_torrents(driver).await; + handling_torrent_persistence::it_should_load_all_persistent_torrents(driver).await; + handling_torrent_persistence::it_should_increase_the_number_of_downloads_for_a_given_torrent(driver).await; + handling_torrent_persistence::it_should_save_and_load_the_global_number_of_downloads(driver).await; + handling_torrent_persistence::it_should_load_the_global_number_of_downloads(driver).await; + handling_torrent_persistence::it_should_increase_the_global_number_of_downloads(driver).await; + + handling_authentication_keys::it_should_load_the_keys(driver).await; + handling_authentication_keys::it_should_save_and_load_permanent_authentication_keys(driver).await; + handling_authentication_keys::it_should_remove_a_permanent_authentication_key(driver).await; + handling_authentication_keys::it_should_save_and_load_expiring_authentication_keys(driver).await; + handling_authentication_keys::it_should_remove_an_expiring_authentication_key(driver).await; + + handling_the_whitelist::it_should_load_the_whitelist(driver).await; + handling_the_whitelist::it_should_add_and_get_infohashes(driver).await; + handling_the_whitelist::it_should_remove_an_infohash_from_the_whitelist(driver).await; + handling_the_whitelist::it_should_fail_trying_to_add_the_same_infohash_twice(driver).await; } - /// It initializes the database schema. - /// - /// Since the drop SQL queries don't check if the tables already exist, - /// we have to create them first, and then drop them. - /// - /// The method to drop tables does not use "DROP TABLE IF EXISTS". We can - /// change this function when we update the `Database::drop_database_tables` - /// method to use "DROP TABLE IF EXISTS". async fn database_setup(driver: &Arc<Box<dyn Database>>) { create_database_tables(driver).await.expect("database tables creation failed"); - driver.drop_database_tables().expect("old database tables deletion failed"); + driver + .drop_database_tables() + .await + .expect("old database tables deletion failed"); create_database_tables(driver) .await .expect("database tables creation from empty schema failed"); @@ -113,7 +92,7 @@ pub(crate) mod tests { async fn create_database_tables(driver: &Arc<Box<dyn Database>>) -> Result<(), Box<dyn std::error::Error>> { for _ in 0..5 { - if driver.create_database_tables().is_ok() { + if driver.create_database_tables().await.is_ok() { return Ok(()); } tokio::time::sleep(Duration::from_secs(2)).await; @@ -130,75 +109,75 @@ pub(crate) mod tests { // Metrics per torrent - pub fn it_should_save_and_load_persistent_torrents(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_save_and_load_persistent_torrents(driver: &Arc<Box<dyn Database>>) { let infohash = sample_info_hash(); let number_of_downloads = 1; - driver.save_torrent_downloads(&infohash, number_of_downloads).unwrap(); + driver.save_torrent_downloads(&infohash, number_of_downloads).await.unwrap(); - let number_of_downloads = driver.load_torrent_downloads(&infohash).unwrap().unwrap(); + let number_of_downloads = driver.load_torrent_downloads(&infohash).await.unwrap().unwrap(); assert_eq!(number_of_downloads, 1); } - pub fn it_should_load_all_persistent_torrents(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_load_all_persistent_torrents(driver: &Arc<Box<dyn Database>>) { let infohash = sample_info_hash(); let number_of_downloads = 1; - driver.save_torrent_downloads(&infohash, number_of_downloads).unwrap(); + driver.save_torrent_downloads(&infohash, number_of_downloads).await.unwrap(); - let torrents = driver.load_all_torrents_downloads().unwrap(); + let torrents = driver.load_all_torrents_downloads().await.unwrap(); assert_eq!(torrents.len(), 1); assert_eq!(torrents.get(&infohash), Some(number_of_downloads).as_ref()); } - pub fn it_should_increase_the_number_of_downloads_for_a_given_torrent(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_increase_the_number_of_downloads_for_a_given_torrent(driver: &Arc<Box<dyn Database>>) { let infohash = sample_info_hash(); let number_of_downloads = 1; - driver.save_torrent_downloads(&infohash, number_of_downloads).unwrap(); + driver.save_torrent_downloads(&infohash, number_of_downloads).await.unwrap(); - driver.increase_downloads_for_torrent(&infohash).unwrap(); + driver.increase_downloads_for_torrent(&infohash).await.unwrap(); - let number_of_downloads = driver.load_torrent_downloads(&infohash).unwrap().unwrap(); + let number_of_downloads = driver.load_torrent_downloads(&infohash).await.unwrap().unwrap(); assert_eq!(number_of_downloads, 2); } // Aggregate metrics for all torrents - pub fn it_should_save_and_load_the_global_number_of_downloads(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_save_and_load_the_global_number_of_downloads(driver: &Arc<Box<dyn Database>>) { let number_of_downloads = 1; - driver.save_global_downloads(number_of_downloads).unwrap(); + driver.save_global_downloads(number_of_downloads).await.unwrap(); - let number_of_downloads = driver.load_global_downloads().unwrap().unwrap(); + let number_of_downloads = driver.load_global_downloads().await.unwrap().unwrap(); assert_eq!(number_of_downloads, 1); } - pub fn it_should_load_the_global_number_of_downloads(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_load_the_global_number_of_downloads(driver: &Arc<Box<dyn Database>>) { let number_of_downloads = 1; - driver.save_global_downloads(number_of_downloads).unwrap(); + driver.save_global_downloads(number_of_downloads).await.unwrap(); - let number_of_downloads = driver.load_global_downloads().unwrap().unwrap(); + let number_of_downloads = driver.load_global_downloads().await.unwrap().unwrap(); assert_eq!(number_of_downloads, 1); } - pub fn it_should_increase_the_global_number_of_downloads(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_increase_the_global_number_of_downloads(driver: &Arc<Box<dyn Database>>) { let number_of_downloads = 1; - driver.save_global_downloads(number_of_downloads).unwrap(); + driver.save_global_downloads(number_of_downloads).await.unwrap(); - driver.increase_global_downloads().unwrap(); + driver.increase_global_downloads().await.unwrap(); - let number_of_downloads = driver.load_global_downloads().unwrap().unwrap(); + let number_of_downloads = driver.load_global_downloads().await.unwrap().unwrap(); assert_eq!(number_of_downloads, 2); } @@ -212,54 +191,54 @@ pub(crate) mod tests { use crate::authentication::key::{generate_expiring_key, generate_permanent_key}; use crate::databases::traits::Database; - pub fn it_should_load_the_keys(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_load_the_keys(driver: &Arc<Box<dyn Database>>) { let permanent_peer_key = generate_permanent_key(); - driver.add_key_to_keys(&permanent_peer_key).unwrap(); + driver.add_key_to_keys(&permanent_peer_key).await.unwrap(); let expiring_peer_key = generate_expiring_key(Duration::from_secs(120)); - driver.add_key_to_keys(&expiring_peer_key).unwrap(); + driver.add_key_to_keys(&expiring_peer_key).await.unwrap(); - let keys = driver.load_keys().unwrap(); + let keys = driver.load_keys().await.unwrap(); assert!(keys.contains(&permanent_peer_key)); assert!(keys.contains(&expiring_peer_key)); } - pub fn it_should_save_and_load_permanent_authentication_keys(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_save_and_load_permanent_authentication_keys(driver: &Arc<Box<dyn Database>>) { let peer_key = generate_permanent_key(); - driver.add_key_to_keys(&peer_key).unwrap(); + driver.add_key_to_keys(&peer_key).await.unwrap(); - let stored_peer_key = driver.get_key_from_keys(&peer_key.key()).unwrap().unwrap(); + let stored_peer_key = driver.get_key_from_keys(&peer_key.key()).await.unwrap().unwrap(); assert_eq!(stored_peer_key, peer_key); } - pub fn it_should_save_and_load_expiring_authentication_keys(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_save_and_load_expiring_authentication_keys(driver: &Arc<Box<dyn Database>>) { let peer_key = generate_expiring_key(Duration::from_secs(120)); - driver.add_key_to_keys(&peer_key).unwrap(); + driver.add_key_to_keys(&peer_key).await.unwrap(); - let stored_peer_key = driver.get_key_from_keys(&peer_key.key()).unwrap().unwrap(); + let stored_peer_key = driver.get_key_from_keys(&peer_key.key()).await.unwrap().unwrap(); assert_eq!(stored_peer_key, peer_key); assert_eq!(stored_peer_key.expiry_time(), peer_key.expiry_time()); } - pub fn it_should_remove_a_permanent_authentication_key(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_remove_a_permanent_authentication_key(driver: &Arc<Box<dyn Database>>) { let peer_key = generate_permanent_key(); - driver.add_key_to_keys(&peer_key).unwrap(); + driver.add_key_to_keys(&peer_key).await.unwrap(); - driver.remove_key_from_keys(&peer_key.key()).unwrap(); + driver.remove_key_from_keys(&peer_key.key()).await.unwrap(); - assert!(driver.get_key_from_keys(&peer_key.key()).unwrap().is_none()); + assert!(driver.get_key_from_keys(&peer_key.key()).await.unwrap().is_none()); } - pub fn it_should_remove_an_expiring_authentication_key(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_remove_an_expiring_authentication_key(driver: &Arc<Box<dyn Database>>) { let peer_key = generate_expiring_key(Duration::from_secs(120)); - driver.add_key_to_keys(&peer_key).unwrap(); + driver.add_key_to_keys(&peer_key).await.unwrap(); - driver.remove_key_from_keys(&peer_key.key()).unwrap(); + driver.remove_key_from_keys(&peer_key.key()).await.unwrap(); - assert!(driver.get_key_from_keys(&peer_key.key()).unwrap().is_none()); + assert!(driver.get_key_from_keys(&peer_key.key()).await.unwrap().is_none()); } } @@ -270,39 +249,39 @@ pub(crate) mod tests { use crate::databases::traits::Database; use crate::test_helpers::tests::random_info_hash; - pub fn it_should_load_the_whitelist(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_load_the_whitelist(driver: &Arc<Box<dyn Database>>) { let infohash = random_info_hash(); - driver.add_info_hash_to_whitelist(infohash).unwrap(); + driver.add_info_hash_to_whitelist(infohash).await.unwrap(); - let whitelist = driver.load_whitelist().unwrap(); + let whitelist = driver.load_whitelist().await.unwrap(); assert!(whitelist.contains(&infohash)); } - pub fn it_should_add_and_get_infohashes(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_add_and_get_infohashes(driver: &Arc<Box<dyn Database>>) { let infohash = random_info_hash(); - driver.add_info_hash_to_whitelist(infohash).unwrap(); + driver.add_info_hash_to_whitelist(infohash).await.unwrap(); - let stored_infohash = driver.get_info_hash_from_whitelist(infohash).unwrap().unwrap(); + let stored_infohash = driver.get_info_hash_from_whitelist(infohash).await.unwrap().unwrap(); assert_eq!(stored_infohash, infohash); } - pub fn it_should_remove_an_infohash_from_the_whitelist(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_remove_an_infohash_from_the_whitelist(driver: &Arc<Box<dyn Database>>) { let infohash = random_info_hash(); - driver.add_info_hash_to_whitelist(infohash).unwrap(); + driver.add_info_hash_to_whitelist(infohash).await.unwrap(); - driver.remove_info_hash_from_whitelist(infohash).unwrap(); + driver.remove_info_hash_from_whitelist(infohash).await.unwrap(); - assert!(driver.get_info_hash_from_whitelist(infohash).unwrap().is_none()); + assert!(driver.get_info_hash_from_whitelist(infohash).await.unwrap().is_none()); } - pub fn it_should_fail_trying_to_add_the_same_infohash_twice(driver: &Arc<Box<dyn Database>>) { + pub async fn it_should_fail_trying_to_add_the_same_infohash_twice(driver: &Arc<Box<dyn Database>>) { let infohash = random_info_hash(); - driver.add_info_hash_to_whitelist(infohash).unwrap(); - let result = driver.add_info_hash_to_whitelist(infohash); + driver.add_info_hash_to_whitelist(infohash).await.unwrap(); + let result = driver.add_info_hash_to_whitelist(infohash).await; assert!(result.is_err()); } diff --git a/packages/tracker-core/src/databases/driver/mysql/auth_key_store.rs b/packages/tracker-core/src/databases/driver/mysql/auth_key_store.rs index b9b207e86..6029855c2 100644 --- a/packages/tracker-core/src/databases/driver/mysql/auth_key_store.rs +++ b/packages/tracker-core/src/databases/driver/mysql/auth_key_store.rs @@ -1,90 +1,125 @@ -use std::time::Duration; - -use r2d2_mysql::mysql::params; -use r2d2_mysql::mysql::prelude::Queryable; +use ::sqlx::Row; +use async_trait::async_trait; +use torrust_tracker_primitives::DurationSinceUnixEpoch; use super::{Mysql, DRIVER}; use crate::authentication::{self, Key}; use crate::databases::error::Error; use crate::databases::AuthKeyStore; +#[async_trait] impl AuthKeyStore for Mysql { - fn load_keys(&self) -> Result<Vec<authentication::PeerKey>, Error> { - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let raw: Vec<(String, Option<i64>)> = conn.query_map( - "SELECT `key`, valid_until FROM `keys`", - |(key, valid_until): (String, Option<i64>)| (key, valid_until), - )?; - - raw.into_iter() - .map(|(key, valid_until)| { - let key = key.parse::<Key>().map_err(|e| Error::MalformedDatabaseRecord { + async fn load_keys(&self) -> Result<Vec<authentication::PeerKey>, Error> { + let rows = ::sqlx::query("SELECT `key`, valid_until FROM `keys`") + .fetch_all(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + + rows.into_iter() + .map(|row| { + let key_value: String = row.try_get("key").map_err(|e| (e, DRIVER))?; + let valid_until: Option<i64> = row.try_get("valid_until").map_err(|e| (e, DRIVER))?; + + let parsed_key = key_value.parse::<Key>().map_err(|e| Error::MalformedDatabaseRecord { message: e.to_string(), driver: DRIVER, })?; - Ok(match valid_until { - Some(valid_until) => authentication::PeerKey { - key, - valid_until: Some(Duration::from_secs(valid_until.unsigned_abs())), - }, - None => authentication::PeerKey { key, valid_until: None }, + + Ok(authentication::PeerKey { + key: parsed_key, + valid_until: valid_until.map(parse_valid_until).transpose()?, }) }) .collect() } - fn get_key_from_keys(&self, key: &Key) -> Result<Option<authentication::PeerKey>, Error> { - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let query = conn.exec_first::<(String, Option<i64>), _, _>( - "SELECT `key`, valid_until FROM `keys` WHERE `key` = :key", - params! { "key" => key.to_string() }, - ); + async fn get_key_from_keys(&self, key: &Key) -> Result<Option<authentication::PeerKey>, Error> { + let maybe_row = ::sqlx::query("SELECT `key`, valid_until FROM `keys` WHERE `key` = ?") + .bind(key.to_string()) + .fetch_optional(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; - let key = query?; + maybe_row + .map(|row| { + let key_value: String = row.try_get("key").map_err(|e| (e, DRIVER))?; + let valid_until: Option<i64> = row.try_get("valid_until").map_err(|e| (e, DRIVER))?; - let peer_key = key - .map(|(key, opt_valid_until)| -> Result<authentication::PeerKey, Error> { - let key = key.parse::<Key>().map_err(|e| Error::MalformedDatabaseRecord { + let parsed_key = key_value.parse::<Key>().map_err(|e| Error::MalformedDatabaseRecord { message: e.to_string(), driver: DRIVER, })?; - Ok(match opt_valid_until { - Some(valid_until) => authentication::PeerKey { - key, - valid_until: Some(Duration::from_secs(valid_until.unsigned_abs())), - }, - None => authentication::PeerKey { key, valid_until: None }, + + Ok(authentication::PeerKey { + key: parsed_key, + valid_until: valid_until.map(parse_valid_until).transpose()?, }) }) - .transpose()?; - - Ok(peer_key) + .transpose() } - fn add_key_to_keys(&self, auth_key: &authentication::PeerKey) -> Result<usize, Error> { - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; + async fn add_key_to_keys(&self, auth_key: &authentication::PeerKey) -> Result<usize, Error> { + let valid_until = auth_key + .valid_until + .map(|value| { + i64::try_from(value.as_secs()).map_err(|e| Error::MalformedDatabaseRecord { + message: e.to_string(), + driver: DRIVER, + }) + }) + .transpose()?; - match auth_key.valid_until { - Some(valid_until) => conn.exec_drop( - "INSERT INTO `keys` (`key`, valid_until) VALUES (:key, :valid_until)", - params! { "key" => auth_key.key.to_string(), "valid_until" => valid_until.as_secs().to_string() }, - )?, - None => conn.exec_drop( - "INSERT INTO `keys` (`key`) VALUES (:key)", - params! { "key" => auth_key.key.to_string() }, - )?, + let insert = ::sqlx::query("INSERT INTO `keys` (`key`, valid_until) VALUES (?, ?)") + .bind(auth_key.key.to_string()) + .bind(valid_until) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))? + .rows_affected(); + + if insert == 0 { + Err(Error::InsertFailed { + location: std::panic::Location::caller(), + driver: DRIVER, + }) + } else { + usize::try_from(insert).map_err(|e| Error::MalformedDatabaseRecord { + message: format!("rows_affected does not fit in usize: {e}"), + driver: DRIVER, + }) } - - Ok(1) } - fn remove_key_from_keys(&self, key: &Key) -> Result<usize, Error> { - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - conn.exec_drop("DELETE FROM `keys` WHERE `key` = :key", params! { "key" => key.to_string() })?; - - Ok(1) + async fn remove_key_from_keys(&self, key: &Key) -> Result<usize, Error> { + let deleted = ::sqlx::query("DELETE FROM `keys` WHERE `key` = ?") + .bind(key.to_string()) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))? + .rows_affected(); + + if deleted == 1 { + Ok(1) + } else { + Err(Error::DeleteFailed { + location: std::panic::Location::caller(), + error_code: usize::try_from(deleted).unwrap_or(0), + driver: DRIVER, + }) + } } } + +/// Convert a signed seconds value loaded from the database into a +/// [`DurationSinceUnixEpoch`]. +/// +/// Negative values indicate a corrupted record (timestamps before the Unix +/// epoch are not representable) and are rejected as +/// [`Error::MalformedDatabaseRecord`]. +fn parse_valid_until(value: i64) -> Result<DurationSinceUnixEpoch, Error> { + let secs = u64::try_from(value).map_err(|_| Error::MalformedDatabaseRecord { + message: format!("negative valid_until timestamp: {value}"), + driver: DRIVER, + })?; + Ok(DurationSinceUnixEpoch::from_secs(secs)) +} diff --git a/packages/tracker-core/src/databases/driver/mysql/mod.rs b/packages/tracker-core/src/databases/driver/mysql/mod.rs index c776e959f..545754e5f 100644 --- a/packages/tracker-core/src/databases/driver/mysql/mod.rs +++ b/packages/tracker-core/src/databases/driver/mysql/mod.rs @@ -1,17 +1,8 @@ //! The `MySQL` database driver. -//! -//! This module provides implementations of the four narrow database traits -//! ([`SchemaMigrator`](crate::databases::SchemaMigrator), -//! [`TorrentMetricsStore`](crate::databases::TorrentMetricsStore), -//! [`WhitelistStore`](crate::databases::WhitelistStore), -//! [`AuthKeyStore`](crate::databases::AuthKeyStore) -//! for `MySQL` using the `r2d2_mysql` connection pool. It configures the `MySQL` -//! connection based on a URL, creates the necessary tables (for torrent metrics, -//! torrent whitelist, and authentication keys), and implements all CRUD -//! operations required by the persistence layer. -use r2d2::Pool; -use r2d2_mysql::mysql::{Opts, OptsBuilder}; -use r2d2_mysql::MySqlConnectionManager; +use std::str::FromStr; + +use ::sqlx::mysql::{MySqlConnectOptions, MySqlPoolOptions}; +use ::sqlx::{MySqlPool, Row}; use torrust_tracker_primitives::NumberOfDownloads; use super::{Driver, Error}; @@ -25,54 +16,54 @@ const DRIVER: Driver = Driver::MySQL; /// `MySQL` driver implementation. /// -/// This struct encapsulates a connection pool for `MySQL`, built using the -/// `r2d2_mysql` connection manager. It implements the [`Database`] trait to -/// provide persistence operations. +/// This struct encapsulates an async `sqlx` connection pool for `MySQL`. +/// It implements the [`Database`] trait to provide persistence operations. pub(crate) struct Mysql { - pool: Pool<MySqlConnectionManager>, + pool: MySqlPool, } impl Mysql { - /// It instantiates a new `MySQL` database driver. - /// - /// - /// # Errors - /// - /// Will return `r2d2::Error` if `db_path` is not able to create `MySQL` database. pub fn new(db_path: &str) -> Result<Self, Error> { - let opts = Opts::from_url(db_path)?; - let builder = OptsBuilder::from_opts(opts); - let manager = MySqlConnectionManager::new(builder); - let pool = r2d2::Pool::builder().build(manager).map_err(|e| (e, DRIVER))?; + let options = MySqlConnectOptions::from_str(db_path).map_err(|e| (e, DRIVER))?; + + let pool = MySqlPoolOptions::new().connect_lazy_with(options); Ok(Self { pool }) } - fn load_torrent_aggregate_metric(&self, metric_name: &str) -> Result<Option<NumberOfDownloads>, Error> { - use r2d2_mysql::mysql::params; - use r2d2_mysql::mysql::prelude::Queryable; - - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let query = conn.exec_first::<u32, _, _>( - "SELECT value FROM torrent_aggregate_metrics WHERE metric_name = :metric_name", - params! { "metric_name" => metric_name }, - ); - - let persistent_torrent = query?; - - Ok(persistent_torrent) + async fn load_torrent_aggregate_metric(&self, metric_name: &str) -> Result<Option<NumberOfDownloads>, Error> { + let maybe_row = ::sqlx::query("SELECT value FROM torrent_aggregate_metrics WHERE metric_name = ?") + .bind(metric_name) + .fetch_optional(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + + maybe_row + .map(|row| { + let value: i64 = row.try_get("value").map_err(|e| (e, DRIVER))?; + u32::try_from(value).map_err(|e| Error::MalformedDatabaseRecord { + message: e.to_string(), + driver: DRIVER, + }) + }) + .transpose() } - fn save_torrent_aggregate_metric(&self, metric_name: &str, completed: NumberOfDownloads) -> Result<(), Error> { - use r2d2_mysql::mysql::params; - use r2d2_mysql::mysql::prelude::Queryable; - - const COMMAND : &str = "INSERT INTO torrent_aggregate_metrics (metric_name, value) VALUES (:metric_name, :completed) ON DUPLICATE KEY UPDATE value = VALUES(value)"; + async fn save_torrent_aggregate_metric(&self, metric_name: &str, completed: NumberOfDownloads) -> Result<(), Error> { + // `ON DUPLICATE KEY UPDATE` may legitimately report `rows_affected() == 0` + // when the row already exists with the same value (no-op update), so we + // do not treat 0 as a failure here. A real failure surfaces as `Err` + // from `execute()`. + ::sqlx::query( + "INSERT INTO torrent_aggregate_metrics (metric_name, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value)", + ) + .bind(metric_name) + .bind(i64::from(completed)) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - Ok(conn.exec_drop(COMMAND, params! { metric_name, completed })?) + Ok(()) } } @@ -184,8 +175,7 @@ mod tests { } fn initialize_driver(config: &Core) -> Arc<Box<dyn Database>> { - let driver: Arc<Box<dyn Database>> = Arc::new(Box::new(Mysql::new(&config.database.path).unwrap())); - driver + Arc::new(Box::new(Mysql::new(&config.database.path).unwrap())) } // This test is invoked by `.github/workflows/testing.yaml` in the diff --git a/packages/tracker-core/src/databases/driver/mysql/schema_migrator.rs b/packages/tracker-core/src/databases/driver/mysql/schema_migrator.rs index 747ff6e47..a72b3feb6 100644 --- a/packages/tracker-core/src/databases/driver/mysql/schema_migrator.rs +++ b/packages/tracker-core/src/databases/driver/mysql/schema_migrator.rs @@ -1,34 +1,32 @@ -use r2d2_mysql::mysql::prelude::Queryable; +use async_trait::async_trait; use super::{Mysql, DRIVER}; use crate::authentication::key::AUTH_KEY_LENGTH; use crate::databases::error::Error; use crate::databases::SchemaMigrator; +#[async_trait] impl SchemaMigrator for Mysql { - fn create_database_tables(&self) -> Result<(), Error> { + async fn create_database_tables(&self) -> Result<(), Error> { let create_whitelist_table = " CREATE TABLE IF NOT EXISTS whitelist ( id integer PRIMARY KEY AUTO_INCREMENT, info_hash VARCHAR(40) NOT NULL UNIQUE - );" - .to_string(); + );"; let create_torrents_table = " CREATE TABLE IF NOT EXISTS torrents ( id integer PRIMARY KEY AUTO_INCREMENT, info_hash VARCHAR(40) NOT NULL UNIQUE, completed INTEGER DEFAULT 0 NOT NULL - );" - .to_string(); + );"; let create_torrent_aggregate_metrics_table = " CREATE TABLE IF NOT EXISTS torrent_aggregate_metrics ( id integer PRIMARY KEY AUTO_INCREMENT, metric_name VARCHAR(50) NOT NULL UNIQUE, value INTEGER DEFAULT 0 NOT NULL - );" - .to_string(); + );"; let create_keys_table = format!( " @@ -42,34 +40,55 @@ impl SchemaMigrator for Mysql { i8::try_from(AUTH_KEY_LENGTH).expect("authentication key length should fit within a i8!") ); - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - conn.query_drop(&create_torrents_table)?; - conn.query_drop(&create_torrent_aggregate_metrics_table)?; - conn.query_drop(&create_keys_table)?; - conn.query_drop(&create_whitelist_table)?; + ::sqlx::query(create_torrents_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + ::sqlx::query(create_torrent_aggregate_metrics_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + ::sqlx::query(&create_keys_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + ::sqlx::query(create_whitelist_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; Ok(()) } - fn drop_database_tables(&self) -> Result<(), Error> { + async fn drop_database_tables(&self) -> Result<(), Error> { let drop_whitelist_table = " - DROP TABLE `whitelist`;" - .to_string(); + DROP TABLE `whitelist`;"; let drop_torrents_table = " - DROP TABLE `torrents`;" - .to_string(); + DROP TABLE `torrents`;"; - let drop_keys_table = " - DROP TABLE `keys`;" - .to_string(); + let drop_torrent_aggregate_metrics_table = " + DROP TABLE `torrent_aggregate_metrics`;"; - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; + let drop_keys_table = " + DROP TABLE `keys`;"; - conn.query_drop(&drop_whitelist_table)?; - conn.query_drop(&drop_torrents_table)?; - conn.query_drop(&drop_keys_table)?; + ::sqlx::query(drop_whitelist_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + ::sqlx::query(drop_torrents_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + ::sqlx::query(drop_torrent_aggregate_metrics_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + ::sqlx::query(drop_keys_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; Ok(()) } diff --git a/packages/tracker-core/src/databases/driver/mysql/torrent_metrics_store.rs b/packages/tracker-core/src/databases/driver/mysql/torrent_metrics_store.rs index 0888e1a0f..1f8d7f436 100644 --- a/packages/tracker-core/src/databases/driver/mysql/torrent_metrics_store.rs +++ b/packages/tracker-core/src/databases/driver/mysql/torrent_metrics_store.rs @@ -1,8 +1,8 @@ use std::str::FromStr; +use ::sqlx::Row; +use async_trait::async_trait; use bittorrent_primitives::info_hash::InfoHash; -use r2d2_mysql::mysql::params; -use r2d2_mysql::mysql::prelude::Queryable; use torrust_tracker_primitives::{NumberOfDownloads, NumberOfDownloadsBTreeMap}; use super::{Mysql, DRIVER}; @@ -10,19 +10,24 @@ use crate::databases::driver::TORRENTS_DOWNLOADS_TOTAL; use crate::databases::error::Error; use crate::databases::TorrentMetricsStore; +#[async_trait] impl TorrentMetricsStore for Mysql { - fn load_all_torrents_downloads(&self) -> Result<NumberOfDownloadsBTreeMap, Error> { - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let raw_rows: Vec<(String, u32)> = conn.query_map( - "SELECT info_hash, completed FROM torrents", - |(info_hash_string, completed): (String, u32)| (info_hash_string, completed), - )?; - - raw_rows - .into_iter() - .map(|(s, completed)| { - InfoHash::from_str(&s) + async fn load_all_torrents_downloads(&self) -> Result<NumberOfDownloadsBTreeMap, Error> { + let rows = ::sqlx::query("SELECT info_hash, completed FROM torrents") + .fetch_all(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + + rows.into_iter() + .map(|row| { + let info_hash_value: String = row.try_get("info_hash").map_err(|e| (e, DRIVER))?; + let completed: i64 = row.try_get("completed").map_err(|e| (e, DRIVER))?; + let completed = u32::try_from(completed).map_err(|e| Error::MalformedDatabaseRecord { + message: e.to_string(), + driver: DRIVER, + })?; + + InfoHash::from_str(&info_hash_value) .map(|info_hash| (info_hash, completed)) .map_err(|e| Error::MalformedDatabaseRecord { message: format!("{e:?}"), @@ -33,59 +38,67 @@ impl TorrentMetricsStore for Mysql { .map(|v| v.iter().copied().collect()) } - fn load_torrent_downloads(&self, info_hash: &InfoHash) -> Result<Option<NumberOfDownloads>, Error> { - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let query = conn.exec_first::<u32, _, _>( - "SELECT completed FROM torrents WHERE info_hash = :info_hash", - params! { "info_hash" => info_hash.to_hex_string() }, - ); - - let persistent_torrent = query?; - - Ok(persistent_torrent) + async fn load_torrent_downloads(&self, info_hash: &InfoHash) -> Result<Option<NumberOfDownloads>, Error> { + let maybe_row = ::sqlx::query("SELECT completed FROM torrents WHERE info_hash = ?") + .bind(info_hash.to_hex_string()) + .fetch_optional(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + + maybe_row + .map(|row| { + let completed: i64 = row.try_get("completed").map_err(|e| (e, DRIVER))?; + u32::try_from(completed).map_err(|e| Error::MalformedDatabaseRecord { + message: e.to_string(), + driver: DRIVER, + }) + }) + .transpose() } - fn save_torrent_downloads(&self, info_hash: &InfoHash, completed: u32) -> Result<(), Error> { - const COMMAND : &str = "INSERT INTO torrents (info_hash, completed) VALUES (:info_hash_str, :completed) ON DUPLICATE KEY UPDATE completed = VALUES(completed)"; - - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; + async fn save_torrent_downloads(&self, info_hash: &InfoHash, completed: u32) -> Result<(), Error> { + // `ON DUPLICATE KEY UPDATE` may legitimately report `rows_affected() == 0` + // when the row already exists with the same value (no-op update), so we + // do not treat 0 as a failure here. A real failure surfaces as `Err` + // from `execute()`. + ::sqlx::query( + "INSERT INTO torrents (info_hash, completed) VALUES (?, ?) ON DUPLICATE KEY UPDATE completed = VALUES(completed)", + ) + .bind(info_hash.to_string()) + .bind(i64::from(completed)) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; - let info_hash_str = info_hash.to_string(); - - Ok(conn.exec_drop(COMMAND, params! { info_hash_str, completed })?) + Ok(()) } - fn increase_downloads_for_torrent(&self, info_hash: &InfoHash) -> Result<(), Error> { - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let info_hash_str = info_hash.to_string(); - - conn.exec_drop( - "UPDATE torrents SET completed = completed + 1 WHERE info_hash = :info_hash_str", - params! { info_hash_str }, - )?; + async fn increase_downloads_for_torrent(&self, info_hash: &InfoHash) -> Result<(), Error> { + ::sqlx::query("UPDATE torrents SET completed = completed + 1 WHERE info_hash = ?") + .bind(info_hash.to_string()) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; Ok(()) } - fn load_global_downloads(&self) -> Result<Option<NumberOfDownloads>, Error> { - self.load_torrent_aggregate_metric(TORRENTS_DOWNLOADS_TOTAL) + async fn load_global_downloads(&self) -> Result<Option<NumberOfDownloads>, Error> { + self.load_torrent_aggregate_metric(TORRENTS_DOWNLOADS_TOTAL).await } - fn save_global_downloads(&self, downloaded: NumberOfDownloads) -> Result<(), Error> { - self.save_torrent_aggregate_metric(TORRENTS_DOWNLOADS_TOTAL, downloaded) + async fn save_global_downloads(&self, downloaded: NumberOfDownloads) -> Result<(), Error> { + self.save_torrent_aggregate_metric(TORRENTS_DOWNLOADS_TOTAL, downloaded).await } - fn increase_global_downloads(&self) -> Result<(), Error> { - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - + async fn increase_global_downloads(&self) -> Result<(), Error> { let metric_name = TORRENTS_DOWNLOADS_TOTAL; - conn.exec_drop( - "UPDATE torrent_aggregate_metrics SET value = value + 1 WHERE metric_name = :metric_name", - params! { metric_name }, - )?; + ::sqlx::query("UPDATE torrent_aggregate_metrics SET value = value + 1 WHERE metric_name = ?") + .bind(metric_name) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; Ok(()) } diff --git a/packages/tracker-core/src/databases/driver/mysql/whitelist_store.rs b/packages/tracker-core/src/databases/driver/mysql/whitelist_store.rs index b0ffb7cc5..71c1ac7bd 100644 --- a/packages/tracker-core/src/databases/driver/mysql/whitelist_store.rs +++ b/packages/tracker-core/src/databases/driver/mysql/whitelist_store.rs @@ -1,22 +1,26 @@ +use std::panic::Location; use std::str::FromStr; +use ::sqlx::Row; +use async_trait::async_trait; use bittorrent_primitives::info_hash::InfoHash; -use r2d2_mysql::mysql::params; -use r2d2_mysql::mysql::prelude::Queryable; use super::{Mysql, DRIVER}; use crate::databases::error::Error; use crate::databases::WhitelistStore; +#[async_trait] impl WhitelistStore for Mysql { - fn load_whitelist(&self) -> Result<Vec<InfoHash>, Error> { - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let raw: Vec<String> = conn.query_map("SELECT info_hash FROM whitelist", |info_hash: String| info_hash)?; - - raw.into_iter() - .map(|s| { - InfoHash::from_str(&s).map_err(|e| Error::MalformedDatabaseRecord { + async fn load_whitelist(&self) -> Result<Vec<InfoHash>, Error> { + let rows = ::sqlx::query("SELECT info_hash FROM whitelist") + .fetch_all(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + + rows.into_iter() + .map(|row| { + let value: String = row.try_get("info_hash").map_err(|e| (e, DRIVER))?; + InfoHash::from_str(&value).map_err(|e| Error::MalformedDatabaseRecord { message: format!("{e:?}"), driver: DRIVER, }) @@ -24,46 +28,61 @@ impl WhitelistStore for Mysql { .collect() } - fn get_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result<Option<InfoHash>, Error> { - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let select = conn.exec_first::<String, _, _>( - "SELECT info_hash FROM whitelist WHERE info_hash = :info_hash", - params! { "info_hash" => info_hash.to_hex_string() }, - )?; - - let info_hash = select - .map(|s| { - InfoHash::from_str(&s).map_err(|e| Error::MalformedDatabaseRecord { + async fn get_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result<Option<InfoHash>, Error> { + let maybe_row = ::sqlx::query("SELECT info_hash FROM whitelist WHERE info_hash = ?") + .bind(info_hash.to_hex_string()) + .fetch_optional(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + + maybe_row + .map(|row| { + let value: String = row.try_get("info_hash").map_err(|e| (e, DRIVER))?; + InfoHash::from_str(&value).map_err(|e| Error::MalformedDatabaseRecord { message: format!("{e:?}"), driver: DRIVER, }) }) - .transpose()?; - - Ok(info_hash) + .transpose() } - fn add_info_hash_to_whitelist(&self, info_hash: InfoHash) -> Result<usize, Error> { - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let info_hash_str = info_hash.to_string(); - - conn.exec_drop( - "INSERT INTO whitelist (info_hash) VALUES (:info_hash_str)", - params! { info_hash_str }, - )?; - - Ok(1) + async fn add_info_hash_to_whitelist(&self, info_hash: InfoHash) -> Result<usize, Error> { + let insert = ::sqlx::query("INSERT INTO whitelist (info_hash) VALUES (?)") + .bind(info_hash.to_string()) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))? + .rows_affected(); + + if insert == 0 { + Err(Error::InsertFailed { + location: Location::caller(), + driver: DRIVER, + }) + } else { + usize::try_from(insert).map_err(|e| Error::MalformedDatabaseRecord { + message: format!("rows_affected does not fit in usize: {e}"), + driver: DRIVER, + }) + } } - fn remove_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result<usize, Error> { - let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let info_hash = info_hash.to_string(); - - conn.exec_drop("DELETE FROM whitelist WHERE info_hash = :info_hash", params! { info_hash })?; - - Ok(1) + async fn remove_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result<usize, Error> { + let deleted = ::sqlx::query("DELETE FROM whitelist WHERE info_hash = ?") + .bind(info_hash.to_string()) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))? + .rows_affected(); + + if deleted == 1 { + Ok(1) + } else { + Err(Error::DeleteFailed { + location: Location::caller(), + error_code: usize::try_from(deleted).unwrap_or(0), + driver: DRIVER, + }) + } } } diff --git a/packages/tracker-core/src/databases/driver/sqlite/auth_key_store.rs b/packages/tracker-core/src/databases/driver/sqlite/auth_key_store.rs index 57e6eef7a..f94770842 100644 --- a/packages/tracker-core/src/databases/driver/sqlite/auth_key_store.rs +++ b/packages/tracker-core/src/databases/driver/sqlite/auth_key_store.rs @@ -1,7 +1,7 @@ use std::panic::Location; -use r2d2_sqlite::rusqlite::params; -use r2d2_sqlite::rusqlite::types::Null; +use ::sqlx::Row; +use async_trait::async_trait; use torrust_tracker_primitives::DurationSinceUnixEpoch; use super::{Sqlite, DRIVER}; @@ -9,77 +9,75 @@ use crate::authentication::{self, Key}; use crate::databases::error::Error; use crate::databases::AuthKeyStore; +#[async_trait] impl AuthKeyStore for Sqlite { - fn load_keys(&self) -> Result<Vec<authentication::PeerKey>, Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let mut stmt = conn.prepare("SELECT key, valid_until FROM keys")?; - - let raw: Vec<(String, Option<i64>)> = stmt - .query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, Option<i64>>(1)?)))? - .filter_map(std::result::Result::ok) - .collect(); - - raw.into_iter() - .map(|(key, opt_valid_until)| { - let key = key.parse::<Key>().map_err(|e| Error::MalformedDatabaseRecord { + async fn load_keys(&self) -> Result<Vec<authentication::PeerKey>, Error> { + let rows = ::sqlx::query("SELECT key, valid_until FROM keys") + .fetch_all(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + + rows.into_iter() + .map(|row| { + let key_value: String = row.try_get("key").map_err(|e| (e, DRIVER))?; + let valid_until: Option<i64> = row.try_get("valid_until").map_err(|e| (e, DRIVER))?; + + let parsed_key = key_value.parse::<Key>().map_err(|e| Error::MalformedDatabaseRecord { message: e.to_string(), driver: DRIVER, })?; - Ok(match opt_valid_until { - Some(valid_until) => authentication::PeerKey { - key, - valid_until: Some(DurationSinceUnixEpoch::from_secs(valid_until.unsigned_abs())), - }, - None => authentication::PeerKey { key, valid_until: None }, + + Ok(authentication::PeerKey { + key: parsed_key, + valid_until: valid_until.map(parse_valid_until).transpose()?, }) }) .collect() } - fn get_key_from_keys(&self, key: &Key) -> Result<Option<authentication::PeerKey>, Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let mut stmt = conn.prepare("SELECT key, valid_until FROM keys WHERE key = ?")?; + async fn get_key_from_keys(&self, key: &Key) -> Result<Option<authentication::PeerKey>, Error> { + let maybe_row = ::sqlx::query("SELECT key, valid_until FROM keys WHERE key = ?1") + .bind(key.to_string()) + .fetch_optional(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; - let mut rows = stmt.query([key.to_string()])?; + maybe_row + .map(|row| { + let key_value: String = row.try_get("key").map_err(|e| (e, DRIVER))?; + let valid_until: Option<i64> = row.try_get("valid_until").map_err(|e| (e, DRIVER))?; - let key = rows.next()?; - - let peer_key = key - .map(|f| -> Result<authentication::PeerKey, Error> { - let valid_until: Option<i64> = f.get(1).map_err(Error::from)?; - let key: String = f.get(0).map_err(Error::from)?; - let key = key.parse::<Key>().map_err(|e| Error::MalformedDatabaseRecord { + let parsed_key = key_value.parse::<Key>().map_err(|e| Error::MalformedDatabaseRecord { message: e.to_string(), driver: DRIVER, })?; - Ok(match valid_until { - Some(valid_until) => authentication::PeerKey { - key, - valid_until: Some(DurationSinceUnixEpoch::from_secs(valid_until.unsigned_abs())), - }, - None => authentication::PeerKey { key, valid_until: None }, + + Ok(authentication::PeerKey { + key: parsed_key, + valid_until: valid_until.map(parse_valid_until).transpose()?, }) }) - .transpose()?; - - Ok(peer_key) + .transpose() } - fn add_key_to_keys(&self, auth_key: &authentication::PeerKey) -> Result<usize, Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; + async fn add_key_to_keys(&self, auth_key: &authentication::PeerKey) -> Result<usize, Error> { + let valid_until = auth_key + .valid_until + .map(|value| { + i64::try_from(value.as_secs()).map_err(|e| Error::MalformedDatabaseRecord { + message: e.to_string(), + driver: DRIVER, + }) + }) + .transpose()?; - let insert = match auth_key.valid_until { - Some(valid_until) => conn.execute( - "INSERT INTO keys (key, valid_until) VALUES (?1, ?2)", - [auth_key.key.to_string(), valid_until.as_secs().to_string()], - )?, - None => conn.execute( - "INSERT INTO keys (key, valid_until) VALUES (?1, ?2)", - params![auth_key.key.to_string(), Null], - )?, - }; + let insert = ::sqlx::query("INSERT INTO keys (key, valid_until) VALUES (?1, ?2)") + .bind(auth_key.key.to_string()) + .bind(valid_until) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))? + .rows_affected(); if insert == 0 { Err(Error::InsertFailed { @@ -87,24 +85,44 @@ impl AuthKeyStore for Sqlite { driver: DRIVER, }) } else { - Ok(insert) + usize::try_from(insert).map_err(|e| Error::MalformedDatabaseRecord { + message: format!("rows_affected does not fit in usize: {e}"), + driver: DRIVER, + }) } } - fn remove_key_from_keys(&self, key: &Key) -> Result<usize, Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let deleted = conn.execute("DELETE FROM keys WHERE key = ?", [key.to_string()])?; + async fn remove_key_from_keys(&self, key: &Key) -> Result<usize, Error> { + let deleted = ::sqlx::query("DELETE FROM keys WHERE key = ?1") + .bind(key.to_string()) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))? + .rows_affected(); if deleted == 1 { // should only remove a single record. - Ok(deleted) + Ok(1) } else { Err(Error::DeleteFailed { location: Location::caller(), - error_code: deleted, + error_code: usize::try_from(deleted).unwrap_or(0), driver: DRIVER, }) } } } + +/// Convert a signed seconds value loaded from the database into a +/// [`DurationSinceUnixEpoch`]. +/// +/// Negative values indicate a corrupted record (timestamps before the Unix +/// epoch are not representable) and are rejected as +/// [`Error::MalformedDatabaseRecord`]. +fn parse_valid_until(value: i64) -> Result<DurationSinceUnixEpoch, Error> { + let secs = u64::try_from(value).map_err(|_| Error::MalformedDatabaseRecord { + message: format!("negative valid_until timestamp: {value}"), + driver: DRIVER, + })?; + Ok(DurationSinceUnixEpoch::from_secs(secs)) +} diff --git a/packages/tracker-core/src/databases/driver/sqlite/mod.rs b/packages/tracker-core/src/databases/driver/sqlite/mod.rs index b82488933..d6a10d818 100644 --- a/packages/tracker-core/src/databases/driver/sqlite/mod.rs +++ b/packages/tracker-core/src/databases/driver/sqlite/mod.rs @@ -1,18 +1,6 @@ //! The `SQLite3` database driver. -//! -//! This module provides implementations of the four narrow database traits -//! ([`SchemaMigrator`](crate::databases::SchemaMigrator), -//! [`TorrentMetricsStore`](crate::databases::TorrentMetricsStore), -//! [`WhitelistStore`](crate::databases::WhitelistStore), -//! [`AuthKeyStore`](crate::databases::AuthKeyStore) -//! for `SQLite3` using the `r2d2_sqlite` connection pool. It defines the schema -//! for whitelist, torrent metrics, and authentication keys, and provides methods -//! to create and drop tables as well as perform CRUD operations on these -//! persistent objects. -use std::panic::Location; - -use r2d2::Pool; -use r2d2_sqlite::SqliteConnectionManager; +use ::sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; +use ::sqlx::{Row, SqlitePool}; use torrust_tracker_primitives::NumberOfDownloads; use super::{Driver, Error}; @@ -26,65 +14,61 @@ const DRIVER: Driver = Driver::Sqlite3; /// `SQLite` driver implementation. /// -/// This struct encapsulates a connection pool for `SQLite` using the `r2d2_sqlite` -/// connection manager. +/// This struct encapsulates an async `sqlx` connection pool for `SQLite`. pub(crate) struct Sqlite { - pool: Pool<SqliteConnectionManager>, + pool: SqlitePool, } impl Sqlite { /// Instantiates a new `SQLite3` database driver. /// - /// This function creates a connection manager for the `SQLite` database - /// located at `db_path` and then builds a connection pool using `r2d2`. If - /// the pool cannot be created, an error is returned (wrapped with the - /// appropriate driver information). - /// - /// # Arguments - /// - /// * `db_path` - A string slice representing the file path to the `SQLite` database. - /// - /// # Errors - /// - /// Returns an [`Error`] if the connection pool cannot be built. + // Keep the `Result` return for API symmetry with the MySQL driver and + // forward-compatibility (future option parsing may surface fallible cases). + #[allow(clippy::unnecessary_wraps)] pub fn new(db_path: &str) -> Result<Self, Error> { - let manager = SqliteConnectionManager::file(db_path); - let pool = r2d2::Pool::builder().build(manager).map_err(|e| (e, DRIVER))?; + // Build the connection options directly from the filesystem path so + // relative paths (e.g. `./storage/...`) are preserved verbatim instead + // of being parsed as the authority component of a `sqlite://` URL. + let options = SqliteConnectOptions::new().filename(db_path).create_if_missing(true); + + let pool = SqlitePoolOptions::new().connect_lazy_with(options); Ok(Self { pool }) } - fn load_torrent_aggregate_metric(&self, metric_name: &str) -> Result<Option<NumberOfDownloads>, Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let mut stmt = conn.prepare("SELECT value FROM torrent_aggregate_metrics WHERE metric_name = ?")?; - - let mut rows = stmt.query([metric_name])?; - - let persistent_torrent = rows.next()?; - - Ok(persistent_torrent.map(|f| { - let value: i64 = f.get(0).unwrap(); - u32::try_from(value).unwrap() - })) + async fn load_torrent_aggregate_metric(&self, metric_name: &str) -> Result<Option<NumberOfDownloads>, Error> { + let maybe_row = ::sqlx::query("SELECT value FROM torrent_aggregate_metrics WHERE metric_name = ?1") + .bind(metric_name) + .fetch_optional(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + + maybe_row + .map(|row| { + let value: i64 = row.try_get("value").map_err(|e| (e, DRIVER))?; + u32::try_from(value).map_err(|e| Error::MalformedDatabaseRecord { + message: e.to_string(), + driver: DRIVER, + }) + }) + .transpose() } - fn save_torrent_aggregate_metric(&self, metric_name: &str, completed: NumberOfDownloads) -> Result<(), Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let insert = conn.execute( + async fn save_torrent_aggregate_metric(&self, metric_name: &str, completed: NumberOfDownloads) -> Result<(), Error> { + // `ON CONFLICT ... DO UPDATE` may legitimately report `rows_affected() == 0` + // when the row already exists with the same value (no-op update), so we + // do not treat 0 as a failure here. A real failure surfaces as `Err` + // from `execute()`. + ::sqlx::query( "INSERT INTO torrent_aggregate_metrics (metric_name, value) VALUES (?1, ?2) ON CONFLICT(metric_name) DO UPDATE SET value = ?2", - [metric_name.to_string(), completed.to_string()], - )?; + ) + .bind(metric_name) + .bind(i64::from(completed)) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; - if insert == 0 { - Err(Error::InsertFailed { - location: Location::caller(), - driver: DRIVER, - }) - } else { - Ok(()) - } + Ok(()) } } @@ -108,8 +92,7 @@ mod tests { } fn initialize_driver(config: &Core) -> Arc<Box<dyn Database>> { - let driver: Arc<Box<dyn Database>> = Arc::new(Box::new(Sqlite::new(&config.database.path).unwrap())); - driver + Arc::new(Box::new(Sqlite::new(&config.database.path).unwrap())) } #[tokio::test] diff --git a/packages/tracker-core/src/databases/driver/sqlite/schema_migrator.rs b/packages/tracker-core/src/databases/driver/sqlite/schema_migrator.rs index 1c3c51ad5..33bed3d4f 100644 --- a/packages/tracker-core/src/databases/driver/sqlite/schema_migrator.rs +++ b/packages/tracker-core/src/databases/driver/sqlite/schema_migrator.rs @@ -1,68 +1,88 @@ +use async_trait::async_trait; + use super::{Sqlite, DRIVER}; use crate::databases::error::Error; use crate::databases::SchemaMigrator; +#[async_trait] impl SchemaMigrator for Sqlite { - fn create_database_tables(&self) -> Result<(), Error> { + async fn create_database_tables(&self) -> Result<(), Error> { let create_whitelist_table = " CREATE TABLE IF NOT EXISTS whitelist ( id INTEGER PRIMARY KEY AUTOINCREMENT, info_hash TEXT NOT NULL UNIQUE - );" - .to_string(); + );"; let create_torrents_table = " CREATE TABLE IF NOT EXISTS torrents ( id INTEGER PRIMARY KEY AUTOINCREMENT, info_hash TEXT NOT NULL UNIQUE, completed INTEGER DEFAULT 0 NOT NULL - );" - .to_string(); + );"; let create_torrent_aggregate_metrics_table = " CREATE TABLE IF NOT EXISTS torrent_aggregate_metrics ( id INTEGER PRIMARY KEY AUTOINCREMENT, metric_name TEXT NOT NULL UNIQUE, value INTEGER DEFAULT 0 NOT NULL - );" - .to_string(); + );"; let create_keys_table = " CREATE TABLE IF NOT EXISTS keys ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL UNIQUE, valid_until INTEGER - );" - .to_string(); - - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; + );"; - conn.execute(&create_whitelist_table, [])?; - conn.execute(&create_keys_table, [])?; - conn.execute(&create_torrents_table, [])?; - conn.execute(&create_torrent_aggregate_metrics_table, [])?; + ::sqlx::query(create_whitelist_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + ::sqlx::query(create_keys_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + ::sqlx::query(create_torrents_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + ::sqlx::query(create_torrent_aggregate_metrics_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; Ok(()) } - fn drop_database_tables(&self) -> Result<(), Error> { + async fn drop_database_tables(&self) -> Result<(), Error> { let drop_whitelist_table = " - DROP TABLE whitelist;" - .to_string(); + DROP TABLE whitelist;"; let drop_torrents_table = " - DROP TABLE torrents;" - .to_string(); + DROP TABLE torrents;"; - let drop_keys_table = " - DROP TABLE keys;" - .to_string(); + let drop_torrent_aggregate_metrics_table = " + DROP TABLE torrent_aggregate_metrics;"; - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; + let drop_keys_table = " + DROP TABLE keys;"; - conn.execute(&drop_whitelist_table, []) - .and_then(|_| conn.execute(&drop_torrents_table, [])) - .and_then(|_| conn.execute(&drop_keys_table, []))?; + ::sqlx::query(drop_whitelist_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + ::sqlx::query(drop_torrents_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + ::sqlx::query(drop_torrent_aggregate_metrics_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + ::sqlx::query(drop_keys_table) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; Ok(()) } diff --git a/packages/tracker-core/src/databases/driver/sqlite/torrent_metrics_store.rs b/packages/tracker-core/src/databases/driver/sqlite/torrent_metrics_store.rs index 67dc54891..b8df34fb1 100644 --- a/packages/tracker-core/src/databases/driver/sqlite/torrent_metrics_store.rs +++ b/packages/tracker-core/src/databases/driver/sqlite/torrent_metrics_store.rs @@ -1,5 +1,7 @@ use std::str::FromStr; +use ::sqlx::Row; +use async_trait::async_trait; use bittorrent_primitives::info_hash::InfoHash; use torrust_tracker_primitives::{NumberOfDownloads, NumberOfDownloadsBTreeMap}; @@ -8,20 +10,24 @@ use crate::databases::driver::TORRENTS_DOWNLOADS_TOTAL; use crate::databases::error::Error; use crate::databases::TorrentMetricsStore; +#[async_trait] impl TorrentMetricsStore for Sqlite { - fn load_all_torrents_downloads(&self) -> Result<NumberOfDownloadsBTreeMap, Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let mut stmt = conn.prepare("SELECT info_hash, completed FROM torrents")?; - - let raw: Vec<(String, u32)> = stmt - .query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, u32>(1)?)))? - .filter_map(std::result::Result::ok) - .collect(); - - raw.into_iter() - .map(|(s, completed)| { - InfoHash::from_str(&s) + async fn load_all_torrents_downloads(&self) -> Result<NumberOfDownloadsBTreeMap, Error> { + let rows = ::sqlx::query("SELECT info_hash, completed FROM torrents") + .fetch_all(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + + rows.into_iter() + .map(|row| { + let info_hash_value: String = row.try_get("info_hash").map_err(|e| (e, DRIVER))?; + let completed: i64 = row.try_get("completed").map_err(|e| (e, DRIVER))?; + let completed = u32::try_from(completed).map_err(|e| Error::MalformedDatabaseRecord { + message: e.to_string(), + driver: DRIVER, + })?; + + InfoHash::from_str(&info_hash_value) .map(|info_hash| (info_hash, completed)) .map_err(|e| Error::MalformedDatabaseRecord { message: format!("{e:?}"), @@ -32,67 +38,67 @@ impl TorrentMetricsStore for Sqlite { .map(|v| v.iter().copied().collect()) } - fn load_torrent_downloads(&self, info_hash: &InfoHash) -> Result<Option<NumberOfDownloads>, Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let mut stmt = conn.prepare("SELECT completed FROM torrents WHERE info_hash = ?")?; - - let mut rows = stmt.query([info_hash.to_hex_string()])?; - - let persistent_torrent = rows.next()?; - - Ok(persistent_torrent.map(|f| { - let completed: i64 = f.get(0).unwrap(); - u32::try_from(completed).unwrap() - })) + async fn load_torrent_downloads(&self, info_hash: &InfoHash) -> Result<Option<NumberOfDownloads>, Error> { + let maybe_row = ::sqlx::query("SELECT completed FROM torrents WHERE info_hash = ?1") + .bind(info_hash.to_hex_string()) + .fetch_optional(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + + maybe_row + .map(|row| { + let completed: i64 = row.try_get("completed").map_err(|e| (e, DRIVER))?; + u32::try_from(completed).map_err(|e| Error::MalformedDatabaseRecord { + message: e.to_string(), + driver: DRIVER, + }) + }) + .transpose() } - fn save_torrent_downloads(&self, info_hash: &InfoHash, completed: u32) -> Result<(), Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let insert = conn.execute( + async fn save_torrent_downloads(&self, info_hash: &InfoHash, completed: u32) -> Result<(), Error> { + // `ON CONFLICT ... DO UPDATE` may legitimately report `rows_affected() == 0` + // when the row already exists with the same value (no-op update), so we + // do not treat 0 as a failure here. A real failure surfaces as `Err` + // from `execute()`. + ::sqlx::query( "INSERT INTO torrents (info_hash, completed) VALUES (?1, ?2) ON CONFLICT(info_hash) DO UPDATE SET completed = ?2", - [info_hash.to_string(), completed.to_string()], - )?; + ) + .bind(info_hash.to_string()) + .bind(i64::from(completed)) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; - if insert == 0 { - Err(Error::InsertFailed { - location: std::panic::Location::caller(), - driver: DRIVER, - }) - } else { - Ok(()) - } + Ok(()) } - fn increase_downloads_for_torrent(&self, info_hash: &InfoHash) -> Result<(), Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let _ = conn.execute( - "UPDATE torrents SET completed = completed + 1 WHERE info_hash = ?", - [info_hash.to_string()], - )?; + async fn increase_downloads_for_torrent(&self, info_hash: &InfoHash) -> Result<(), Error> { + ::sqlx::query("UPDATE torrents SET completed = completed + 1 WHERE info_hash = ?1") + .bind(info_hash.to_string()) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; Ok(()) } - fn load_global_downloads(&self) -> Result<Option<NumberOfDownloads>, Error> { - self.load_torrent_aggregate_metric(TORRENTS_DOWNLOADS_TOTAL) + async fn load_global_downloads(&self) -> Result<Option<NumberOfDownloads>, Error> { + self.load_torrent_aggregate_metric(TORRENTS_DOWNLOADS_TOTAL).await } - fn save_global_downloads(&self, downloaded: NumberOfDownloads) -> Result<(), Error> { - self.save_torrent_aggregate_metric(TORRENTS_DOWNLOADS_TOTAL, downloaded) + async fn save_global_downloads(&self, downloaded: NumberOfDownloads) -> Result<(), Error> { + self.save_torrent_aggregate_metric(TORRENTS_DOWNLOADS_TOTAL, downloaded).await } - fn increase_global_downloads(&self) -> Result<(), Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - + async fn increase_global_downloads(&self) -> Result<(), Error> { let metric_name = TORRENTS_DOWNLOADS_TOTAL; - let _ = conn.execute( - "UPDATE torrent_aggregate_metrics SET value = value + 1 WHERE metric_name = ?", - [metric_name], - )?; + ::sqlx::query("UPDATE torrent_aggregate_metrics SET value = value + 1 WHERE metric_name = ?1") + .bind(metric_name) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; Ok(()) } diff --git a/packages/tracker-core/src/databases/driver/sqlite/whitelist_store.rs b/packages/tracker-core/src/databases/driver/sqlite/whitelist_store.rs index 9cfb3f600..263eae2fb 100644 --- a/packages/tracker-core/src/databases/driver/sqlite/whitelist_store.rs +++ b/packages/tracker-core/src/databases/driver/sqlite/whitelist_store.rs @@ -1,26 +1,26 @@ use std::panic::Location; use std::str::FromStr; +use ::sqlx::Row; +use async_trait::async_trait; use bittorrent_primitives::info_hash::InfoHash; use super::{Sqlite, DRIVER}; use crate::databases::error::Error; use crate::databases::WhitelistStore; +#[async_trait] impl WhitelistStore for Sqlite { - fn load_whitelist(&self) -> Result<Vec<InfoHash>, Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let mut stmt = conn.prepare("SELECT info_hash FROM whitelist")?; - - let raw: Vec<String> = stmt - .query_map([], |row| row.get::<_, String>(0))? - .filter_map(std::result::Result::ok) - .collect(); - - raw.into_iter() - .map(|s| { - InfoHash::from_str(&s).map_err(|e| Error::MalformedDatabaseRecord { + async fn load_whitelist(&self) -> Result<Vec<InfoHash>, Error> { + let rows = ::sqlx::query("SELECT info_hash FROM whitelist") + .fetch_all(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + + rows.into_iter() + .map(|row| { + let value: String = row.try_get("info_hash").map_err(|e| (e, DRIVER))?; + InfoHash::from_str(&value).map_err(|e| Error::MalformedDatabaseRecord { message: format!("{e:?}"), driver: DRIVER, }) @@ -28,32 +28,31 @@ impl WhitelistStore for Sqlite { .collect() } - fn get_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result<Option<InfoHash>, Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let mut stmt = conn.prepare("SELECT info_hash FROM whitelist WHERE info_hash = ?")?; - - let mut rows = stmt.query([info_hash.to_hex_string()])?; - - let query = rows.next()?; - - let info_hash = query - .map(|f| -> Result<InfoHash, Error> { - let s: String = f.get(0).map_err(Error::from)?; - InfoHash::from_str(&s).map_err(|e| Error::MalformedDatabaseRecord { + async fn get_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result<Option<InfoHash>, Error> { + let maybe_row = ::sqlx::query("SELECT info_hash FROM whitelist WHERE info_hash = ?1") + .bind(info_hash.to_hex_string()) + .fetch_optional(&self.pool) + .await + .map_err(|e| (e, DRIVER))?; + + maybe_row + .map(|row| { + let value: String = row.try_get("info_hash").map_err(|e| (e, DRIVER))?; + InfoHash::from_str(&value).map_err(|e| Error::MalformedDatabaseRecord { message: format!("{e:?}"), driver: DRIVER, }) }) - .transpose()?; - - Ok(info_hash) + .transpose() } - fn add_info_hash_to_whitelist(&self, info_hash: InfoHash) -> Result<usize, Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let insert = conn.execute("INSERT INTO whitelist (info_hash) VALUES (?)", [info_hash.to_string()])?; + async fn add_info_hash_to_whitelist(&self, info_hash: InfoHash) -> Result<usize, Error> { + let insert = ::sqlx::query("INSERT INTO whitelist (info_hash) VALUES (?1)") + .bind(info_hash.to_string()) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))? + .rows_affected(); if insert == 0 { Err(Error::InsertFailed { @@ -61,22 +60,28 @@ impl WhitelistStore for Sqlite { driver: DRIVER, }) } else { - Ok(insert) + usize::try_from(insert).map_err(|e| Error::MalformedDatabaseRecord { + message: format!("rows_affected does not fit in usize: {e}"), + driver: DRIVER, + }) } } - fn remove_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result<usize, Error> { - let conn = self.pool.get().map_err(|e| (e, DRIVER))?; - - let deleted = conn.execute("DELETE FROM whitelist WHERE info_hash = ?", [info_hash.to_string()])?; + async fn remove_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result<usize, Error> { + let deleted = ::sqlx::query("DELETE FROM whitelist WHERE info_hash = ?1") + .bind(info_hash.to_string()) + .execute(&self.pool) + .await + .map_err(|e| (e, DRIVER))? + .rows_affected(); if deleted == 1 { // should only remove a single record. - Ok(deleted) + Ok(1) } else { Err(Error::DeleteFailed { location: Location::caller(), - error_code: deleted, + error_code: usize::try_from(deleted).unwrap_or(0), driver: DRIVER, }) } diff --git a/packages/tracker-core/src/databases/error.rs b/packages/tracker-core/src/databases/error.rs index 1b6d718f2..427270c65 100644 --- a/packages/tracker-core/src/databases/error.rs +++ b/packages/tracker-core/src/databases/error.rs @@ -6,13 +6,13 @@ //! creation errors. Each error variant includes contextual information such as //! the associated database driver and, when applicable, the source error. //! -//! External errors from database libraries (e.g., `rusqlite`, `mysql`) are -//! converted into this error type using the provided `From` implementations. +//! External errors from the `sqlx` database library are converted into this +//! error type using the provided `From` implementations. use std::panic::Location; use std::sync::Arc; -use r2d2_mysql::mysql::UrlError; -use torrust_tracker_located_error::{DynError, Located, LocatedError}; +use sqlx::Error as SqlxError; +use torrust_tracker_located_error::{DynError, LocatedError}; use super::driver::Driver; @@ -77,102 +77,63 @@ pub enum Error { /// Indicates a failure to connect to the database. /// - /// This error variant wraps connection-related errors, such as those caused by an invalid URL. + /// This error variant wraps connection-related errors, such as pool + /// timeouts, TLS failures, or invalid URL errors. #[error("Failed to connect to {driver} database: {source}")] ConnectionError { - source: LocatedError<'static, UrlError>, - driver: Driver, - }, - - /// Indicates a failure to create a connection pool. - /// - /// This error variant is used when the connection pool creation (using r2d2) fails. - #[error("Failed to create r2d2 {driver} connection pool: {source}")] - ConnectionPool { - source: LocatedError<'static, r2d2::Error>, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, driver: Driver, }, } -impl From<r2d2_sqlite::rusqlite::Error> for Error { +impl From<(SqlxError, Driver)> for Error { #[track_caller] - fn from(err: r2d2_sqlite::rusqlite::Error) -> Self { + fn from(value: (SqlxError, Driver)) -> Self { + let (err, driver) = value; + match err { - r2d2_sqlite::rusqlite::Error::QueryReturnedNoRows => Error::QueryReturnedNoRows { + SqlxError::RowNotFound => Self::QueryReturnedNoRows { + source: (Arc::new(SqlxError::RowNotFound) as DynError).into(), + driver, + }, + SqlxError::Io(_) + | SqlxError::Tls(_) + | SqlxError::PoolTimedOut + | SqlxError::PoolClosed + | SqlxError::WorkerCrashed + | SqlxError::Configuration(_) => Self::ConnectionError { source: (Arc::new(err) as DynError).into(), - driver: Driver::Sqlite3, + driver, }, - _ => Error::InvalidQuery { + _ => Self::InvalidQuery { source: (Arc::new(err) as DynError).into(), - driver: Driver::Sqlite3, + driver, }, } } } -impl From<r2d2_mysql::mysql::Error> for Error { - #[track_caller] - fn from(err: r2d2_mysql::mysql::Error) -> Self { - let e: DynError = Arc::new(err); - Error::InvalidQuery { - source: e.into(), - driver: Driver::MySQL, - } - } -} - -impl From<UrlError> for Error { - #[track_caller] - fn from(err: UrlError) -> Self { - Self::ConnectionError { - source: Located(err).into(), - driver: Driver::MySQL, - } - } -} - -impl From<(r2d2::Error, Driver)> for Error { - #[track_caller] - fn from(e: (r2d2::Error, Driver)) -> Self { - let (err, driver) = e; - Self::ConnectionPool { - source: Located(err).into(), - driver, - } - } -} - #[cfg(test)] mod tests { - use r2d2_mysql::mysql; - + use crate::databases::driver::Driver; use crate::databases::error::Error; #[test] - fn it_should_build_a_database_error_from_a_rusqlite_error() { - let err: Error = r2d2_sqlite::rusqlite::Error::InvalidQuery.into(); - - assert!(matches!(err, Error::InvalidQuery { .. })); - } - - #[test] - fn it_should_build_an_specific_database_error_from_a_no_rows_returned_rusqlite_error() { - let err: Error = r2d2_sqlite::rusqlite::Error::QueryReturnedNoRows.into(); + fn it_should_build_a_database_error_from_a_sqlx_row_not_found_error() { + let err: Error = (sqlx::Error::RowNotFound, Driver::Sqlite3).into(); assert!(matches!(err, Error::QueryReturnedNoRows { .. })); } #[test] - fn it_should_build_a_database_error_from_a_mysql_error() { - let url_err = mysql::error::UrlError::BadUrl; - let err: Error = r2d2_mysql::mysql::Error::UrlError(url_err).into(); - - assert!(matches!(err, Error::InvalidQuery { .. })); - } - - #[test] - fn it_should_build_a_database_error_from_a_mysql_url_error() { - let err: Error = mysql::error::UrlError::BadUrl.into(); + fn it_should_build_a_database_error_from_a_sqlx_io_error() { + use std::io; + + let err: Error = ( + sqlx::Error::Io(io::Error::from(io::ErrorKind::ConnectionRefused)), + Driver::MySQL, + ) + .into(); assert!(matches!(err, Error::ConnectionError { .. })); } diff --git a/packages/tracker-core/src/databases/setup.rs b/packages/tracker-core/src/databases/setup.rs index 71a0c1e73..c09a754e3 100644 --- a/packages/tracker-core/src/databases/setup.rs +++ b/packages/tracker-core/src/databases/setup.rs @@ -60,6 +60,18 @@ where /// driver fails to build the connection). This is enforced by the use of /// [`expect`](std::result::Result::expect) in the implementation. /// +/// In particular, schema initialization issues a query against the configured +/// database immediately after the driver is built. If the database service is +/// not yet ready to accept connections (for example, a freshly started `MySQL` +/// container that has not finished binding its TCP listener), the first query +/// can fail and this function will panic. The `sqlx` driver does not retry the +/// initial connection on its own, so callers are responsible for ensuring the +/// database is reachable before calling `initialize_database`. +/// +/// Other panic causes include malformed connection URLs, authentication +/// failures, insufficient permissions to issue DDL, network errors, or any +/// other underlying `sqlx::Error` returned while creating the schema. +/// /// # Example /// /// ```rust,no_run @@ -70,10 +82,12 @@ where /// let config = Core::default(); /// /// // Initialize the database; this will panic if initialization fails. -/// let stores = initialize_database(&config); +/// # async { +/// let stores = initialize_database(&config).await; +/// # }; /// ``` #[must_use] -pub fn initialize_database(config: &Core) -> DatabaseStores { +pub async fn initialize_database(config: &Core) -> DatabaseStores { let driver = match config.database.driver { torrust_tracker_configuration::Driver::Sqlite3 => Driver::Sqlite3, torrust_tracker_configuration::Driver::MySQL => Driver::MySQL, @@ -82,12 +96,12 @@ pub fn initialize_database(config: &Core) -> DatabaseStores { match driver { Driver::Sqlite3 => { let db = Arc::new(Sqlite::new(&config.database.path).expect("Database driver build failed.")); - db.create_database_tables().expect("Could not create database tables."); + db.create_database_tables().await.expect("Could not create database tables."); build_database_stores(db) } Driver::MySQL => { let db = Arc::new(Mysql::new(&config.database.path).expect("Database driver build failed.")); - db.create_database_tables().expect("Could not create database tables."); + db.create_database_tables().await.expect("Could not create database tables."); build_database_stores(db) } } @@ -98,9 +112,9 @@ mod tests { use super::initialize_database; use crate::test_helpers::tests::ephemeral_configuration; - #[test] - fn it_should_initialize_the_sqlite_database() { + #[tokio::test] + async fn it_should_initialize_the_sqlite_database() { let config = ephemeral_configuration(); - let _database = initialize_database(&config); + let _database = initialize_database(&config).await; } } diff --git a/packages/tracker-core/src/databases/traits/auth_keys.rs b/packages/tracker-core/src/databases/traits/auth_keys.rs index 623f70176..d99759ef0 100644 --- a/packages/tracker-core/src/databases/traits/auth_keys.rs +++ b/packages/tracker-core/src/databases/traits/auth_keys.rs @@ -1,4 +1,5 @@ //! The [`AuthKeyStore`] trait — authentication keys context. +use async_trait::async_trait; use mockall::automock; use super::super::error::Error; @@ -8,6 +9,7 @@ use crate::authentication::{self, Key}; // The `automock` macro generates a struct whose fields all end with `keys`, // which triggers `clippy::struct_field_names` (pedantic). Suppressed here // because the generated mock struct is outside our control. +#[async_trait] #[allow(clippy::struct_field_names)] #[automock] pub trait AuthKeyStore: Sync + Send { @@ -16,7 +18,7 @@ pub trait AuthKeyStore: Sync + Send { /// # Errors /// /// Returns an [`Error`] if the keys cannot be loaded. - fn load_keys(&self) -> Result<Vec<authentication::PeerKey>, Error>; + async fn load_keys(&self) -> Result<Vec<authentication::PeerKey>, Error>; /// Retrieves a specific authentication key from the database. /// @@ -26,19 +28,19 @@ pub trait AuthKeyStore: Sync + Send { /// # Errors /// /// Returns an [`Error`] if the key cannot be queried. - fn get_key_from_keys(&self, key: &Key) -> Result<Option<authentication::PeerKey>, Error>; + async fn get_key_from_keys(&self, key: &Key) -> Result<Option<authentication::PeerKey>, Error>; /// Adds an authentication key to the database. /// /// # Errors /// /// Returns an [`Error`] if the key cannot be saved. - fn add_key_to_keys(&self, auth_key: &authentication::PeerKey) -> Result<usize, Error>; + async fn add_key_to_keys(&self, auth_key: &authentication::PeerKey) -> Result<usize, Error>; /// Removes an authentication key from the database. /// /// # Errors /// /// Returns an [`Error`] if the key cannot be removed. - fn remove_key_from_keys(&self, key: &Key) -> Result<usize, Error>; + async fn remove_key_from_keys(&self, key: &Key) -> Result<usize, Error>; } diff --git a/packages/tracker-core/src/databases/traits/schema.rs b/packages/tracker-core/src/databases/traits/schema.rs index 0c0ef05ca..86ce385f3 100644 --- a/packages/tracker-core/src/databases/traits/schema.rs +++ b/packages/tracker-core/src/databases/traits/schema.rs @@ -1,4 +1,5 @@ //! The [`SchemaMigrator`] trait — schema management context. +use async_trait::async_trait; use mockall::automock; use super::super::error::Error; @@ -7,6 +8,7 @@ use super::super::error::Error; /// /// Implementors are responsible for creating and dropping the full set of /// database tables used by the tracker. +#[async_trait] #[automock] pub trait SchemaMigrator: Sync + Send { /// Creates the necessary database tables. @@ -16,7 +18,7 @@ pub trait SchemaMigrator: Sync + Send { /// # Errors /// /// Returns an [`Error`] if the tables cannot be created. - fn create_database_tables(&self) -> Result<(), Error>; + async fn create_database_tables(&self) -> Result<(), Error>; /// Drops the database tables. /// @@ -25,5 +27,5 @@ pub trait SchemaMigrator: Sync + Send { /// # Errors /// /// Returns an [`Error`] if the tables cannot be dropped. - fn drop_database_tables(&self) -> Result<(), Error>; + async fn drop_database_tables(&self) -> Result<(), Error>; } diff --git a/packages/tracker-core/src/databases/traits/torrent_metrics.rs b/packages/tracker-core/src/databases/traits/torrent_metrics.rs index 0d77ac77a..0a618a20d 100644 --- a/packages/tracker-core/src/databases/traits/torrent_metrics.rs +++ b/packages/tracker-core/src/databases/traits/torrent_metrics.rs @@ -4,6 +4,7 @@ //! aggregate downloads metric. The decision and revisit criteria are documented //! in ADR //! [`20260429000000_keep_database_as_aggregate_supertrait`](../../../../docs/adrs/20260429000000_keep_database_as_aggregate_supertrait.md). +use async_trait::async_trait; use bittorrent_primitives::info_hash::InfoHash; use mockall::automock; use torrust_tracker_primitives::{NumberOfDownloads, NumberOfDownloadsBTreeMap}; @@ -12,6 +13,7 @@ use super::super::error::Error; /// Trait covering persistence operations for per-torrent and global download /// counters. +#[async_trait] #[automock] pub trait TorrentMetricsStore: Sync + Send { /// Loads torrent metrics data from the database for all torrents. @@ -23,14 +25,14 @@ pub trait TorrentMetricsStore: Sync + Send { /// # Errors /// /// Returns an [`Error`] if the metrics cannot be loaded. - fn load_all_torrents_downloads(&self) -> Result<NumberOfDownloadsBTreeMap, Error>; + async fn load_all_torrents_downloads(&self) -> Result<NumberOfDownloadsBTreeMap, Error>; /// Loads torrent metrics data from the database for one torrent. /// /// # Errors /// /// Returns an [`Error`] if the metrics cannot be loaded. - fn load_torrent_downloads(&self, info_hash: &InfoHash) -> Result<Option<NumberOfDownloads>, Error>; + async fn load_torrent_downloads(&self, info_hash: &InfoHash) -> Result<Option<NumberOfDownloads>, Error>; /// Saves torrent metrics data into the database. /// @@ -42,7 +44,7 @@ pub trait TorrentMetricsStore: Sync + Send { /// # Errors /// /// Returns an [`Error`] if the metrics cannot be saved. - fn save_torrent_downloads(&self, info_hash: &InfoHash, downloaded: u32) -> Result<(), Error>; + async fn save_torrent_downloads(&self, info_hash: &InfoHash, downloaded: u32) -> Result<(), Error>; /// Increases the number of downloads for a given torrent. /// @@ -58,14 +60,14 @@ pub trait TorrentMetricsStore: Sync + Send { /// # Errors /// /// Returns an [`Error`] if the query failed. - fn increase_downloads_for_torrent(&self, info_hash: &InfoHash) -> Result<(), Error>; + async fn increase_downloads_for_torrent(&self, info_hash: &InfoHash) -> Result<(), Error>; /// Loads the total number of downloads for all torrents from the database. /// /// # Errors /// /// Returns an [`Error`] if the total downloads cannot be loaded. - fn load_global_downloads(&self) -> Result<Option<NumberOfDownloads>, Error>; + async fn load_global_downloads(&self) -> Result<Option<NumberOfDownloads>, Error>; /// Saves the total number of downloads for all torrents into the database. /// @@ -76,12 +78,12 @@ pub trait TorrentMetricsStore: Sync + Send { /// # Errors /// /// Returns an [`Error`] if the total downloads cannot be saved. - fn save_global_downloads(&self, downloaded: NumberOfDownloads) -> Result<(), Error>; + async fn save_global_downloads(&self, downloaded: NumberOfDownloads) -> Result<(), Error>; /// Increases the total number of downloads for all torrents. /// /// # Errors /// /// Returns an [`Error`] if the query failed. - fn increase_global_downloads(&self) -> Result<(), Error>; + async fn increase_global_downloads(&self) -> Result<(), Error>; } diff --git a/packages/tracker-core/src/databases/traits/whitelist.rs b/packages/tracker-core/src/databases/traits/whitelist.rs index 4ad9546ad..b463708f2 100644 --- a/packages/tracker-core/src/databases/traits/whitelist.rs +++ b/packages/tracker-core/src/databases/traits/whitelist.rs @@ -1,10 +1,12 @@ //! The [`WhitelistStore`] trait — torrent whitelist context. +use async_trait::async_trait; use bittorrent_primitives::info_hash::InfoHash; use mockall::automock; use super::super::error::Error; /// Trait covering persistence operations for the torrent whitelist. +#[async_trait] #[automock] pub trait WhitelistStore: Sync + Send { /// Loads the whitelisted torrents from the database. @@ -12,7 +14,7 @@ pub trait WhitelistStore: Sync + Send { /// # Errors /// /// Returns an [`Error`] if the whitelist cannot be loaded. - fn load_whitelist(&self) -> Result<Vec<InfoHash>, Error>; + async fn load_whitelist(&self) -> Result<Vec<InfoHash>, Error>; /// Retrieves a whitelisted torrent from the database. /// @@ -22,21 +24,21 @@ pub trait WhitelistStore: Sync + Send { /// # Errors /// /// Returns an [`Error`] if the whitelist cannot be queried. - fn get_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result<Option<InfoHash>, Error>; + async fn get_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result<Option<InfoHash>, Error>; /// Adds a torrent to the whitelist. /// /// # Errors /// /// Returns an [`Error`] if the torrent cannot be added to the whitelist. - fn add_info_hash_to_whitelist(&self, info_hash: InfoHash) -> Result<usize, Error>; + async fn add_info_hash_to_whitelist(&self, info_hash: InfoHash) -> Result<usize, Error>; /// Removes a torrent from the whitelist. /// /// # Errors /// /// Returns an [`Error`] if the torrent cannot be removed from the whitelist. - fn remove_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result<usize, Error>; + async fn remove_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result<usize, Error>; /// Checks whether a torrent is whitelisted. /// @@ -46,7 +48,7 @@ pub trait WhitelistStore: Sync + Send { /// # Errors /// /// Returns an [`Error`] if the whitelist cannot be queried. - fn is_info_hash_whitelisted(&self, info_hash: InfoHash) -> Result<bool, Error> { - Ok(self.get_info_hash_from_whitelist(info_hash)?.is_some()) + async fn is_info_hash_whitelisted(&self, info_hash: InfoHash) -> Result<bool, Error> { + Ok(self.get_info_hash_from_whitelist(info_hash).await?.is_some()) } } diff --git a/packages/tracker-core/src/lib.rs b/packages/tracker-core/src/lib.rs index 5167abf51..b711cda13 100644 --- a/packages/tracker-core/src/lib.rs +++ b/packages/tracker-core/src/lib.rs @@ -170,14 +170,14 @@ mod tests { use crate::scrape_handler::ScrapeHandler; use crate::test_helpers::tests::initialize_handlers; - fn initialize_handlers_for_public_tracker() -> (Arc<AnnounceHandler>, Arc<ScrapeHandler>) { + async fn initialize_handlers_for_public_tracker() -> (Arc<AnnounceHandler>, Arc<ScrapeHandler>) { let config = configuration::ephemeral_public(); - initialize_handlers(&config) + initialize_handlers(&config).await } - fn initialize_handlers_for_listed_tracker() -> (Arc<AnnounceHandler>, Arc<ScrapeHandler>) { + async fn initialize_handlers_for_listed_tracker() -> (Arc<AnnounceHandler>, Arc<ScrapeHandler>) { let config = configuration::ephemeral_listed(); - initialize_handlers(&config) + initialize_handlers(&config).await } mod for_all_config_modes { @@ -196,7 +196,7 @@ mod tests { #[tokio::test] async fn it_should_return_the_swarm_metadata_for_the_requested_file_if_the_tracker_has_that_torrent() { - let (announce_handler, scrape_handler) = initialize_handlers_for_public_tracker(); + let (announce_handler, scrape_handler) = initialize_handlers_for_public_tracker().await; let info_hash = "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap(); // DevSkim: ignore DS173237 @@ -255,7 +255,7 @@ mod tests { #[tokio::test] async fn it_should_return_the_zeroed_swarm_metadata_for_the_requested_file_if_it_is_not_whitelisted() { - let (_announce_handler, scrape_handler) = initialize_handlers_for_listed_tracker(); + let (_announce_handler, scrape_handler) = initialize_handlers_for_listed_tracker().await; let non_whitelisted_info_hash = "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap(); // DevSkim: ignore DS173237 diff --git a/packages/tracker-core/src/statistics/event/handler.rs b/packages/tracker-core/src/statistics/event/handler.rs index 9a5182f25..afcff4e82 100644 --- a/packages/tracker-core/src/statistics/event/handler.rs +++ b/packages/tracker-core/src/statistics/event/handler.rs @@ -53,7 +53,10 @@ pub async fn handle_event( if persistent_torrent_completed_stat { // Increment the number of downloads for the torrent in the database - match db_downloads_metric_repository.increase_downloads_for_torrent(&info_hash) { + match db_downloads_metric_repository + .increase_downloads_for_torrent(&info_hash) + .await + { Ok(()) => { tracing::debug!(info_hash = ?info_hash, "Number of torrent downloads increased"); } @@ -63,7 +66,7 @@ pub async fn handle_event( } // Increment the global number of downloads (for all torrents) in the database - match db_downloads_metric_repository.increase_global_downloads() { + match db_downloads_metric_repository.increase_global_downloads().await { Ok(()) => { tracing::debug!("Global number of downloads increased"); } diff --git a/packages/tracker-core/src/statistics/persisted/downloads.rs b/packages/tracker-core/src/statistics/persisted/downloads.rs index 4c81fb50b..e308c0063 100644 --- a/packages/tracker-core/src/statistics/persisted/downloads.rs +++ b/packages/tracker-core/src/statistics/persisted/downloads.rs @@ -59,12 +59,12 @@ impl DatabaseDownloadsMetricRepository { /// # Errors /// /// Returns an [`Error`] if the database operation fails. - pub(crate) fn increase_downloads_for_torrent(&self, info_hash: &InfoHash) -> Result<(), Error> { - let torrent = self.load_torrent_downloads(info_hash)?; + pub(crate) async fn increase_downloads_for_torrent(&self, info_hash: &InfoHash) -> Result<(), Error> { + let torrent = self.load_torrent_downloads(info_hash).await?; match torrent { - Some(_number_of_downloads) => self.database.increase_downloads_for_torrent(info_hash), - None => self.save_torrent_downloads(info_hash, 1), + Some(_number_of_downloads) => self.database.increase_downloads_for_torrent(info_hash).await, + None => self.save_torrent_downloads(info_hash, 1).await, } } @@ -76,8 +76,8 @@ impl DatabaseDownloadsMetricRepository { /// # Errors /// /// Returns an [`Error`] if the underlying database query fails. - pub(crate) fn load_all_torrents_downloads(&self) -> Result<NumberOfDownloadsBTreeMap, Error> { - self.database.load_all_torrents_downloads() + pub(crate) async fn load_all_torrents_downloads(&self) -> Result<NumberOfDownloadsBTreeMap, Error> { + self.database.load_all_torrents_downloads().await } /// Loads one persistent torrent metrics from the database. @@ -88,8 +88,8 @@ impl DatabaseDownloadsMetricRepository { /// # Errors /// /// Returns an [`Error`] if the underlying database query fails. - pub(crate) fn load_torrent_downloads(&self, info_hash: &InfoHash) -> Result<Option<NumberOfDownloads>, Error> { - self.database.load_torrent_downloads(info_hash) + pub(crate) async fn load_torrent_downloads(&self, info_hash: &InfoHash) -> Result<Option<NumberOfDownloads>, Error> { + self.database.load_torrent_downloads(info_hash).await } /// Saves the persistent torrent metric into the database. @@ -105,8 +105,8 @@ impl DatabaseDownloadsMetricRepository { /// # Errors /// /// Returns an [`Error`] if the database operation fails. - pub(crate) fn save_torrent_downloads(&self, info_hash: &InfoHash, downloaded: u32) -> Result<(), Error> { - self.database.save_torrent_downloads(info_hash, downloaded) + pub(crate) async fn save_torrent_downloads(&self, info_hash: &InfoHash, downloaded: u32) -> Result<(), Error> { + self.database.save_torrent_downloads(info_hash, downloaded).await } // Aggregate Metrics @@ -118,12 +118,12 @@ impl DatabaseDownloadsMetricRepository { /// # Errors /// /// Returns an [`Error`] if the database operation fails. - pub(crate) fn increase_global_downloads(&self) -> Result<(), Error> { - let torrent = self.database.load_global_downloads()?; + pub(crate) async fn increase_global_downloads(&self) -> Result<(), Error> { + let torrent = self.database.load_global_downloads().await?; match torrent { - Some(_number_of_downloads) => self.database.increase_global_downloads(), - None => self.database.save_global_downloads(1), + Some(_number_of_downloads) => self.database.increase_global_downloads().await, + None => self.database.save_global_downloads(1).await, } } @@ -132,8 +132,8 @@ impl DatabaseDownloadsMetricRepository { /// # Errors /// /// Returns an [`Error`] if the underlying database query fails. - pub(crate) fn load_global_downloads(&self) -> Result<Option<NumberOfDownloads>, Error> { - self.database.load_global_downloads() + pub(crate) async fn load_global_downloads(&self) -> Result<Option<NumberOfDownloads>, Error> { + self.database.load_global_downloads().await } } @@ -146,49 +146,49 @@ mod tests { use crate::databases::setup::initialize_database; use crate::test_helpers::tests::{ephemeral_configuration, sample_info_hash, sample_info_hash_one, sample_info_hash_two}; - fn initialize_db_persistent_torrent_repository() -> DatabaseDownloadsMetricRepository { + async fn initialize_db_persistent_torrent_repository() -> DatabaseDownloadsMetricRepository { let config = ephemeral_configuration(); - let stores = initialize_database(&config); + let stores = initialize_database(&config).await; DatabaseDownloadsMetricRepository::new(&stores.torrent_metrics_store) } - #[test] - fn it_saves_the_numbers_of_downloads_for_a_torrent_into_the_database() { - let repository = initialize_db_persistent_torrent_repository(); + #[tokio::test] + async fn it_saves_the_numbers_of_downloads_for_a_torrent_into_the_database() { + let repository = initialize_db_persistent_torrent_repository().await; let infohash = sample_info_hash(); - repository.save_torrent_downloads(&infohash, 1).unwrap(); + repository.save_torrent_downloads(&infohash, 1).await.unwrap(); - let torrents = repository.load_all_torrents_downloads().unwrap(); + let torrents = repository.load_all_torrents_downloads().await.unwrap(); assert_eq!(torrents.get(&infohash), Some(1).as_ref()); } - #[test] - fn it_increases_the_numbers_of_downloads_for_a_torrent_into_the_database() { - let repository = initialize_db_persistent_torrent_repository(); + #[tokio::test] + async fn it_increases_the_numbers_of_downloads_for_a_torrent_into_the_database() { + let repository = initialize_db_persistent_torrent_repository().await; let infohash = sample_info_hash(); - repository.increase_downloads_for_torrent(&infohash).unwrap(); + repository.increase_downloads_for_torrent(&infohash).await.unwrap(); - let torrents = repository.load_all_torrents_downloads().unwrap(); + let torrents = repository.load_all_torrents_downloads().await.unwrap(); assert_eq!(torrents.get(&infohash), Some(1).as_ref()); } - #[test] - fn it_loads_the_numbers_of_downloads_for_all_torrents_from_the_database() { - let repository = initialize_db_persistent_torrent_repository(); + #[tokio::test] + async fn it_loads_the_numbers_of_downloads_for_all_torrents_from_the_database() { + let repository = initialize_db_persistent_torrent_repository().await; let infohash_one = sample_info_hash_one(); let infohash_two = sample_info_hash_two(); - repository.save_torrent_downloads(&infohash_one, 1).unwrap(); - repository.save_torrent_downloads(&infohash_two, 2).unwrap(); + repository.save_torrent_downloads(&infohash_one, 1).await.unwrap(); + repository.save_torrent_downloads(&infohash_two, 2).await.unwrap(); - let torrents = repository.load_all_torrents_downloads().unwrap(); + let torrents = repository.load_all_torrents_downloads().await.unwrap(); let mut expected_torrents = NumberOfDownloadsBTreeMap::new(); expected_torrents.insert(infohash_one, 1); diff --git a/packages/tracker-core/src/statistics/persisted/mod.rs b/packages/tracker-core/src/statistics/persisted/mod.rs index 86c28370d..b808d9cf2 100644 --- a/packages/tracker-core/src/statistics/persisted/mod.rs +++ b/packages/tracker-core/src/statistics/persisted/mod.rs @@ -23,7 +23,7 @@ pub async fn load_persisted_metrics( db_downloads_metric_repository: &Arc<DatabaseDownloadsMetricRepository>, now: DurationSinceUnixEpoch, ) -> Result<(), Error> { - if let Some(downloads) = db_downloads_metric_repository.load_global_downloads()? { + if let Some(downloads) = db_downloads_metric_repository.load_global_downloads().await? { stats_repository .set_counter( &metric_name!(TRACKER_CORE_PERSISTENT_TORRENTS_DOWNLOADS_TOTAL), diff --git a/packages/tracker-core/src/test_helpers.rs b/packages/tracker-core/src/test_helpers.rs index 1d3b9e117..08677363e 100644 --- a/packages/tracker-core/src/test_helpers.rs +++ b/packages/tracker-core/src/test_helpers.rs @@ -129,8 +129,8 @@ pub(crate) mod tests { } #[must_use] - pub fn initialize_handlers(config: &Configuration) -> (Arc<AnnounceHandler>, Arc<ScrapeHandler>) { - let stores = initialize_database(&config.core); + pub async fn initialize_handlers(config: &Configuration) -> (Arc<AnnounceHandler>, Arc<ScrapeHandler>) { + let stores = initialize_database(&config.core).await; let in_memory_whitelist = Arc::new(InMemoryWhitelist::default()); let whitelist_authorization = Arc::new(whitelist::authorization::WhitelistAuthorization::new( &config.core, diff --git a/packages/tracker-core/src/torrent/manager.rs b/packages/tracker-core/src/torrent/manager.rs index 60ccb54eb..60b626328 100644 --- a/packages/tracker-core/src/torrent/manager.rs +++ b/packages/tracker-core/src/torrent/manager.rs @@ -70,8 +70,8 @@ impl TorrentsManager { /// /// Returns a `databases::error::Error` if unable to load the persistent /// torrent data. - pub fn load_torrents_from_database(&self) -> Result<(), databases::error::Error> { - let persistent_torrents = self.db_downloads_metric_repository.load_all_torrents_downloads()?; + pub async fn load_torrents_from_database(&self) -> Result<(), databases::error::Error> { + let persistent_torrents = self.db_downloads_metric_repository.load_all_torrents_downloads().await?; self.in_memory_torrent_repository.import_persistent(&persistent_torrents); @@ -161,15 +161,15 @@ mod tests { database_persistent_torrent_repository: Arc<DatabaseDownloadsMetricRepository>, } - fn initialize_torrents_manager() -> (Arc<TorrentsManager>, Arc<TorrentsManagerDeps>) { + async fn initialize_torrents_manager() -> (Arc<TorrentsManager>, Arc<TorrentsManagerDeps>) { let config = ephemeral_configuration(); - initialize_torrents_manager_with(config.clone()) + initialize_torrents_manager_with(config.clone()).await } - fn initialize_torrents_manager_with(config: Core) -> (Arc<TorrentsManager>, Arc<TorrentsManagerDeps>) { + async fn initialize_torrents_manager_with(config: Core) -> (Arc<TorrentsManager>, Arc<TorrentsManagerDeps>) { let swarms = Arc::new(Registry::default()); let in_memory_torrent_repository = Arc::new(InMemoryTorrentRepository::new(swarms)); - let database = initialize_database(&config); + let database = initialize_database(&config).await; let database_persistent_torrent_repository = Arc::new(DatabaseDownloadsMetricRepository::new(&database.torrent_metrics_store)); @@ -191,16 +191,17 @@ mod tests { #[tokio::test] async fn it_should_load_the_numbers_of_downloads_for_all_torrents_from_the_database() { - let (torrents_manager, services) = initialize_torrents_manager(); + let (torrents_manager, services) = initialize_torrents_manager().await; let infohash = sample_info_hash(); services .database_persistent_torrent_repository .save_torrent_downloads(&infohash, 1) + .await .unwrap(); - torrents_manager.load_torrents_from_database().unwrap(); + torrents_manager.load_torrents_from_database().await.unwrap(); assert_eq!( services @@ -231,7 +232,7 @@ mod tests { #[tokio::test] async fn it_should_remove_peers_that_have_not_been_updated_after_a_cutoff_time() { - let (torrents_manager, services) = initialize_torrents_manager(); + let (torrents_manager, services) = initialize_torrents_manager().await; let infohash = sample_info_hash(); @@ -273,7 +274,7 @@ mod tests { let mut config = ephemeral_configuration(); config.tracker_policy.remove_peerless_torrents = true; - let (torrents_manager, services) = initialize_torrents_manager_with(config); + let (torrents_manager, services) = initialize_torrents_manager_with(config).await; let infohash = sample_info_hash(); @@ -289,7 +290,7 @@ mod tests { let mut config = ephemeral_configuration(); config.tracker_policy.remove_peerless_torrents = false; - let (torrents_manager, services) = initialize_torrents_manager_with(config); + let (torrents_manager, services) = initialize_torrents_manager_with(config).await; let infohash = sample_info_hash(); diff --git a/packages/tracker-core/src/whitelist/manager.rs b/packages/tracker-core/src/whitelist/manager.rs index eed0f3a2e..bdef1eb81 100644 --- a/packages/tracker-core/src/whitelist/manager.rs +++ b/packages/tracker-core/src/whitelist/manager.rs @@ -50,7 +50,7 @@ impl WhitelistManager { /// # Errors /// Returns a `database::Error` if the operation fails in the database. pub async fn add_torrent_to_whitelist(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { - self.database_whitelist.add(info_hash)?; + self.database_whitelist.add(info_hash).await?; self.in_memory_whitelist.add(info_hash).await; Ok(()) } @@ -63,7 +63,7 @@ impl WhitelistManager { /// # Errors /// Returns a `database::Error` if the operation fails in the database. pub async fn remove_torrent_from_whitelist(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { - self.database_whitelist.remove(info_hash)?; + self.database_whitelist.remove(info_hash).await?; self.in_memory_whitelist.remove(info_hash).await; Ok(()) } @@ -76,7 +76,7 @@ impl WhitelistManager { /// # Errors /// Returns a `database::Error` if the operation fails to load from the database. pub async fn load_whitelist_from_database(&self) -> Result<(), databases::error::Error> { - let whitelisted_torrents_from_database = self.database_whitelist.load_from_database()?; + let whitelisted_torrents_from_database = self.database_whitelist.load_from_database().await?; self.in_memory_whitelist.clear().await; @@ -106,13 +106,13 @@ mod tests { pub in_memory_whitelist: Arc<InMemoryWhitelist>, } - fn initialize_whitelist_manager_for_whitelisted_tracker() -> (Arc<WhitelistManager>, Arc<WhitelistManagerDeps>) { + async fn initialize_whitelist_manager_for_whitelisted_tracker() -> (Arc<WhitelistManager>, Arc<WhitelistManagerDeps>) { let config = ephemeral_configuration_for_listed_tracker(); - initialize_whitelist_manager_and_deps(&config) + initialize_whitelist_manager_and_deps(&config).await } - fn initialize_whitelist_manager_and_deps(config: &Core) -> (Arc<WhitelistManager>, Arc<WhitelistManagerDeps>) { - let stores = initialize_database(config); + async fn initialize_whitelist_manager_and_deps(config: &Core) -> (Arc<WhitelistManager>, Arc<WhitelistManagerDeps>) { + let stores = initialize_database(config).await; let database_whitelist = Arc::new(DatabaseWhitelist::new(stores.whitelist_store.clone())); let in_memory_whitelist = Arc::new(InMemoryWhitelist::default()); @@ -135,19 +135,24 @@ mod tests { #[tokio::test] async fn it_should_add_a_torrent_to_the_whitelist() { - let (whitelist_manager, services) = initialize_whitelist_manager_for_whitelisted_tracker(); + let (whitelist_manager, services) = initialize_whitelist_manager_for_whitelisted_tracker().await; let info_hash = sample_info_hash(); whitelist_manager.add_torrent_to_whitelist(&info_hash).await.unwrap(); assert!(services.in_memory_whitelist.contains(&info_hash).await); - assert!(services.database_whitelist.load_from_database().unwrap().contains(&info_hash)); + assert!(services + .database_whitelist + .load_from_database() + .await + .unwrap() + .contains(&info_hash)); } #[tokio::test] async fn it_should_remove_a_torrent_from_the_whitelist() { - let (whitelist_manager, services) = initialize_whitelist_manager_for_whitelisted_tracker(); + let (whitelist_manager, services) = initialize_whitelist_manager_for_whitelisted_tracker().await; let info_hash = sample_info_hash(); @@ -156,7 +161,12 @@ mod tests { whitelist_manager.remove_torrent_from_whitelist(&info_hash).await.unwrap(); assert!(!services.in_memory_whitelist.contains(&info_hash).await); - assert!(!services.database_whitelist.load_from_database().unwrap().contains(&info_hash)); + assert!(!services + .database_whitelist + .load_from_database() + .await + .unwrap() + .contains(&info_hash)); } mod persistence { @@ -165,11 +175,11 @@ mod tests { #[tokio::test] async fn it_should_load_the_whitelist_from_the_database() { - let (whitelist_manager, services) = initialize_whitelist_manager_for_whitelisted_tracker(); + let (whitelist_manager, services) = initialize_whitelist_manager_for_whitelisted_tracker().await; let info_hash = sample_info_hash(); - services.database_whitelist.add(&info_hash).unwrap(); + services.database_whitelist.add(&info_hash).await.unwrap(); whitelist_manager.load_whitelist_from_database().await.unwrap(); diff --git a/packages/tracker-core/src/whitelist/mod.rs b/packages/tracker-core/src/whitelist/mod.rs index d9ad18311..a0dd7c23e 100644 --- a/packages/tracker-core/src/whitelist/mod.rs +++ b/packages/tracker-core/src/whitelist/mod.rs @@ -33,7 +33,7 @@ mod tests { #[tokio::test] async fn it_should_authorize_the_announce_and_scrape_actions_on_whitelisted_torrents() { - let (whitelist_authorization, whitelist_manager) = initialize_whitelist_services_for_listed_tracker(); + let (whitelist_authorization, whitelist_manager) = initialize_whitelist_services_for_listed_tracker().await; let info_hash = sample_info_hash(); @@ -46,7 +46,7 @@ mod tests { #[tokio::test] async fn it_should_not_authorize_the_announce_and_scrape_actions_on_not_whitelisted_torrents() { - let (whitelist_authorization, _whitelist_manager) = initialize_whitelist_services_for_listed_tracker(); + let (whitelist_authorization, _whitelist_manager) = initialize_whitelist_services_for_listed_tracker().await; let info_hash = sample_info_hash(); diff --git a/packages/tracker-core/src/whitelist/repository/persisted.rs b/packages/tracker-core/src/whitelist/repository/persisted.rs index 950ab13a0..aa78eb7c7 100644 --- a/packages/tracker-core/src/whitelist/repository/persisted.rs +++ b/packages/tracker-core/src/whitelist/repository/persisted.rs @@ -26,14 +26,14 @@ impl DatabaseWhitelist { /// # Errors /// Returns a `database::Error` if unable to add the `info_hash` to the /// whitelist. - pub(crate) fn add(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { - let is_whitelisted = self.database.is_info_hash_whitelisted(*info_hash)?; + pub(crate) async fn add(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { + let is_whitelisted = self.database.is_info_hash_whitelisted(*info_hash).await?; if is_whitelisted { return Ok(()); } - self.database.add_info_hash_to_whitelist(*info_hash)?; + self.database.add_info_hash_to_whitelist(*info_hash).await?; Ok(()) } @@ -42,14 +42,14 @@ impl DatabaseWhitelist { /// /// # Errors /// Returns a `database::Error` if unable to remove the `info_hash`. - pub(crate) fn remove(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { - let is_whitelisted = self.database.is_info_hash_whitelisted(*info_hash)?; + pub(crate) async fn remove(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { + let is_whitelisted = self.database.is_info_hash_whitelisted(*info_hash).await?; if !is_whitelisted { return Ok(()); } - self.database.remove_info_hash_from_whitelist(*info_hash)?; + self.database.remove_info_hash_from_whitelist(*info_hash).await?; Ok(()) } @@ -59,8 +59,8 @@ impl DatabaseWhitelist { /// # Errors /// Returns a `database::Error` if unable to load whitelisted `info_hash` /// values. - pub(crate) fn load_from_database(&self) -> Result<Vec<InfoHash>, databases::error::Error> { - self.database.load_whitelist() + pub(crate) async fn load_from_database(&self) -> Result<Vec<InfoHash>, databases::error::Error> { + self.database.load_whitelist().await } } @@ -72,68 +72,68 @@ mod tests { use crate::test_helpers::tests::{ephemeral_configuration_for_listed_tracker, sample_info_hash}; use crate::whitelist::repository::persisted::DatabaseWhitelist; - fn initialize_database_whitelist() -> DatabaseWhitelist { + async fn initialize_database_whitelist() -> DatabaseWhitelist { let configuration = ephemeral_configuration_for_listed_tracker(); - let stores = initialize_database(&configuration); + let stores = initialize_database(&configuration).await; DatabaseWhitelist::new(stores.whitelist_store) } - #[test] - fn should_add_a_new_infohash_to_the_list() { - let whitelist = initialize_database_whitelist(); + #[tokio::test] + async fn should_add_a_new_infohash_to_the_list() { + let whitelist = initialize_database_whitelist().await; let infohash = sample_info_hash(); - let _result = whitelist.add(&infohash); + let _result = whitelist.add(&infohash).await; - assert_eq!(whitelist.load_from_database().unwrap(), vec!(infohash)); + assert_eq!(whitelist.load_from_database().await.unwrap(), vec!(infohash)); } - #[test] - fn should_remove_a_infohash_from_the_list() { - let whitelist = initialize_database_whitelist(); + #[tokio::test] + async fn should_remove_a_infohash_from_the_list() { + let whitelist = initialize_database_whitelist().await; let infohash = sample_info_hash(); - let _result = whitelist.add(&infohash); + let _result = whitelist.add(&infohash).await; - let _result = whitelist.remove(&infohash); + let _result = whitelist.remove(&infohash).await; - assert_eq!(whitelist.load_from_database().unwrap(), vec!()); + assert_eq!(whitelist.load_from_database().await.unwrap(), vec!()); } - #[test] - fn should_load_all_infohashes_from_the_database() { - let whitelist = initialize_database_whitelist(); + #[tokio::test] + async fn should_load_all_infohashes_from_the_database() { + let whitelist = initialize_database_whitelist().await; let infohash = sample_info_hash(); - let _result = whitelist.add(&infohash); + let _result = whitelist.add(&infohash).await; - let result = whitelist.load_from_database().unwrap(); + let result = whitelist.load_from_database().await.unwrap(); assert_eq!(result, vec!(infohash)); } - #[test] - fn should_not_add_the_same_infohash_to_the_list_twice() { - let whitelist = initialize_database_whitelist(); + #[tokio::test] + async fn should_not_add_the_same_infohash_to_the_list_twice() { + let whitelist = initialize_database_whitelist().await; let infohash = sample_info_hash(); - let _result = whitelist.add(&infohash); - let _result = whitelist.add(&infohash); + let _result = whitelist.add(&infohash).await; + let _result = whitelist.add(&infohash).await; - assert_eq!(whitelist.load_from_database().unwrap(), vec!(infohash)); + assert_eq!(whitelist.load_from_database().await.unwrap(), vec!(infohash)); } - #[test] - fn should_not_fail_removing_an_infohash_that_is_not_in_the_list() { - let whitelist = initialize_database_whitelist(); + #[tokio::test] + async fn should_not_fail_removing_an_infohash_that_is_not_in_the_list() { + let whitelist = initialize_database_whitelist().await; let infohash = sample_info_hash(); - let result = whitelist.remove(&infohash); + let result = whitelist.remove(&infohash).await; assert!(result.is_ok()); } diff --git a/packages/tracker-core/src/whitelist/test_helpers.rs b/packages/tracker-core/src/whitelist/test_helpers.rs index c5f66e1df..4c30c35a7 100644 --- a/packages/tracker-core/src/whitelist/test_helpers.rs +++ b/packages/tracker-core/src/whitelist/test_helpers.rs @@ -17,8 +17,8 @@ pub(crate) mod tests { use crate::whitelist::setup::initialize_whitelist_manager; #[must_use] - pub fn initialize_whitelist_services(config: &Configuration) -> (Arc<WhitelistAuthorization>, Arc<WhitelistManager>) { - let stores = initialize_database(&config.core); + pub async fn initialize_whitelist_services(config: &Configuration) -> (Arc<WhitelistAuthorization>, Arc<WhitelistManager>) { + let stores = initialize_database(&config.core).await; let in_memory_whitelist = Arc::new(InMemoryWhitelist::default()); let whitelist_authorization = Arc::new(WhitelistAuthorization::new(&config.core, &in_memory_whitelist.clone())); let whitelist_manager = initialize_whitelist_manager(stores.whitelist_store.clone(), in_memory_whitelist.clone()); @@ -27,9 +27,9 @@ pub(crate) mod tests { } #[must_use] - pub fn initialize_whitelist_services_for_listed_tracker() -> (Arc<WhitelistAuthorization>, Arc<WhitelistManager>) { + pub async fn initialize_whitelist_services_for_listed_tracker() -> (Arc<WhitelistAuthorization>, Arc<WhitelistManager>) { use torrust_tracker_test_helpers::configuration; - initialize_whitelist_services(&configuration::ephemeral_listed()) + initialize_whitelist_services(&configuration::ephemeral_listed()).await } } diff --git a/packages/tracker-core/tests/common/test_env.rs b/packages/tracker-core/tests/common/test_env.rs index 3fe0464fe..c5f61366a 100644 --- a/packages/tracker-core/tests/common/test_env.rs +++ b/packages/tracker-core/tests/common/test_env.rs @@ -25,23 +25,21 @@ pub struct TestEnv { impl TestEnv { #[must_use] pub async fn started(core_config: Core) -> Self { - let test_env = TestEnv::new(core_config); + let test_env = TestEnv::new(core_config).await; test_env.start().await; test_env } #[must_use] - pub fn new(core_config: Core) -> Self { + pub async fn new(core_config: Core) -> Self { let core_config = Arc::new(core_config); let swarm_coordination_registry_container = Arc::new(SwarmCoordinationRegistryContainer::initialize( core_config.tracker_usage_statistics.into(), )); - let tracker_core_container = Arc::new(TrackerCoreContainer::initialize_from( - &core_config, - &swarm_coordination_registry_container, - )); + let tracker_core_container = + Arc::new(TrackerCoreContainer::initialize_from(&core_config, &swarm_coordination_registry_container).await); Self { swarm_coordination_registry_container, diff --git a/packages/tracker-core/tests/integration.rs b/packages/tracker-core/tests/integration.rs index b170aaebd..1c683923b 100644 --- a/packages/tracker-core/tests/integration.rs +++ b/packages/tracker-core/tests/integration.rs @@ -77,14 +77,35 @@ async fn it_should_persist_the_number_of_completed_peers_for_each_torrent_into_t // Ensure the swarm metadata is removed assert!(test_env.get_swarm_metadata(&info_hash).await.is_none()); - // Load torrents from the database to ensure the completed stats are persisted - test_env - .tracker_core_container - .torrents_manager - .load_torrents_from_database() - .unwrap(); + // Load torrents from the database to ensure the completed stats are persisted. + // Bound the wait with a timeout instead of a fixed iteration count so the + // test fails loudly on a stalled system rather than after an arbitrary + // number of immediate retries. Re-check the desired state (`downloads == 1`) + // inside the retry condition so an intermediate observation does not + // panic the test before the background listener has finished applying + // the persisted value. + let restored = tokio::time::timeout(std::time::Duration::from_secs(5), async { + loop { + test_env + .tracker_core_container + .torrents_manager + .load_torrents_from_database() + .await + .unwrap(); + + if let Some(swarm_metadata) = test_env.get_swarm_metadata(&info_hash).await { + if swarm_metadata.downloads() == 1 { + break true; + } + } - assert!(test_env.get_swarm_metadata(&info_hash).await.unwrap().downloads() == 1); + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + }) + .await + .unwrap_or(false); + + assert!(restored); } #[tokio::test] diff --git a/packages/udp-tracker-core/src/container.rs b/packages/udp-tracker-core/src/container.rs index 1d8b1d71c..e6db5aec6 100644 --- a/packages/udp-tracker-core/src/container.rs +++ b/packages/udp-tracker-core/src/container.rs @@ -31,15 +31,13 @@ pub struct UdpTrackerCoreContainer { impl UdpTrackerCoreContainer { #[must_use] - pub fn initialize(core_config: &Arc<Core>, udp_tracker_config: &Arc<UdpTracker>) -> Arc<UdpTrackerCoreContainer> { + pub async fn initialize(core_config: &Arc<Core>, udp_tracker_config: &Arc<UdpTracker>) -> Arc<UdpTrackerCoreContainer> { let swarm_coordination_registry_container = Arc::new(SwarmCoordinationRegistryContainer::initialize( core_config.tracker_usage_statistics.into(), )); - let tracker_core_container = Arc::new(TrackerCoreContainer::initialize_from( - core_config, - &swarm_coordination_registry_container, - )); + let tracker_core_container = + Arc::new(TrackerCoreContainer::initialize_from(core_config, &swarm_coordination_registry_container).await); Self::initialize_from_tracker_core(&tracker_core_container, udp_tracker_config) } diff --git a/packages/udp-tracker-server/src/environment.rs b/packages/udp-tracker-server/src/environment.rs index 13e18ba9b..36c5dcd1d 100644 --- a/packages/udp-tracker-server/src/environment.rs +++ b/packages/udp-tracker-server/src/environment.rs @@ -32,10 +32,10 @@ where impl Environment<Stopped> { #[allow(dead_code)] #[must_use] - pub fn new(configuration: &Arc<Configuration>) -> Self { + pub async fn new(configuration: &Arc<Configuration>) -> Self { initialize_global_services(configuration); - let container = Arc::new(EnvContainer::initialize(configuration)); + let container = Arc::new(EnvContainer::initialize(configuration).await); let bind_to = container.udp_tracker_core_container.udp_tracker_config.bind_address; @@ -112,7 +112,7 @@ impl Environment<Running> { /// /// Will panic if it cannot start the server within the timeout. pub async fn new(configuration: &Arc<Configuration>) -> Self { - tokio::time::timeout(DEFAULT_TIMEOUT, Environment::<Stopped>::new(configuration).start()) + tokio::time::timeout(DEFAULT_TIMEOUT, Environment::<Stopped>::new(configuration).await.start()) .await .expect("Failed to create a UDP tracker server running environment within the timeout") } @@ -179,7 +179,7 @@ impl EnvContainer { /// /// Will panic if the configuration is missing the UDP tracker configuration. #[must_use] - pub fn initialize(configuration: &Configuration) -> Self { + pub async fn initialize(configuration: &Configuration) -> Self { let core_config = Arc::new(configuration.core.clone()); let udp_tracker_configurations = configuration.udp_trackers.clone().expect("missing UDP tracker configuration"); let udp_tracker_config = Arc::new(udp_tracker_configurations[0].clone()); @@ -188,10 +188,8 @@ impl EnvContainer { core_config.tracker_usage_statistics.into(), )); - let tracker_core_container = Arc::new(TrackerCoreContainer::initialize_from( - &core_config, - &swarm_coordination_registry_container, - )); + let tracker_core_container = + Arc::new(TrackerCoreContainer::initialize_from(&core_config, &swarm_coordination_registry_container).await); let udp_tracker_core_container = UdpTrackerCoreContainer::initialize_from_tracker_core(&tracker_core_container, &udp_tracker_config); diff --git a/packages/udp-tracker-server/src/handlers/announce.rs b/packages/udp-tracker-server/src/handlers/announce.rs index 447ee7b83..b74de43a0 100644 --- a/packages/udp-tracker-server/src/handlers/announce.rs +++ b/packages/udp-tracker-server/src/handlers/announce.rs @@ -232,7 +232,7 @@ pub(crate) mod tests { #[tokio::test] async fn an_announced_peer_should_be_added_to_the_tracker() { let (core_tracker_services, core_udp_tracker_services, server_udp_tracker_services) = - initialize_core_tracker_services_for_public_tracker(); + initialize_core_tracker_services_for_public_tracker().await; let client_ip = Ipv4Addr::new(126, 0, 0, 1); let client_port = 8080; @@ -280,7 +280,7 @@ pub(crate) mod tests { #[tokio::test] async fn the_announced_peer_should_not_be_included_in_the_response() { let (core_tracker_services, core_udp_tracker_services, server_udp_tracker_services) = - initialize_core_tracker_services_for_public_tracker(); + initialize_core_tracker_services_for_public_tracker().await; let client_socket_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080); let server_socket_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 196)), 6969); @@ -324,7 +324,7 @@ pub(crate) mod tests { // "Do note that most trackers will only honor the IP address field under limited circumstances." let (core_tracker_services, core_udp_tracker_services, server_udp_tracker_services) = - initialize_core_tracker_services_for_public_tracker(); + initialize_core_tracker_services_for_public_tracker().await; let info_hash = AquaticInfoHash([0u8; 20]); let peer_id = AquaticPeerId([255u8; 20]); @@ -420,7 +420,7 @@ pub(crate) mod tests { #[tokio::test] async fn when_the_announce_request_comes_from_a_client_using_ipv4_the_response_should_not_include_peers_using_ipv6() { let (core_tracker_services, core_udp_tracker_services, _server_udp_tracker_services) = - initialize_core_tracker_services_for_public_tracker(); + initialize_core_tracker_services_for_public_tracker().await; add_a_torrent_peer_using_ipv6(&core_tracker_services.in_memory_torrent_repository).await; @@ -456,7 +456,7 @@ pub(crate) mod tests { Some(Arc::new(udp_server_stats_event_sender_mock)); let (core_tracker_services, core_udp_tracker_services, _server_udp_tracker_services) = - initialize_core_tracker_services_for_default_tracker_configuration(); + initialize_core_tracker_services_for_default_tracker_configuration().await; handle_announce( &core_udp_tracker_services.announce_service, @@ -489,7 +489,7 @@ pub(crate) mod tests { #[tokio::test] async fn the_peer_ip_should_be_changed_to_the_external_ip_in_the_tracker_configuration_if_defined() { let (core_tracker_services, core_udp_tracker_services, server_udp_tracker_services) = - initialize_core_tracker_services_for_public_tracker(); + initialize_core_tracker_services_for_public_tracker().await; let client_ip = Ipv4Addr::LOCALHOST; let client_port = 8080; @@ -573,7 +573,7 @@ pub(crate) mod tests { #[tokio::test] async fn an_announced_peer_should_be_added_to_the_tracker() { let (core_tracker_services, core_udp_tracker_services, server_udp_tracker_services) = - initialize_core_tracker_services_for_public_tracker(); + initialize_core_tracker_services_for_public_tracker().await; let client_ip_v4 = Ipv4Addr::new(126, 0, 0, 1); let client_ip_v6 = client_ip_v4.to_ipv6_compatible(); @@ -622,7 +622,7 @@ pub(crate) mod tests { #[tokio::test] async fn the_announced_peer_should_not_be_included_in_the_response() { let (core_tracker_services, core_udp_tracker_services, server_udp_tracker_services) = - initialize_core_tracker_services_for_public_tracker(); + initialize_core_tracker_services_for_public_tracker().await; let client_ip_v4 = Ipv4Addr::new(126, 0, 0, 1); let client_ip_v6 = client_ip_v4.to_ipv6_compatible(); @@ -669,7 +669,7 @@ pub(crate) mod tests { // "Do note that most trackers will only honor the IP address field under limited circumstances." let (core_tracker_services, core_udp_tracker_services, server_udp_tracker_service) = - initialize_core_tracker_services_for_public_tracker(); + initialize_core_tracker_services_for_public_tracker().await; let info_hash = AquaticInfoHash([0u8; 20]); let peer_id = AquaticPeerId([255u8; 20]); @@ -780,7 +780,7 @@ pub(crate) mod tests { #[tokio::test] async fn when_the_announce_request_comes_from_a_client_using_ipv6_the_response_should_not_include_peers_using_ipv4() { let (core_tracker_services, _core_udp_tracker_services, _server_udp_tracker_services) = - initialize_core_tracker_services_for_public_tracker(); + initialize_core_tracker_services_for_public_tracker().await; add_a_torrent_peer_using_ipv4(&core_tracker_services.in_memory_torrent_repository).await; @@ -823,7 +823,7 @@ pub(crate) mod tests { Some(Arc::new(udp_server_stats_event_sender_mock)); let (core_tracker_services, core_udp_tracker_services, _server_udp_tracker_services) = - initialize_core_tracker_services_for_default_tracker_configuration(); + initialize_core_tracker_services_for_default_tracker_configuration().await; handle_announce( &core_udp_tracker_services.announce_service, @@ -891,7 +891,7 @@ pub(crate) mod tests { let server_service_binding = ServiceBinding::new(Protocol::UDP, server_socket_addr).unwrap(); let server_service_binding_clone = server_service_binding.clone(); - let database = initialize_database(&config.core); + let database = initialize_database(&config.core).await; let in_memory_whitelist = Arc::new(InMemoryWhitelist::default()); let whitelist_authorization = Arc::new(WhitelistAuthorization::new(&config.core, &in_memory_whitelist.clone())); diff --git a/packages/udp-tracker-server/src/handlers/mod.rs b/packages/udp-tracker-server/src/handlers/mod.rs index 4aefb6b79..acbaed905 100644 --- a/packages/udp-tracker-server/src/handlers/mod.rs +++ b/packages/udp-tracker-server/src/handlers/mod.rs @@ -250,26 +250,26 @@ pub(crate) mod tests { configuration::ephemeral() } - pub(crate) fn initialize_core_tracker_services_for_default_tracker_configuration( + pub(crate) async fn initialize_core_tracker_services_for_default_tracker_configuration( ) -> (CoreTrackerServices, CoreUdpTrackerServices, ServerUdpTrackerServices) { - initialize_core_tracker_services(&default_testing_tracker_configuration()) + initialize_core_tracker_services(&default_testing_tracker_configuration()).await } - pub(crate) fn initialize_core_tracker_services_for_public_tracker( + pub(crate) async fn initialize_core_tracker_services_for_public_tracker( ) -> (CoreTrackerServices, CoreUdpTrackerServices, ServerUdpTrackerServices) { - initialize_core_tracker_services(&configuration::ephemeral_public()) + initialize_core_tracker_services(&configuration::ephemeral_public()).await } - pub(crate) fn initialize_core_tracker_services_for_listed_tracker( + pub(crate) async fn initialize_core_tracker_services_for_listed_tracker( ) -> (CoreTrackerServices, CoreUdpTrackerServices, ServerUdpTrackerServices) { - initialize_core_tracker_services(&configuration::ephemeral_listed()) + initialize_core_tracker_services(&configuration::ephemeral_listed()).await } - fn initialize_core_tracker_services( + async fn initialize_core_tracker_services( config: &Configuration, ) -> (CoreTrackerServices, CoreUdpTrackerServices, ServerUdpTrackerServices) { let core_config = Arc::new(config.core.clone()); - let database = initialize_database(&config.core); + let database = initialize_database(&config.core).await; let in_memory_whitelist = Arc::new(InMemoryWhitelist::default()); let whitelist_authorization = Arc::new(WhitelistAuthorization::new(&config.core, &in_memory_whitelist.clone())); let in_memory_torrent_repository = Arc::new(InMemoryTorrentRepository::default()); diff --git a/packages/udp-tracker-server/src/handlers/scrape.rs b/packages/udp-tracker-server/src/handlers/scrape.rs index 92160c2bd..8bd86f509 100644 --- a/packages/udp-tracker-server/src/handlers/scrape.rs +++ b/packages/udp-tracker-server/src/handlers/scrape.rs @@ -118,7 +118,7 @@ mod tests { #[tokio::test] async fn should_return_no_stats_when_the_tracker_does_not_have_any_torrent() { let (_core_tracker_services, core_udp_tracker_services, server_udp_tracker_services) = - initialize_core_tracker_services_for_public_tracker(); + initialize_core_tracker_services_for_public_tracker().await; let client_socket_addr = sample_ipv4_remote_addr(); let server_socket_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 196)), 6969); @@ -235,7 +235,7 @@ mod tests { #[tokio::test] async fn should_return_torrent_statistics_when_the_tracker_has_the_requested_torrent() { let (core_tracker_services, core_udp_tracker_services, _server_udp_tracker_services) = - initialize_core_tracker_services_for_public_tracker(); + initialize_core_tracker_services_for_public_tracker().await; let torrent_stats = match_scrape_response( add_a_sample_seeder_and_scrape(core_tracker_services.into(), core_udp_tracker_services.into()).await, @@ -268,7 +268,7 @@ mod tests { #[tokio::test] async fn should_return_the_torrent_statistics_when_the_requested_torrent_is_whitelisted() { let (core_tracker_services, core_udp_tracker_services, server_udp_tracker_services) = - initialize_core_tracker_services_for_listed_tracker(); + initialize_core_tracker_services_for_listed_tracker().await; let client_socket_addr = sample_ipv4_remote_addr(); let server_socket_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 196)), 6969); @@ -313,7 +313,7 @@ mod tests { #[tokio::test] async fn should_return_zeroed_statistics_when_the_requested_torrent_is_not_whitelisted() { let (core_tracker_services, core_udp_tracker_services, server_udp_tracker_services) = - initialize_core_tracker_services_for_listed_tracker(); + initialize_core_tracker_services_for_listed_tracker().await; let client_socket_addr = sample_ipv4_remote_addr(); let server_socket_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 196)), 6969); @@ -396,7 +396,7 @@ mod tests { Some(Arc::new(udp_server_stats_event_sender_mock)); let (_core_tracker_services, core_udp_tracker_services, _server_udp_tracker_services) = - initialize_core_tracker_services_for_default_tracker_configuration(); + initialize_core_tracker_services_for_default_tracker_configuration().await; handle_scrape( &core_udp_tracker_services.scrape_service, @@ -446,7 +446,7 @@ mod tests { Some(Arc::new(udp_server_stats_event_sender_mock)); let (_core_tracker_services, core_udp_tracker_services, _server_udp_tracker_services) = - initialize_core_tracker_services_for_default_tracker_configuration(); + initialize_core_tracker_services_for_default_tracker_configuration().await; handle_scrape( &core_udp_tracker_services.scrape_service, diff --git a/packages/udp-tracker-server/src/server/mod.rs b/packages/udp-tracker-server/src/server/mod.rs index f70e28b27..c46277e50 100644 --- a/packages/udp-tracker-server/src/server/mod.rs +++ b/packages/udp-tracker-server/src/server/mod.rs @@ -98,7 +98,7 @@ mod tests { let stopped = Server::new(Spawner::new(bind_to)); - let udp_tracker_core_container = UdpTrackerCoreContainer::initialize(&core_config, &udp_tracker_config); + let udp_tracker_core_container = UdpTrackerCoreContainer::initialize(&core_config, &udp_tracker_config).await; let udp_tracker_server_container = UdpTrackerServerContainer::initialize(&core_config); let started = stopped @@ -138,7 +138,7 @@ mod tests { let stopped = Server::new(Spawner::new(bind_to)); - let udp_tracker_core_container = UdpTrackerCoreContainer::initialize(&core_config, &udp_tracker_config); + let udp_tracker_core_container = UdpTrackerCoreContainer::initialize(&core_config, &udp_tracker_config).await; let udp_tracker_server_container = UdpTrackerServerContainer::initialize(&core_config); let started = stopped diff --git a/project-words.txt b/project-words.txt index 08ce61ebf..98ea65f62 100644 --- a/project-words.txt +++ b/project-words.txt @@ -81,6 +81,7 @@ fastrand fdbased fdget filesd +finalises flamegraph formatjson fput @@ -152,6 +153,7 @@ MSRV multimap myacicontext mysqladmin +mysqld ñaca Naim nanos @@ -242,6 +244,7 @@ subsec supertrait Swatinem Swiftbit +syscall sysmalloc sysret taiki @@ -250,6 +253,7 @@ tdyne Tebibytes tempfile Tera +testcontainer testcontainers thiserror timespec diff --git a/src/app.rs b/src/app.rs index 2149a6d4c..dc93710de 100644 --- a/src/app.rs +++ b/src/app.rs @@ -36,7 +36,7 @@ use crate::container::AppContainer; use crate::CurrentClock; pub async fn run() -> (Arc<AppContainer>, JobManager) { - let (config, app_container) = bootstrap::app::setup(); + let (config, app_container) = bootstrap::app::setup().await; let app_container = Arc::new(app_container); diff --git a/src/bootstrap/app.rs b/src/bootstrap/app.rs index bcf000dfd..4671ccbfd 100644 --- a/src/bootstrap/app.rs +++ b/src/bootstrap/app.rs @@ -23,10 +23,10 @@ use crate::container::AppContainer; /// /// # Panics /// -/// Setup can file if the configuration is invalid. +/// Setup can fail if the configuration is invalid. #[must_use] #[instrument(skip())] -pub fn setup() -> (Configuration, AppContainer) { +pub async fn setup() -> (Configuration, AppContainer) { #[cfg(not(test))] check_seed(); @@ -40,7 +40,7 @@ pub fn setup() -> (Configuration, AppContainer) { tracing::info!("Configuration:\n{}", configuration.clone().mask_secrets().to_json()); - let app_container = AppContainer::initialize(&configuration); + let app_container = AppContainer::initialize(&configuration).await; (configuration, app_container) } diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index 013031395..e10b3b6d3 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -94,7 +94,7 @@ mod tests { initialize_global_services(&cfg); - let http_tracker_container = HttpTrackerCoreContainer::initialize(&core_config, &http_tracker_config); + let http_tracker_container = HttpTrackerCoreContainer::initialize(&core_config, &http_tracker_config).await; let version = Version::V1; diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index 9f3964c20..2d5eb14af 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -121,7 +121,8 @@ mod tests { initialize_global_services(&cfg); let http_api_container = - TrackerHttpApiCoreContainer::initialize(&core_config, &http_tracker_config, &udp_tracker_config, &http_api_config); + TrackerHttpApiCoreContainer::initialize(&core_config, &http_tracker_config, &udp_tracker_config, &http_api_config) + .await; let version = Version::V1; diff --git a/src/container.rs b/src/container.rs index 7112a54e8..3fb88fafa 100644 --- a/src/container.rs +++ b/src/container.rs @@ -47,7 +47,7 @@ pub struct AppContainer { impl AppContainer { #[instrument(skip(configuration))] - pub fn initialize(configuration: &Configuration) -> AppContainer { + pub async fn initialize(configuration: &Configuration) -> AppContainer { // Configuration let core_config = Arc::new(configuration.core.clone()); @@ -66,10 +66,8 @@ impl AppContainer { // Core - let tracker_core_container = Arc::new(TrackerCoreContainer::initialize_from( - &core_config, - &swarm_coordination_registry_container, - )); + let tracker_core_container = + Arc::new(TrackerCoreContainer::initialize_from(&core_config, &swarm_coordination_registry_container).await); // HTTP