QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides handle-to-DID resolution with Redis-backed caching and queue processing.

feature: add sqlite handle resolver cache

+1727 -24
+581
Cargo.lock
··· 33 33 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 34 34 35 35 [[package]] 36 + name = "android-tzdata" 37 + version = "0.1.1" 38 + source = "registry+https://github.com/rust-lang/crates.io-index" 39 + checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 40 + 41 + [[package]] 42 + name = "android_system_properties" 43 + version = "0.1.5" 44 + source = "registry+https://github.com/rust-lang/crates.io-index" 45 + checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 46 + dependencies = [ 47 + "libc", 48 + ] 49 + 50 + [[package]] 36 51 name = "anstream" 37 52 version = "0.6.20" 38 53 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 103 118 "proc-macro2", 104 119 "quote", 105 120 "syn", 121 + ] 122 + 123 + [[package]] 124 + name = "atoi" 125 + version = "2.0.0" 126 + source = "registry+https://github.com/rust-lang/crates.io-index" 127 + checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 128 + dependencies = [ 129 + "num-traits", 106 130 ] 107 131 108 132 [[package]] ··· 270 294 version = "2.9.4" 271 295 source = "registry+https://github.com/rust-lang/crates.io-index" 272 296 checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 297 + dependencies = [ 298 + "serde", 299 + ] 273 300 274 301 [[package]] 275 302 name = "block-buffer" ··· 285 312 version = "3.19.0" 286 313 source = "registry+https://github.com/rust-lang/crates.io-index" 287 314 checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 315 + 316 + [[package]] 317 + name = "byteorder" 318 + version = "1.5.0" 319 + source = "registry+https://github.com/rust-lang/crates.io-index" 320 + checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 288 321 289 322 [[package]] 290 323 name = "bytes" ··· 324 357 checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 325 358 326 359 [[package]] 360 + name = "chrono" 361 + version = "0.4.41" 362 + source = "registry+https://github.com/rust-lang/crates.io-index" 363 + checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 364 + dependencies = [ 365 + "android-tzdata", 366 + "iana-time-zone", 367 + "num-traits", 368 + "windows-link", 369 + ] 370 + 371 + [[package]] 327 372 name = "cid" 328 373 version = "0.11.1" 329 374 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 398 443 ] 399 444 400 445 [[package]] 446 + name = "concurrent-queue" 447 + version = "2.5.0" 448 + source = "registry+https://github.com/rust-lang/crates.io-index" 449 + checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 450 + dependencies = [ 451 + "crossbeam-utils", 452 + ] 453 + 454 + [[package]] 401 455 name = "const-oid" 402 456 version = "0.9.6" 403 457 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 448 502 ] 449 503 450 504 [[package]] 505 + name = "crc" 506 + version = "3.3.0" 507 + source = "registry+https://github.com/rust-lang/crates.io-index" 508 + checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" 509 + dependencies = [ 510 + "crc-catalog", 511 + ] 512 + 513 + [[package]] 514 + name = "crc-catalog" 515 + version = "2.4.0" 516 + source = "registry+https://github.com/rust-lang/crates.io-index" 517 + checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 518 + 519 + [[package]] 451 520 name = "critical-section" 452 521 version = "1.2.0" 453 522 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 472 541 ] 473 542 474 543 [[package]] 544 + name = "crossbeam-queue" 545 + version = "0.3.12" 546 + source = "registry+https://github.com/rust-lang/crates.io-index" 547 + checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" 548 + dependencies = [ 549 + "crossbeam-utils", 550 + ] 551 + 552 + [[package]] 475 553 name = "crossbeam-utils" 476 554 version = "0.8.21" 477 555 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 591 669 ] 592 670 593 671 [[package]] 672 + name = "dotenvy" 673 + version = "0.15.7" 674 + source = "registry+https://github.com/rust-lang/crates.io-index" 675 + checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 676 + 677 + [[package]] 594 678 name = "ecdsa" 595 679 version = "0.16.9" 596 680 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 602 686 "rfc6979", 603 687 "signature", 604 688 "spki", 689 + ] 690 + 691 + [[package]] 692 + name = "either" 693 + version = "1.15.0" 694 + source = "registry+https://github.com/rust-lang/crates.io-index" 695 + checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 696 + dependencies = [ 697 + "serde", 605 698 ] 606 699 607 700 [[package]] ··· 666 759 ] 667 760 668 761 [[package]] 762 + name = "etcetera" 763 + version = "0.8.0" 764 + source = "registry+https://github.com/rust-lang/crates.io-index" 765 + checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" 766 + dependencies = [ 767 + "cfg-if", 768 + "home", 769 + "windows-sys 0.48.0", 770 + ] 771 + 772 + [[package]] 773 + name = "event-listener" 774 + version = "5.4.1" 775 + source = "registry+https://github.com/rust-lang/crates.io-index" 776 + checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" 777 + dependencies = [ 778 + "concurrent-queue", 779 + "parking", 780 + "pin-project-lite", 781 + ] 782 + 783 + [[package]] 669 784 name = "fastrand" 670 785 version = "2.3.0" 671 786 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 688 803 checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" 689 804 690 805 [[package]] 806 + name = "flume" 807 + version = "0.11.1" 808 + source = "registry+https://github.com/rust-lang/crates.io-index" 809 + checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" 810 + dependencies = [ 811 + "futures-core", 812 + "futures-sink", 813 + "spin", 814 + ] 815 + 816 + [[package]] 691 817 name = "fnv" 692 818 version = "1.0.7" 693 819 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 730 856 checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 731 857 dependencies = [ 732 858 "futures-core", 859 + "futures-sink", 733 860 ] 734 861 735 862 [[package]] ··· 739 866 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 740 867 741 868 [[package]] 869 + name = "futures-executor" 870 + version = "0.3.31" 871 + source = "registry+https://github.com/rust-lang/crates.io-index" 872 + checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 873 + dependencies = [ 874 + "futures-core", 875 + "futures-task", 876 + "futures-util", 877 + ] 878 + 879 + [[package]] 880 + name = "futures-intrusive" 881 + version = "0.5.0" 882 + source = "registry+https://github.com/rust-lang/crates.io-index" 883 + checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" 884 + dependencies = [ 885 + "futures-core", 886 + "lock_api", 887 + "parking_lot", 888 + ] 889 + 890 + [[package]] 742 891 name = "futures-io" 743 892 version = "0.3.31" 744 893 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 774 923 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 775 924 dependencies = [ 776 925 "futures-core", 926 + "futures-io", 777 927 "futures-macro", 778 928 "futures-sink", 779 929 "futures-task", 930 + "memchr", 780 931 "pin-project-lite", 781 932 "pin-utils", 782 933 "slab", ··· 882 1033 ] 883 1034 884 1035 [[package]] 1036 + name = "hashlink" 1037 + version = "0.10.0" 1038 + source = "registry+https://github.com/rust-lang/crates.io-index" 1039 + checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 1040 + dependencies = [ 1041 + "hashbrown", 1042 + ] 1043 + 1044 + [[package]] 885 1045 name = "heck" 886 1046 version = "0.5.0" 887 1047 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 894 1054 checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 895 1055 896 1056 [[package]] 1057 + name = "hex" 1058 + version = "0.4.3" 1059 + source = "registry+https://github.com/rust-lang/crates.io-index" 1060 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 1061 + 1062 + [[package]] 897 1063 name = "hickory-proto" 898 1064 version = "0.25.2" 899 1065 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 955 1121 checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 956 1122 dependencies = [ 957 1123 "digest", 1124 + ] 1125 + 1126 + [[package]] 1127 + name = "home" 1128 + version = "0.5.11" 1129 + source = "registry+https://github.com/rust-lang/crates.io-index" 1130 + checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 1131 + dependencies = [ 1132 + "windows-sys 0.59.0", 958 1133 ] 959 1134 960 1135 [[package]] ··· 1086 1261 ] 1087 1262 1088 1263 [[package]] 1264 + name = "iana-time-zone" 1265 + version = "0.1.63" 1266 + source = "registry+https://github.com/rust-lang/crates.io-index" 1267 + checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 1268 + dependencies = [ 1269 + "android_system_properties", 1270 + "core-foundation-sys", 1271 + "iana-time-zone-haiku", 1272 + "js-sys", 1273 + "log", 1274 + "wasm-bindgen", 1275 + "windows-core", 1276 + ] 1277 + 1278 + [[package]] 1279 + name = "iana-time-zone-haiku" 1280 + version = "0.1.2" 1281 + source = "registry+https://github.com/rust-lang/crates.io-index" 1282 + checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 1283 + dependencies = [ 1284 + "cc", 1285 + ] 1286 + 1287 + [[package]] 1089 1288 name = "icu_collections" 1090 1289 version = "2.0.0" 1091 1290 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1293 1492 version = "1.5.0" 1294 1493 source = "registry+https://github.com/rust-lang/crates.io-index" 1295 1494 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1495 + dependencies = [ 1496 + "spin", 1497 + ] 1296 1498 1297 1499 [[package]] 1298 1500 name = "libc" ··· 1301 1503 checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" 1302 1504 1303 1505 [[package]] 1506 + name = "libm" 1507 + version = "0.2.15" 1508 + source = "registry+https://github.com/rust-lang/crates.io-index" 1509 + checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 1510 + 1511 + [[package]] 1512 + name = "libredox" 1513 + version = "0.1.9" 1514 + source = "registry+https://github.com/rust-lang/crates.io-index" 1515 + checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" 1516 + dependencies = [ 1517 + "bitflags", 1518 + "libc", 1519 + "redox_syscall", 1520 + ] 1521 + 1522 + [[package]] 1523 + name = "libsqlite3-sys" 1524 + version = "0.30.1" 1525 + source = "registry+https://github.com/rust-lang/crates.io-index" 1526 + checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 1527 + dependencies = [ 1528 + "cc", 1529 + "pkg-config", 1530 + "vcpkg", 1531 + ] 1532 + 1533 + [[package]] 1304 1534 name = "linux-raw-sys" 1305 1535 version = "0.9.4" 1306 1536 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1370 1600 version = "0.8.4" 1371 1601 source = "registry+https://github.com/rust-lang/crates.io-index" 1372 1602 checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 1603 + 1604 + [[package]] 1605 + name = "md-5" 1606 + version = "0.10.6" 1607 + source = "registry+https://github.com/rust-lang/crates.io-index" 1608 + checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 1609 + dependencies = [ 1610 + "cfg-if", 1611 + "digest", 1612 + ] 1373 1613 1374 1614 [[package]] 1375 1615 name = "memchr" ··· 1487 1727 ] 1488 1728 1489 1729 [[package]] 1730 + name = "num-bigint-dig" 1731 + version = "0.8.4" 1732 + source = "registry+https://github.com/rust-lang/crates.io-index" 1733 + checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" 1734 + dependencies = [ 1735 + "byteorder", 1736 + "lazy_static", 1737 + "libm", 1738 + "num-integer", 1739 + "num-iter", 1740 + "num-traits", 1741 + "rand 0.8.5", 1742 + "smallvec", 1743 + "zeroize", 1744 + ] 1745 + 1746 + [[package]] 1490 1747 name = "num-integer" 1491 1748 version = "0.1.46" 1492 1749 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1496 1753 ] 1497 1754 1498 1755 [[package]] 1756 + name = "num-iter" 1757 + version = "0.1.45" 1758 + source = "registry+https://github.com/rust-lang/crates.io-index" 1759 + checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 1760 + dependencies = [ 1761 + "autocfg", 1762 + "num-integer", 1763 + "num-traits", 1764 + ] 1765 + 1766 + [[package]] 1499 1767 name = "num-traits" 1500 1768 version = "0.2.19" 1501 1769 source = "registry+https://github.com/rust-lang/crates.io-index" 1502 1770 checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1503 1771 dependencies = [ 1504 1772 "autocfg", 1773 + "libm", 1505 1774 ] 1506 1775 1507 1776 [[package]] ··· 1608 1877 ] 1609 1878 1610 1879 [[package]] 1880 + name = "parking" 1881 + version = "2.2.1" 1882 + source = "registry+https://github.com/rust-lang/crates.io-index" 1883 + checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 1884 + 1885 + [[package]] 1611 1886 name = "parking_lot" 1612 1887 version = "0.12.4" 1613 1888 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1656 1931 version = "0.1.0" 1657 1932 source = "registry+https://github.com/rust-lang/crates.io-index" 1658 1933 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1934 + 1935 + [[package]] 1936 + name = "pkcs1" 1937 + version = "0.7.5" 1938 + source = "registry+https://github.com/rust-lang/crates.io-index" 1939 + checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 1940 + dependencies = [ 1941 + "der", 1942 + "pkcs8", 1943 + "spki", 1944 + ] 1659 1945 1660 1946 [[package]] 1661 1947 name = "pkcs8" ··· 1731 2017 "reqwest", 1732 2018 "serde", 1733 2019 "serde_json", 2020 + "sqlx", 1734 2021 "thiserror 2.0.16", 1735 2022 "tokio", 1736 2023 "tokio-util", ··· 1995 2282 ] 1996 2283 1997 2284 [[package]] 2285 + name = "rsa" 2286 + version = "0.9.8" 2287 + source = "registry+https://github.com/rust-lang/crates.io-index" 2288 + checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" 2289 + dependencies = [ 2290 + "const-oid", 2291 + "digest", 2292 + "num-bigint-dig", 2293 + "num-integer", 2294 + "num-traits", 2295 + "pkcs1", 2296 + "pkcs8", 2297 + "rand_core 0.6.4", 2298 + "signature", 2299 + "spki", 2300 + "subtle", 2301 + "zeroize", 2302 + ] 2303 + 2304 + [[package]] 1998 2305 name = "rustc-demangle" 1999 2306 version = "0.1.26" 2000 2307 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2251 2558 ] 2252 2559 2253 2560 [[package]] 2561 + name = "sha1" 2562 + version = "0.10.6" 2563 + source = "registry+https://github.com/rust-lang/crates.io-index" 2564 + checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 2565 + dependencies = [ 2566 + "cfg-if", 2567 + "cpufeatures", 2568 + "digest", 2569 + ] 2570 + 2571 + [[package]] 2254 2572 name = "sha2" 2255 2573 version = "0.10.9" 2256 2574 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2306 2624 version = "1.15.1" 2307 2625 source = "registry+https://github.com/rust-lang/crates.io-index" 2308 2626 checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 2627 + dependencies = [ 2628 + "serde", 2629 + ] 2309 2630 2310 2631 [[package]] 2311 2632 name = "socket2" ··· 2328 2649 ] 2329 2650 2330 2651 [[package]] 2652 + name = "spin" 2653 + version = "0.9.8" 2654 + source = "registry+https://github.com/rust-lang/crates.io-index" 2655 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 2656 + dependencies = [ 2657 + "lock_api", 2658 + ] 2659 + 2660 + [[package]] 2331 2661 name = "spki" 2332 2662 version = "0.7.3" 2333 2663 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2338 2668 ] 2339 2669 2340 2670 [[package]] 2671 + name = "sqlx" 2672 + version = "0.8.6" 2673 + source = "registry+https://github.com/rust-lang/crates.io-index" 2674 + checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" 2675 + dependencies = [ 2676 + "sqlx-core", 2677 + "sqlx-macros", 2678 + "sqlx-mysql", 2679 + "sqlx-postgres", 2680 + "sqlx-sqlite", 2681 + ] 2682 + 2683 + [[package]] 2684 + name = "sqlx-core" 2685 + version = "0.8.6" 2686 + source = "registry+https://github.com/rust-lang/crates.io-index" 2687 + checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" 2688 + dependencies = [ 2689 + "base64", 2690 + "bytes", 2691 + "chrono", 2692 + "crc", 2693 + "crossbeam-queue", 2694 + "either", 2695 + "event-listener", 2696 + "futures-core", 2697 + "futures-intrusive", 2698 + "futures-io", 2699 + "futures-util", 2700 + "hashbrown", 2701 + "hashlink", 2702 + "indexmap", 2703 + "log", 2704 + "memchr", 2705 + "once_cell", 2706 + "percent-encoding", 2707 + "serde", 2708 + "serde_json", 2709 + "sha2", 2710 + "smallvec", 2711 + "thiserror 2.0.16", 2712 + "tokio", 2713 + "tokio-stream", 2714 + "tracing", 2715 + "url", 2716 + ] 2717 + 2718 + [[package]] 2719 + name = "sqlx-macros" 2720 + version = "0.8.6" 2721 + source = "registry+https://github.com/rust-lang/crates.io-index" 2722 + checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" 2723 + dependencies = [ 2724 + "proc-macro2", 2725 + "quote", 2726 + "sqlx-core", 2727 + "sqlx-macros-core", 2728 + "syn", 2729 + ] 2730 + 2731 + [[package]] 2732 + name = "sqlx-macros-core" 2733 + version = "0.8.6" 2734 + source = "registry+https://github.com/rust-lang/crates.io-index" 2735 + checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" 2736 + dependencies = [ 2737 + "dotenvy", 2738 + "either", 2739 + "heck", 2740 + "hex", 2741 + "once_cell", 2742 + "proc-macro2", 2743 + "quote", 2744 + "serde", 2745 + "serde_json", 2746 + "sha2", 2747 + "sqlx-core", 2748 + "sqlx-mysql", 2749 + "sqlx-postgres", 2750 + "sqlx-sqlite", 2751 + "syn", 2752 + "tokio", 2753 + "url", 2754 + ] 2755 + 2756 + [[package]] 2757 + name = "sqlx-mysql" 2758 + version = "0.8.6" 2759 + source = "registry+https://github.com/rust-lang/crates.io-index" 2760 + checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" 2761 + dependencies = [ 2762 + "atoi", 2763 + "base64", 2764 + "bitflags", 2765 + "byteorder", 2766 + "bytes", 2767 + "chrono", 2768 + "crc", 2769 + "digest", 2770 + "dotenvy", 2771 + "either", 2772 + "futures-channel", 2773 + "futures-core", 2774 + "futures-io", 2775 + "futures-util", 2776 + "generic-array", 2777 + "hex", 2778 + "hkdf", 2779 + "hmac", 2780 + "itoa", 2781 + "log", 2782 + "md-5", 2783 + "memchr", 2784 + "once_cell", 2785 + "percent-encoding", 2786 + "rand 0.8.5", 2787 + "rsa", 2788 + "serde", 2789 + "sha1", 2790 + "sha2", 2791 + "smallvec", 2792 + "sqlx-core", 2793 + "stringprep", 2794 + "thiserror 2.0.16", 2795 + "tracing", 2796 + "whoami", 2797 + ] 2798 + 2799 + [[package]] 2800 + name = "sqlx-postgres" 2801 + version = "0.8.6" 2802 + source = "registry+https://github.com/rust-lang/crates.io-index" 2803 + checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" 2804 + dependencies = [ 2805 + "atoi", 2806 + "base64", 2807 + "bitflags", 2808 + "byteorder", 2809 + "chrono", 2810 + "crc", 2811 + "dotenvy", 2812 + "etcetera", 2813 + "futures-channel", 2814 + "futures-core", 2815 + "futures-util", 2816 + "hex", 2817 + "hkdf", 2818 + "hmac", 2819 + "home", 2820 + "itoa", 2821 + "log", 2822 + "md-5", 2823 + "memchr", 2824 + "once_cell", 2825 + "rand 0.8.5", 2826 + "serde", 2827 + "serde_json", 2828 + "sha2", 2829 + "smallvec", 2830 + "sqlx-core", 2831 + "stringprep", 2832 + "thiserror 2.0.16", 2833 + "tracing", 2834 + "whoami", 2835 + ] 2836 + 2837 + [[package]] 2838 + name = "sqlx-sqlite" 2839 + version = "0.8.6" 2840 + source = "registry+https://github.com/rust-lang/crates.io-index" 2841 + checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" 2842 + dependencies = [ 2843 + "atoi", 2844 + "chrono", 2845 + "flume", 2846 + "futures-channel", 2847 + "futures-core", 2848 + "futures-executor", 2849 + "futures-intrusive", 2850 + "futures-util", 2851 + "libsqlite3-sys", 2852 + "log", 2853 + "percent-encoding", 2854 + "serde", 2855 + "serde_urlencoded", 2856 + "sqlx-core", 2857 + "thiserror 2.0.16", 2858 + "tracing", 2859 + "url", 2860 + ] 2861 + 2862 + [[package]] 2341 2863 name = "stable_deref_trait" 2342 2864 version = "1.2.0" 2343 2865 source = "registry+https://github.com/rust-lang/crates.io-index" 2344 2866 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 2867 + 2868 + [[package]] 2869 + name = "stringprep" 2870 + version = "0.1.5" 2871 + source = "registry+https://github.com/rust-lang/crates.io-index" 2872 + checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" 2873 + dependencies = [ 2874 + "unicode-bidi", 2875 + "unicode-normalization", 2876 + "unicode-properties", 2877 + ] 2345 2878 2346 2879 [[package]] 2347 2880 name = "strsim" ··· 2551 3084 ] 2552 3085 2553 3086 [[package]] 3087 + name = "tokio-stream" 3088 + version = "0.1.17" 3089 + source = "registry+https://github.com/rust-lang/crates.io-index" 3090 + checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 3091 + dependencies = [ 3092 + "futures-core", 3093 + "pin-project-lite", 3094 + "tokio", 3095 + ] 3096 + 3097 + [[package]] 2554 3098 name = "tokio-util" 2555 3099 version = "0.7.16" 2556 3100 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2685 3229 checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 2686 3230 2687 3231 [[package]] 3232 + name = "unicode-bidi" 3233 + version = "0.3.18" 3234 + source = "registry+https://github.com/rust-lang/crates.io-index" 3235 + checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 3236 + 3237 + [[package]] 2688 3238 name = "unicode-ident" 2689 3239 version = "1.0.18" 2690 3240 source = "registry+https://github.com/rust-lang/crates.io-index" 2691 3241 checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 2692 3242 2693 3243 [[package]] 3244 + name = "unicode-normalization" 3245 + version = "0.1.24" 3246 + source = "registry+https://github.com/rust-lang/crates.io-index" 3247 + checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 3248 + dependencies = [ 3249 + "tinyvec", 3250 + ] 3251 + 3252 + [[package]] 3253 + name = "unicode-properties" 3254 + version = "0.1.3" 3255 + source = "registry+https://github.com/rust-lang/crates.io-index" 3256 + checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 3257 + 3258 + [[package]] 2694 3259 name = "unsigned-varint" 2695 3260 version = "0.8.0" 2696 3261 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2792 3357 ] 2793 3358 2794 3359 [[package]] 3360 + name = "wasite" 3361 + version = "0.1.0" 3362 + source = "registry+https://github.com/rust-lang/crates.io-index" 3363 + checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 3364 + 3365 + [[package]] 2795 3366 name = "wasm-bindgen" 2796 3367 version = "0.2.101" 2797 3368 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2890 3461 checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" 2891 3462 dependencies = [ 2892 3463 "rustls-pki-types", 3464 + ] 3465 + 3466 + [[package]] 3467 + name = "whoami" 3468 + version = "1.6.1" 3469 + source = "registry+https://github.com/rust-lang/crates.io-index" 3470 + checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" 3471 + dependencies = [ 3472 + "libredox", 3473 + "wasite", 2893 3474 ] 2894 3475 2895 3476 [[package]]
+1
Cargo.toml
··· 26 26 reqwest = { version = "0.12", features = ["json"] } 27 27 serde = { version = "1.0", features = ["derive"] } 28 28 serde_json = "1.0" 29 + sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "chrono"] } 29 30 thiserror = "2.0" 30 31 tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "signal", "sync", "time", "net", "fs"] } 31 32 tokio-util = { version = "0.7", features = ["rt"] }
+158 -13
docs/configuration-reference.md
··· 162 162 163 163 ### `REDIS_URL` 164 164 165 - **Required**: No (but highly recommended for production) 165 + **Required**: No (recommended for multi-instance production) 166 166 **Type**: String 167 167 **Format**: Redis connection URL 168 168 ··· 186 186 REDIS_URL=rediss://secure-redis.example.com:6380/0 187 187 ``` 188 188 189 + ### `SQLITE_URL` 190 + 191 + **Required**: No (recommended for single-instance production) 192 + **Type**: String 193 + **Format**: SQLite database URL 194 + 195 + SQLite database URL for persistent caching. Provides single-file persistent storage without external dependencies. 196 + 197 + **Examples**: 198 + ```bash 199 + # File-based database (recommended) 200 + SQLITE_URL=sqlite:./quickdid.db 201 + 202 + # With absolute path 203 + SQLITE_URL=sqlite:/var/lib/quickdid/cache.db 204 + 205 + # In-memory database (testing only) 206 + SQLITE_URL=sqlite::memory: 207 + 208 + # Alternative file syntax 209 + SQLITE_URL=sqlite:///path/to/database.db 210 + ``` 211 + 212 + **Cache Priority**: QuickDID uses the first available cache: 213 + 1. Redis (if `REDIS_URL` is configured) 214 + 2. SQLite (if `SQLITE_URL` is configured) 215 + 3. In-memory cache (fallback) 216 + 189 217 ### `CACHE_TTL_MEMORY` 190 218 191 219 **Required**: No ··· 229 257 CACHE_TTL_REDIS=7776000 # 90 days (default, maximum stability) 230 258 ``` 231 259 232 - **Recommendations**: 260 + ### `CACHE_TTL_SQLITE` 261 + 262 + **Required**: No 263 + **Type**: Integer (seconds) 264 + **Default**: `7776000` (90 days) 265 + **Range**: 3600-31536000 (1 hour to 1 year) 266 + **Constraints**: Must be > 0 267 + 268 + Time-to-live for SQLite cache entries in seconds. Only used when `SQLITE_URL` is configured. 269 + 270 + **Examples**: 271 + ```bash 272 + CACHE_TTL_SQLITE=3600 # 1 hour (frequently changing data) 273 + CACHE_TTL_SQLITE=86400 # 1 day (recommended for active handles) 274 + CACHE_TTL_SQLITE=604800 # 1 week (balanced) 275 + CACHE_TTL_SQLITE=2592000 # 30 days (stable handles) 276 + CACHE_TTL_SQLITE=7776000 # 90 days (default, maximum stability) 277 + ``` 278 + 279 + **TTL Recommendations**: 233 280 - Social media handles: 1-7 days 234 281 - Corporate/stable handles: 30-90 days 235 282 - Test environments: 1 hour 283 + - Single-instance deployments: Can use longer TTLs (30-90 days) 284 + - Multi-instance deployments: Use shorter TTLs (1-7 days) 236 285 237 286 ## Queue Configuration 238 287 ··· 377 426 RUST_LOG=debug 378 427 ``` 379 428 380 - ### Standard Production Configuration 429 + ### Standard Production Configuration (Redis) 381 430 382 431 ```bash 383 - # .env.production 432 + # .env.production.redis 384 433 # Required 385 434 HTTP_EXTERNAL=quickdid.example.com 386 435 SERVICE_KEY=${SECRET_SERVICE_KEY} # From secrets manager ··· 389 438 HTTP_PORT=8080 390 439 USER_AGENT=quickdid/1.0.0 (+https://quickdid.example.com) 391 440 392 - # Caching 441 + # Caching (Redis-based) 393 442 REDIS_URL=redis://redis:6379/0 394 443 CACHE_TTL_MEMORY=600 395 444 CACHE_TTL_REDIS=86400 # 1 day ··· 403 452 RUST_LOG=info 404 453 ``` 405 454 406 - ### High-Availability Configuration 455 + ### Standard Production Configuration (SQLite) 456 + 457 + ```bash 458 + # .env.production.sqlite 459 + # Required 460 + HTTP_EXTERNAL=quickdid.example.com 461 + SERVICE_KEY=${SECRET_SERVICE_KEY} # From secrets manager 462 + 463 + # Network 464 + HTTP_PORT=8080 465 + USER_AGENT=quickdid/1.0.0 (+https://quickdid.example.com) 466 + 467 + # Caching (SQLite-based for single instance) 468 + SQLITE_URL=sqlite:/data/quickdid.db 469 + CACHE_TTL_MEMORY=600 470 + CACHE_TTL_SQLITE=86400 # 1 day 471 + 472 + # Queue (MPSC for single instance) 473 + QUEUE_ADAPTER=mpsc 474 + QUEUE_BUFFER_SIZE=5000 475 + 476 + # Logging 477 + RUST_LOG=info 478 + ``` 479 + 480 + ### High-Availability Configuration (Redis) 407 481 408 482 ```bash 409 - # .env.ha 483 + # .env.ha.redis 410 484 # Required 411 485 HTTP_EXTERNAL=quickdid.example.com 412 486 SERVICE_KEY=${SECRET_SERVICE_KEY} ··· 434 508 RUST_LOG=warn 435 509 ``` 436 510 437 - ### Docker Compose Configuration 511 + ### Hybrid Configuration (Redis + SQLite Fallback) 512 + 513 + ```bash 514 + # .env.hybrid 515 + # Required 516 + HTTP_EXTERNAL=quickdid.example.com 517 + SERVICE_KEY=${SECRET_SERVICE_KEY} 518 + 519 + # Network 520 + HTTP_PORT=8080 521 + 522 + # Caching (Redis primary, SQLite fallback) 523 + REDIS_URL=redis://redis:6379/0 524 + SQLITE_URL=sqlite:/data/fallback.db 525 + CACHE_TTL_MEMORY=600 526 + CACHE_TTL_REDIS=86400 527 + CACHE_TTL_SQLITE=604800 # 1 week (longer for fallback) 528 + 529 + # Queue 530 + QUEUE_ADAPTER=redis 531 + QUEUE_REDIS_TIMEOUT=5 532 + 533 + # Logging 534 + RUST_LOG=info 535 + ``` 536 + 537 + ### Docker Compose Configuration (Redis) 438 538 439 539 ```yaml 540 + # docker-compose.redis.yml 440 541 version: '3.8' 441 542 442 543 services: ··· 462 563 command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru 463 564 ``` 464 565 566 + ### Docker Compose Configuration (SQLite) 567 + 568 + ```yaml 569 + # docker-compose.sqlite.yml 570 + version: '3.8' 571 + 572 + services: 573 + quickdid: 574 + image: quickdid:latest 575 + environment: 576 + HTTP_EXTERNAL: quickdid.example.com 577 + SERVICE_KEY: ${SERVICE_KEY} 578 + HTTP_PORT: 8080 579 + SQLITE_URL: sqlite:/data/quickdid.db 580 + CACHE_TTL_MEMORY: 600 581 + CACHE_TTL_SQLITE: 86400 582 + QUEUE_ADAPTER: mpsc 583 + QUEUE_BUFFER_SIZE: 5000 584 + RUST_LOG: info 585 + ports: 586 + - "8080:8080" 587 + volumes: 588 + - quickdid-data:/data 589 + 590 + volumes: 591 + quickdid-data: 592 + driver: local 593 + ``` 594 + 465 595 ## Validation Rules 466 596 467 597 QuickDID validates configuration at startup. The following rules are enforced: ··· 473 603 474 604 ### Value Constraints 475 605 476 - 1. **TTL Values** (`CACHE_TTL_MEMORY`, `CACHE_TTL_REDIS`): 606 + 1. **TTL Values** (`CACHE_TTL_MEMORY`, `CACHE_TTL_REDIS`, `CACHE_TTL_SQLITE`): 477 607 - Must be positive integers (> 0) 478 608 - Recommended minimum: 60 seconds 479 609 ··· 526 656 ### Performance 527 657 528 658 1. **With Redis**: Use lower memory cache TTL (300-600s) 529 - 2. **Without Redis**: Use higher memory cache TTL (1800-3600s) 530 - 3. **High traffic**: Increase QUEUE_BUFFER_SIZE (5000-10000) 531 - 4. **Multi-region**: Use region-specific QUEUE_WORKER_ID 659 + 2. **With SQLite**: Use moderate memory cache TTL (600-1800s) 660 + 3. **Without persistent cache**: Use higher memory cache TTL (1800-3600s) 661 + 4. **High traffic**: Increase QUEUE_BUFFER_SIZE (5000-10000) 662 + 5. **Multi-region**: Use region-specific QUEUE_WORKER_ID 663 + 664 + ### Caching Strategy 665 + 666 + 1. **Multi-instance/HA deployments**: Use Redis for distributed caching 667 + 2. **Single-instance deployments**: Use SQLite for persistent caching 668 + 3. **Development/testing**: Use memory-only caching 669 + 4. **Hybrid setups**: Configure both Redis and SQLite for redundancy 670 + 5. **Cache TTL guidelines**: 671 + - Redis: Shorter TTLs (1-7 days) for frequently updated handles 672 + - SQLite: Longer TTLs (7-90 days) for stable single-instance caching 673 + - Memory: Short TTLs (5-30 minutes) as fallback 532 674 533 675 ### Monitoring 534 676 ··· 543 685 2. Use secrets management for production SERVICE_KEY 544 686 3. Set resource limits in container orchestration 545 687 4. Use health checks to monitor service availability 546 - 5. Implement gradual rollouts with feature flags 688 + 5. Implement gradual rollouts with feature flags 689 + 6. **SQLite deployments**: Ensure persistent volume for database file 690 + 7. **Redis deployments**: Configure Redis persistence and backup 691 + 8. **Hybrid deployments**: Test fallback scenarios (Redis unavailable)
+155 -8
docs/production-deployment.md
··· 1 1 # QuickDID Production Deployment Guide 2 2 3 - This guide provides comprehensive instructions for deploying QuickDID in a production environment using Docker. 3 + This guide provides comprehensive instructions for deploying QuickDID in a production environment using Docker. QuickDID supports multiple caching strategies: Redis (distributed), SQLite (single-instance), or in-memory caching. 4 4 5 5 ## Table of Contents 6 6 ··· 17 17 - Docker 20.10.0 or higher 18 18 - Docker Compose 2.0.0 or higher (optional, for multi-container setup) 19 19 - Redis 6.0 or higher (optional, for persistent caching and queue management) 20 + - SQLite 3.35 or higher (optional, alternative to Redis for single-instance caching) 20 21 - Valid SSL certificates for HTTPS (recommended for production) 21 22 - Domain name configured with appropriate DNS records 22 23 ··· 66 67 # CACHING CONFIGURATION 67 68 # ---------------------------------------------------------------------------- 68 69 69 - # Redis connection URL for caching (highly recommended for production) 70 + # Redis connection URL for caching (recommended for production) 70 71 # Format: redis://[username:password@]host:port/database 71 72 # Examples: 72 73 # - redis://localhost:6379/0 (local Redis, no auth) ··· 76 77 # Benefits: Persistent cache, distributed caching, better performance 77 78 REDIS_URL=redis://redis:6379/0 78 79 80 + # SQLite database URL for caching (alternative to Redis for single-instance deployments) 81 + # Format: sqlite:path/to/database.db 82 + # Examples: 83 + # - sqlite:./quickdid.db (file-based database) 84 + # - sqlite::memory: (in-memory database for testing) 85 + # - sqlite:/var/lib/quickdid/cache.db (absolute path) 86 + # Benefits: Persistent cache, single-file storage, no external dependencies 87 + # Note: Cache priority is Redis > SQLite > Memory (first available is used) 88 + # SQLITE_URL=sqlite:./quickdid.db 89 + 79 90 # TTL for in-memory cache in seconds (default: 600 = 10 minutes) 80 91 # Range: 60-3600 recommended 81 92 # Lower = fresher data, more DNS/HTTP lookups ··· 89 100 # - 604800 (1 week) for balanced performance 90 101 # - 7776000 (90 days) for stable data 91 102 CACHE_TTL_REDIS=86400 103 + 104 + # TTL for SQLite cache in seconds (default: 7776000 = 90 days) 105 + # Range: 3600-31536000 (1 hour to 1 year) 106 + # Same recommendations as Redis TTL 107 + # Only used when SQLITE_URL is configured 108 + CACHE_TTL_SQLITE=86400 92 109 93 110 # ---------------------------------------------------------------------------- 94 111 # QUEUE CONFIGURATION ··· 263 280 264 281 ## Docker Compose Setup 265 282 266 - Create a `docker-compose.yml` file for a complete production setup: 283 + ### Redis-based Production Setup 284 + 285 + Create a `docker-compose.yml` file for a complete production setup with Redis: 267 286 268 287 ```yaml 269 288 version: '3.8' ··· 352 371 driver: local 353 372 ``` 354 373 374 + ### SQLite-based Single-Instance Setup 375 + 376 + For single-instance deployments without Redis, create a simpler `docker-compose.sqlite.yml`: 377 + 378 + ```yaml 379 + version: '3.8' 380 + 381 + services: 382 + quickdid: 383 + image: quickdid:latest 384 + container_name: quickdid-sqlite 385 + environment: 386 + HTTP_EXTERNAL: quickdid.example.com 387 + SERVICE_KEY: ${SERVICE_KEY} 388 + HTTP_PORT: 8080 389 + SQLITE_URL: sqlite:/data/quickdid.db 390 + CACHE_TTL_MEMORY: 600 391 + CACHE_TTL_SQLITE: 86400 392 + QUEUE_ADAPTER: mpsc 393 + QUEUE_BUFFER_SIZE: 5000 394 + RUST_LOG: info 395 + ports: 396 + - "8080:8080" 397 + volumes: 398 + - quickdid-data:/data 399 + networks: 400 + - quickdid-network 401 + restart: ${RESTART_POLICY:-unless-stopped} 402 + deploy: 403 + resources: 404 + limits: 405 + memory: ${MEMORY_LIMIT:-256M} 406 + cpus: ${CPU_LIMIT:-0.5} 407 + reservations: 408 + memory: 128M 409 + cpus: '0.25' 410 + healthcheck: 411 + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] 412 + interval: 30s 413 + timeout: 3s 414 + retries: 3 415 + start_period: 10s 416 + logging: 417 + driver: "json-file" 418 + options: 419 + max-size: "10m" 420 + max-file: "3" 421 + 422 + # Optional: Nginx reverse proxy with SSL 423 + nginx: 424 + image: nginx:alpine 425 + container_name: quickdid-nginx 426 + ports: 427 + - "80:80" 428 + - "443:443" 429 + volumes: 430 + - ./nginx.conf:/etc/nginx/nginx.conf:ro 431 + - ./certs:/etc/nginx/certs:ro 432 + - ./acme-challenge:/var/www/acme:ro 433 + depends_on: 434 + - quickdid 435 + networks: 436 + - quickdid-network 437 + restart: unless-stopped 438 + logging: 439 + driver: "json-file" 440 + options: 441 + max-size: "10m" 442 + max-file: "3" 443 + 444 + networks: 445 + quickdid-network: 446 + driver: bridge 447 + 448 + volumes: 449 + quickdid-data: 450 + driver: local 451 + ``` 452 + 355 453 ### Nginx Configuration (nginx.conf) 356 454 357 455 ```nginx ··· 418 516 ### Starting the Stack 419 517 420 518 ```bash 421 - # Start all services 519 + # Start Redis-based stack 422 520 docker-compose up -d 521 + 522 + # Start SQLite-based stack 523 + docker-compose -f docker-compose.sqlite.yml up -d 423 524 424 525 # View logs 425 526 docker-compose logs -f 527 + # or for SQLite setup 528 + docker-compose -f docker-compose.sqlite.yml logs -f 426 529 427 530 # Check service status 428 531 docker-compose ps 429 532 430 533 # Stop all services 431 534 docker-compose down 535 + # or for SQLite setup 536 + docker-compose -f docker-compose.sqlite.yml down 432 537 ``` 433 538 434 539 ## Health Monitoring ··· 539 644 # Check DNS resolution 540 645 docker exec quickdid nslookup plc.directory 541 646 542 - # Verify Redis cache 647 + # Verify Redis cache (if using Redis) 543 648 docker exec -it quickdid-redis redis-cli 544 649 > KEYS handle:* 545 650 > TTL handle:example_key 651 + 652 + # Check SQLite cache (if using SQLite) 653 + docker exec quickdid sqlite3 /data/quickdid.db ".tables" 654 + docker exec quickdid sqlite3 /data/quickdid.db "SELECT COUNT(*) FROM handle_resolution_cache;" 546 655 ``` 547 656 548 657 #### 3. Performance Issues 549 658 550 659 ```bash 551 - # Monitor Redis memory usage 660 + # Monitor Redis memory usage (if using Redis) 552 661 docker exec quickdid-redis redis-cli INFO memory 553 662 663 + # Check SQLite database size (if using SQLite) 664 + docker exec quickdid ls -lh /data/quickdid.db 665 + docker exec quickdid sqlite3 /data/quickdid.db "PRAGMA page_count; PRAGMA page_size;" 666 + 554 667 # Check container resource usage 555 668 docker stats quickdid 556 669 ··· 577 690 # Test handle resolution 578 691 curl "http://localhost:8080/xrpc/com.atproto.identity.resolveHandle?handle=example.bsky.social" 579 692 580 - # Check Redis keys 693 + # Check Redis keys (if using Redis) 581 694 docker exec quickdid-redis redis-cli --scan --pattern "handle:*" | head -20 582 695 696 + # Check SQLite cache entries (if using SQLite) 697 + docker exec quickdid sqlite3 /data/quickdid.db "SELECT COUNT(*) as total_entries, MIN(updated) as oldest, MAX(updated) as newest FROM handle_resolution_cache;" 698 + 583 699 # Monitor real-time logs 584 700 docker-compose logs -f quickdid | grep -E "ERROR|WARN" 585 701 ``` ··· 588 704 589 705 ### Backup and Restore 590 706 707 + #### Redis Backup 591 708 ```bash 592 709 # Backup Redis data 593 710 docker exec quickdid-redis redis-cli BGSAVE ··· 598 715 docker restart quickdid-redis 599 716 ``` 600 717 718 + #### SQLite Backup 719 + ```bash 720 + # Backup SQLite database 721 + docker exec quickdid sqlite3 /data/quickdid.db ".backup /tmp/backup.db" 722 + docker cp quickdid:/tmp/backup.db ./backups/sqlite-$(date +%Y%m%d).db 723 + 724 + # Alternative: Copy database file directly (service must be stopped) 725 + docker-compose -f docker-compose.sqlite.yml stop quickdid 726 + docker cp quickdid:/data/quickdid.db ./backups/sqlite-$(date +%Y%m%d).db 727 + docker-compose -f docker-compose.sqlite.yml start quickdid 728 + 729 + # Restore SQLite database 730 + docker-compose -f docker-compose.sqlite.yml stop quickdid 731 + docker cp ./backups/sqlite-backup.db quickdid:/data/quickdid.db 732 + docker-compose -f docker-compose.sqlite.yml start quickdid 733 + ``` 734 + 601 735 ### Updates and Rollbacks 602 736 603 737 ```bash ··· 628 762 629 763 ## Performance Optimization 630 764 765 + ### Caching Strategy Selection 766 + 767 + **Cache Priority**: QuickDID uses the first available cache in this order: 768 + 1. **Redis** (distributed, best for multi-instance) 769 + 2. **SQLite** (persistent, best for single-instance) 770 + 3. **Memory** (fast, but lost on restart) 771 + 772 + **Recommendations by Deployment Type**: 773 + - **Single instance, persistent**: Use SQLite (`SQLITE_URL=sqlite:./quickdid.db`) 774 + - **Multi-instance, HA**: Use Redis (`REDIS_URL=redis://redis:6379/0`) 775 + - **Testing/development**: Use memory only (no Redis/SQLite URLs) 776 + - **Hybrid**: Configure both Redis and SQLite for redundancy 777 + 631 778 ### Redis Optimization 632 779 633 780 ```redis ··· 660 807 661 808 ### Value Constraints 662 809 663 - 1. **TTL Values** (`CACHE_TTL_MEMORY`, `CACHE_TTL_REDIS`): 810 + 1. **TTL Values** (`CACHE_TTL_MEMORY`, `CACHE_TTL_REDIS`, `CACHE_TTL_SQLITE`): 664 811 - Must be positive integers (> 0) 665 812 - Recommended minimum: 60 seconds 666 813
+33 -3
src/bin/quickdid.rs
··· 10 10 config::{Args, Config}, 11 11 handle_resolver::{ 12 12 create_base_resolver, create_caching_resolver, create_redis_resolver_with_ttl, 13 + create_sqlite_resolver_with_ttl, 13 14 }, 15 + sqlite_schema::create_sqlite_pool, 14 16 handle_resolver_task::{HandleResolverTaskConfig, create_handle_resolver_task_with_config}, 15 17 http::{AppContext, create_router}, 16 18 queue_adapter::{ ··· 39 41 } 40 42 } 41 43 44 + /// Helper function to create a SQLite pool with consistent error handling 45 + async fn try_create_sqlite_pool(sqlite_url: &str, purpose: &str) -> Option<sqlx::SqlitePool> { 46 + match create_sqlite_pool(sqlite_url).await { 47 + Ok(pool) => { 48 + tracing::info!("SQLite pool created for {}", purpose); 49 + Some(pool) 50 + } 51 + Err(e) => { 52 + tracing::warn!("Failed to create SQLite pool for {}: {}", purpose, e); 53 + None 54 + } 55 + } 56 + } 57 + 42 58 #[tokio::main] 43 59 async fn main() -> Result<()> { 44 60 // Initialize tracing ··· 60 76 tracing::info!("Starting QuickDID service on port {}", config.http_port); 61 77 tracing::info!("Service DID: {}", config.service_did); 62 78 tracing::info!( 63 - "Cache TTL - Memory: {}s, Redis: {}s", 79 + "Cache TTL - Memory: {}s, Redis: {}s, SQLite: {}s", 64 80 config.cache_ttl_memory, 65 - config.cache_ttl_redis 81 + config.cache_ttl_redis, 82 + config.cache_ttl_sqlite 66 83 ); 67 84 68 85 // Parse certificate bundles if provided ··· 122 139 .as_ref() 123 140 .and_then(|url| try_create_redis_pool(url, "handle resolver cache")); 124 141 125 - // Create handle resolver with Redis caching if available, otherwise use in-memory caching 142 + // Create SQLite pool if configured 143 + let sqlite_pool = if let Some(url) = config.sqlite_url.as_ref() { 144 + try_create_sqlite_pool(url, "handle resolver cache").await 145 + } else { 146 + None 147 + }; 148 + 149 + // Create handle resolver with cache priority: Redis > SQLite > In-memory 126 150 let handle_resolver: Arc<dyn quickdid::handle_resolver::HandleResolver> = 127 151 if let Some(pool) = redis_pool { 128 152 tracing::info!( ··· 130 154 config.cache_ttl_redis 131 155 ); 132 156 create_redis_resolver_with_ttl(base_handle_resolver, pool, config.cache_ttl_redis) 157 + } else if let Some(pool) = sqlite_pool { 158 + tracing::info!( 159 + "Using SQLite-backed handle resolver with {}-second cache TTL", 160 + config.cache_ttl_sqlite 161 + ); 162 + create_sqlite_resolver_with_ttl(base_handle_resolver, pool, config.cache_ttl_sqlite) 133 163 } else { 134 164 tracing::info!( 135 165 "Using in-memory handle resolver with {}-second cache TTL",
+42
src/config.rs
··· 90 90 91 91 CACHING: 92 92 REDIS_URL Redis URL for handle resolution caching (optional) 93 + SQLITE_URL SQLite database URL for handle resolution caching (optional) 93 94 CACHE_TTL_MEMORY TTL for in-memory cache in seconds (default: 600) 94 95 CACHE_TTL_REDIS TTL for Redis cache in seconds (default: 7776000 = 90 days) 96 + CACHE_TTL_SQLITE TTL for SQLite cache in seconds (default: 7776000 = 90 days) 95 97 96 98 QUEUE CONFIGURATION: 97 99 QUEUE_ADAPTER Queue adapter: 'mpsc', 'redis', 'noop', 'none' (default: mpsc) ··· 174 176 #[arg(long, env = "REDIS_URL")] 175 177 pub redis_url: Option<String>, 176 178 179 + /// SQLite database URL for caching 180 + /// 181 + /// Examples: 182 + /// - "sqlite:./quickdid.db" (file-based database) 183 + /// - "sqlite::memory:" (in-memory database for testing) 184 + /// - "sqlite:/path/to/cache.db" (absolute path) 185 + /// 186 + /// Benefits: Persistent cache, single-file storage, no external dependencies 187 + #[arg(long, env = "SQLITE_URL")] 188 + pub sqlite_url: Option<String>, 189 + 177 190 /// Queue adapter type for background processing 178 191 /// 179 192 /// Values: ··· 245 258 #[arg(long, env = "CACHE_TTL_REDIS", default_value = "7776000")] 246 259 pub cache_ttl_redis: u64, 247 260 261 + /// TTL for SQLite cache in seconds 262 + /// 263 + /// Range: 3600-31536000 (1 hour to 1 year) 264 + /// Default: 7776000 (90 days) 265 + /// 266 + /// Recommendation: 86400 (1 day) for frequently changing data 267 + #[arg(long, env = "CACHE_TTL_SQLITE", default_value = "7776000")] 268 + pub cache_ttl_sqlite: u64, 269 + 248 270 /// Redis blocking timeout for queue operations in seconds 249 271 /// 250 272 /// Range: 1-60 (recommended) ··· 307 329 /// Redis URL for caching (e.g., "redis://localhost:6379/0") 308 330 pub redis_url: Option<String>, 309 331 332 + /// SQLite database URL for caching (e.g., "sqlite:./quickdid.db") 333 + pub sqlite_url: Option<String>, 334 + 310 335 /// Queue adapter type: "mpsc", "redis", or "noop" 311 336 pub queue_adapter: String, 312 337 ··· 327 352 328 353 /// TTL for Redis cache in seconds (e.g., 7776000 = 90 days) 329 354 pub cache_ttl_redis: u64, 355 + 356 + /// TTL for SQLite cache in seconds (e.g., 7776000 = 90 days) 357 + pub cache_ttl_sqlite: u64, 330 358 331 359 /// Redis blocking timeout for queue operations in seconds (e.g., 5) 332 360 pub queue_redis_timeout: u64, ··· 449 477 Some(env_val) 450 478 } 451 479 }), 480 + sqlite_url: args.sqlite_url.or_else(|| { 481 + let env_val = optional_env("SQLITE_URL"); 482 + if env_val.is_empty() { 483 + None 484 + } else { 485 + Some(env_val) 486 + } 487 + }), 452 488 queue_adapter: args.queue_adapter, 453 489 queue_redis_url: args.queue_redis_url.or_else(|| { 454 490 let env_val = optional_env("QUEUE_REDIS_URL"); ··· 472 508 queue_buffer_size: args.queue_buffer_size, 473 509 cache_ttl_memory: args.cache_ttl_memory, 474 510 cache_ttl_redis: args.cache_ttl_redis, 511 + cache_ttl_sqlite: args.cache_ttl_sqlite, 475 512 queue_redis_timeout: args.queue_redis_timeout, 476 513 }) 477 514 } ··· 510 547 if self.cache_ttl_redis == 0 { 511 548 return Err(ConfigError::InvalidTtl( 512 549 "CACHE_TTL_REDIS must be > 0".to_string(), 550 + )); 551 + } 552 + if self.cache_ttl_sqlite == 0 { 553 + return Err(ConfigError::InvalidTtl( 554 + "CACHE_TTL_SQLITE must be > 0".to_string(), 513 555 )); 514 556 } 515 557 if self.queue_redis_timeout == 0 {
+3
src/handle_resolver/mod.rs
··· 11 11 //! - [`BaseHandleResolver`]: Core resolver that performs actual DNS/HTTP lookups 12 12 //! - [`CachingHandleResolver`]: In-memory caching wrapper with configurable TTL 13 13 //! - [`RedisHandleResolver`]: Redis-backed persistent caching with binary serialization 14 + //! - [`SqliteHandleResolver`]: SQLite-backed persistent caching for single-instance deployments 14 15 //! 15 16 //! # Example Usage 16 17 //! ··· 43 44 mod errors; 44 45 mod memory; 45 46 mod redis; 47 + mod sqlite; 46 48 mod traits; 47 49 48 50 // Re-export public API ··· 53 55 pub use base::create_base_resolver; 54 56 pub use memory::create_caching_resolver; 55 57 pub use redis::{create_redis_resolver, create_redis_resolver_with_ttl}; 58 + pub use sqlite::{create_sqlite_resolver, create_sqlite_resolver_with_ttl};
+468
src/handle_resolver/sqlite.rs
··· 1 + //! SQLite-backed caching handle resolver. 2 + //! 3 + //! This module provides a handle resolver that caches resolution results in SQLite 4 + //! with configurable expiration times. SQLite caching provides persistence across 5 + //! service restarts while remaining lightweight for single-instance deployments. 6 + 7 + use super::errors::HandleResolverError; 8 + use super::traits::HandleResolver; 9 + use crate::handle_resolution_result::HandleResolutionResult; 10 + use async_trait::async_trait; 11 + use metrohash::MetroHash64; 12 + use sqlx::{Row, SqlitePool}; 13 + use std::hash::Hasher as _; 14 + use std::sync::Arc; 15 + use std::time::{SystemTime, UNIX_EPOCH}; 16 + 17 + /// SQLite-backed caching handle resolver. 18 + /// 19 + /// This resolver caches handle resolution results in SQLite with a configurable TTL. 20 + /// Results are stored in a compact binary format using bincode serialization 21 + /// to minimize storage overhead. 22 + /// 23 + /// # Features 24 + /// 25 + /// - Persistent caching across service restarts 26 + /// - Lightweight single-file database 27 + /// - Configurable TTL (default: 90 days) 28 + /// - Compact binary storage format 29 + /// - Automatic schema management 30 + /// - Graceful fallback if SQLite is unavailable 31 + /// 32 + /// # Example 33 + /// 34 + /// ```no_run 35 + /// use std::sync::Arc; 36 + /// use sqlx::SqlitePool; 37 + /// use quickdid::handle_resolver::{create_base_resolver, create_sqlite_resolver, HandleResolver}; 38 + /// 39 + /// # async fn example() { 40 + /// # use atproto_identity::resolve::HickoryDnsResolver; 41 + /// # use reqwest::Client; 42 + /// # let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[])); 43 + /// # let http_client = Client::new(); 44 + /// # let base_resolver = create_base_resolver(dns_resolver, http_client); 45 + /// # let sqlite_pool: SqlitePool = todo!(); 46 + /// // Create with default 90-day TTL 47 + /// let resolver = create_sqlite_resolver( 48 + /// base_resolver, 49 + /// sqlite_pool 50 + /// ); 51 + /// # } 52 + /// ``` 53 + pub(super) struct SqliteHandleResolver { 54 + /// Base handle resolver to perform actual resolution 55 + inner: Arc<dyn HandleResolver>, 56 + /// SQLite connection pool 57 + pool: SqlitePool, 58 + /// TTL for cache entries in seconds 59 + ttl_seconds: u64, 60 + } 61 + 62 + impl SqliteHandleResolver { 63 + /// Create a new SQLite-backed handle resolver with default 90-day TTL. 64 + fn new(inner: Arc<dyn HandleResolver>, pool: SqlitePool) -> Self { 65 + Self::with_ttl(inner, pool, 90 * 24 * 60 * 60) // 90 days default 66 + } 67 + 68 + /// Create a new SQLite-backed handle resolver with custom TTL. 69 + fn with_ttl(inner: Arc<dyn HandleResolver>, pool: SqlitePool, ttl_seconds: u64) -> Self { 70 + Self { 71 + inner, 72 + pool, 73 + ttl_seconds, 74 + } 75 + } 76 + 77 + /// Generate the cache key for a handle. 78 + /// 79 + /// Uses MetroHash64 to generate a consistent hash of the handle 80 + /// for use as the primary key. This provides better key distribution 81 + /// and avoids issues with special characters in handles. 82 + fn make_key(&self, handle: &str) -> u64 { 83 + let mut h = MetroHash64::default(); 84 + h.write(handle.as_bytes()); 85 + h.finish() 86 + } 87 + 88 + /// Check if a cache entry is expired. 89 + fn is_expired(&self, updated_timestamp: i64) -> bool { 90 + let current_timestamp = SystemTime::now() 91 + .duration_since(UNIX_EPOCH) 92 + .unwrap_or_default() 93 + .as_secs() as i64; 94 + 95 + (current_timestamp - updated_timestamp) > (self.ttl_seconds as i64) 96 + } 97 + } 98 + 99 + #[async_trait] 100 + impl HandleResolver for SqliteHandleResolver { 101 + async fn resolve(&self, s: &str) -> Result<String, HandleResolverError> { 102 + let handle = s.to_string(); 103 + let key = self.make_key(&handle) as i64; // SQLite uses signed integers 104 + 105 + // Try to get from SQLite cache first 106 + let cached_result = sqlx::query( 107 + "SELECT result, updated FROM handle_resolution_cache WHERE key = ?1" 108 + ) 109 + .bind(key) 110 + .fetch_optional(&self.pool) 111 + .await; 112 + 113 + match cached_result { 114 + Ok(Some(row)) => { 115 + let cached_bytes: Vec<u8> = row.get("result"); 116 + let updated_timestamp: i64 = row.get("updated"); 117 + 118 + // Check if the entry is expired 119 + if !self.is_expired(updated_timestamp) { 120 + // Deserialize the cached result 121 + match HandleResolutionResult::from_bytes(&cached_bytes) { 122 + Ok(cached_result) => { 123 + if let Some(did) = cached_result.to_did() { 124 + tracing::debug!("Cache hit for handle {}: {}", handle, did); 125 + return Ok(did); 126 + } else { 127 + tracing::debug!("Cache hit (not resolved) for handle {}", handle); 128 + return Err(HandleResolverError::HandleNotFound); 129 + } 130 + } 131 + Err(e) => { 132 + tracing::warn!( 133 + "Failed to deserialize cached result for handle {}: {}", 134 + handle, 135 + e 136 + ); 137 + // Fall through to re-resolve if deserialization fails 138 + } 139 + } 140 + } else { 141 + tracing::debug!("Cache entry expired for handle {}", handle); 142 + // Entry is expired, we'll re-resolve and update it 143 + } 144 + } 145 + Ok(None) => { 146 + tracing::debug!("Cache miss for handle {}, resolving...", handle); 147 + } 148 + Err(e) => { 149 + tracing::warn!("Failed to query SQLite cache for handle {}: {}", handle, e); 150 + // Fall through to resolve without caching on database error 151 + } 152 + } 153 + 154 + // Not in cache or expired, resolve through inner resolver 155 + let result = self.inner.resolve(s).await; 156 + 157 + // Create and serialize resolution result 158 + let resolution_result = match &result { 159 + Ok(did) => { 160 + tracing::debug!( 161 + "Caching successful resolution for handle {}: {}", 162 + handle, 163 + did 164 + ); 165 + match HandleResolutionResult::success(did) { 166 + Ok(res) => res, 167 + Err(e) => { 168 + tracing::warn!("Failed to create resolution result: {}", e); 169 + return result; 170 + } 171 + } 172 + } 173 + Err(e) => { 174 + tracing::debug!("Caching failed resolution for handle {}: {}", handle, e); 175 + match HandleResolutionResult::not_resolved() { 176 + Ok(res) => res, 177 + Err(err) => { 178 + tracing::warn!("Failed to create not_resolved result: {}", err); 179 + return result; 180 + } 181 + } 182 + } 183 + }; 184 + 185 + // Serialize to bytes 186 + match resolution_result.to_bytes() { 187 + Ok(bytes) => { 188 + let current_timestamp = SystemTime::now() 189 + .duration_since(UNIX_EPOCH) 190 + .unwrap_or_default() 191 + .as_secs() as i64; 192 + 193 + // Insert or update the cache entry 194 + let query_result = sqlx::query( 195 + r#" 196 + INSERT INTO handle_resolution_cache (key, result, created, updated) 197 + VALUES (?1, ?2, ?3, ?4) 198 + ON CONFLICT(key) DO UPDATE SET 199 + result = excluded.result, 200 + updated = excluded.updated 201 + "# 202 + ) 203 + .bind(key) 204 + .bind(&bytes) 205 + .bind(current_timestamp) 206 + .bind(current_timestamp) 207 + .execute(&self.pool) 208 + .await; 209 + 210 + if let Err(e) = query_result { 211 + tracing::warn!("Failed to cache handle resolution in SQLite: {}", e); 212 + } 213 + } 214 + Err(e) => { 215 + tracing::warn!( 216 + "Failed to serialize resolution result for handle {}: {}", 217 + handle, 218 + e 219 + ); 220 + } 221 + } 222 + 223 + result 224 + } 225 + } 226 + 227 + /// Create a new SQLite-backed handle resolver with default 90-day TTL. 228 + /// 229 + /// # Arguments 230 + /// 231 + /// * `inner` - The underlying resolver to use for actual resolution 232 + /// * `pool` - SQLite connection pool 233 + /// 234 + /// # Example 235 + /// 236 + /// ```no_run 237 + /// use std::sync::Arc; 238 + /// use quickdid::handle_resolver::{create_base_resolver, create_sqlite_resolver, HandleResolver}; 239 + /// use quickdid::sqlite_schema::create_sqlite_pool; 240 + /// 241 + /// # async fn example() -> anyhow::Result<()> { 242 + /// # use atproto_identity::resolve::HickoryDnsResolver; 243 + /// # use reqwest::Client; 244 + /// # let dns_resolver = Arc::new(HickoryDnsResolver::create_resolver(&[])); 245 + /// # let http_client = Client::new(); 246 + /// let base = create_base_resolver( 247 + /// dns_resolver, 248 + /// http_client, 249 + /// ); 250 + /// 251 + /// let pool = create_sqlite_pool("sqlite:./quickdid.db").await?; 252 + /// let resolver = create_sqlite_resolver(base, pool); 253 + /// let did = resolver.resolve("alice.bsky.social").await.unwrap(); 254 + /// # Ok(()) 255 + /// # } 256 + /// ``` 257 + pub fn create_sqlite_resolver( 258 + inner: Arc<dyn HandleResolver>, 259 + pool: SqlitePool, 260 + ) -> Arc<dyn HandleResolver> { 261 + Arc::new(SqliteHandleResolver::new(inner, pool)) 262 + } 263 + 264 + /// Create a new SQLite-backed handle resolver with custom TTL. 265 + /// 266 + /// # Arguments 267 + /// 268 + /// * `inner` - The underlying resolver to use for actual resolution 269 + /// * `pool` - SQLite connection pool 270 + /// * `ttl_seconds` - TTL for cache entries in seconds 271 + pub fn create_sqlite_resolver_with_ttl( 272 + inner: Arc<dyn HandleResolver>, 273 + pool: SqlitePool, 274 + ttl_seconds: u64, 275 + ) -> Arc<dyn HandleResolver> { 276 + Arc::new(SqliteHandleResolver::with_ttl(inner, pool, ttl_seconds)) 277 + } 278 + 279 + #[cfg(test)] 280 + mod tests { 281 + use super::*; 282 + 283 + // Mock handle resolver for testing 284 + #[derive(Clone)] 285 + struct MockHandleResolver { 286 + should_fail: bool, 287 + expected_did: String, 288 + } 289 + 290 + #[async_trait] 291 + impl HandleResolver for MockHandleResolver { 292 + async fn resolve(&self, _handle: &str) -> Result<String, HandleResolverError> { 293 + if self.should_fail { 294 + Err(HandleResolverError::MockResolutionFailure) 295 + } else { 296 + Ok(self.expected_did.clone()) 297 + } 298 + } 299 + } 300 + 301 + #[tokio::test] 302 + async fn test_sqlite_handle_resolver_cache_hit() { 303 + // Create in-memory SQLite database for testing 304 + let pool = SqlitePool::connect("sqlite::memory:") 305 + .await 306 + .expect("Failed to connect to in-memory SQLite"); 307 + 308 + // Create the schema 309 + crate::sqlite_schema::create_schema(&pool) 310 + .await 311 + .expect("Failed to create schema"); 312 + 313 + // Create mock resolver 314 + let mock_resolver = Arc::new(MockHandleResolver { 315 + should_fail: false, 316 + expected_did: "did:plc:testuser123".to_string(), 317 + }); 318 + 319 + // Create SQLite-backed resolver 320 + let sqlite_resolver = SqliteHandleResolver::with_ttl(mock_resolver, pool.clone(), 3600); 321 + 322 + let test_handle = "alice.bsky.social"; 323 + let expected_key = sqlite_resolver.make_key(test_handle) as i64; 324 + 325 + // Verify database is empty initially 326 + let initial_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM handle_resolution_cache") 327 + .fetch_one(&pool) 328 + .await 329 + .expect("Failed to query initial count"); 330 + assert_eq!(initial_count, 0); 331 + 332 + // First resolution - should call inner resolver and cache the result 333 + let result1 = sqlite_resolver.resolve(test_handle).await.unwrap(); 334 + assert_eq!(result1, "did:plc:testuser123"); 335 + 336 + // Verify record was inserted 337 + let count_after_first: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM handle_resolution_cache") 338 + .fetch_one(&pool) 339 + .await 340 + .expect("Failed to query count after first resolution"); 341 + assert_eq!(count_after_first, 1); 342 + 343 + // Verify the cached record has correct key and non-empty result 344 + let cached_record = sqlx::query("SELECT key, result, created, updated FROM handle_resolution_cache WHERE key = ?1") 345 + .bind(expected_key) 346 + .fetch_one(&pool) 347 + .await 348 + .expect("Failed to fetch cached record"); 349 + 350 + let cached_key: i64 = cached_record.get("key"); 351 + let cached_result: Vec<u8> = cached_record.get("result"); 352 + let cached_created: i64 = cached_record.get("created"); 353 + let cached_updated: i64 = cached_record.get("updated"); 354 + 355 + assert_eq!(cached_key, expected_key); 356 + assert!(!cached_result.is_empty(), "Cached result should not be empty"); 357 + assert!(cached_created > 0, "Created timestamp should be positive"); 358 + assert!(cached_updated > 0, "Updated timestamp should be positive"); 359 + assert_eq!(cached_created, cached_updated, "Created and updated should be equal on first insert"); 360 + 361 + // Verify we can deserialize the cached result 362 + let resolution_result = crate::handle_resolution_result::HandleResolutionResult::from_bytes(&cached_result) 363 + .expect("Failed to deserialize cached result"); 364 + let cached_did = resolution_result.to_did().expect("Should have a DID"); 365 + assert_eq!(cached_did, "did:plc:testuser123"); 366 + 367 + // Second resolution - should hit cache (no additional database insert) 368 + let result2 = sqlite_resolver.resolve(test_handle).await.unwrap(); 369 + assert_eq!(result2, "did:plc:testuser123"); 370 + 371 + // Verify count hasn't changed (cache hit, no new insert) 372 + let count_after_second: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM handle_resolution_cache") 373 + .fetch_one(&pool) 374 + .await 375 + .expect("Failed to query count after second resolution"); 376 + assert_eq!(count_after_second, 1); 377 + } 378 + 379 + #[tokio::test] 380 + async fn test_sqlite_handle_resolver_cache_error() { 381 + // Create in-memory SQLite database for testing 382 + let pool = SqlitePool::connect("sqlite::memory:") 383 + .await 384 + .expect("Failed to connect to in-memory SQLite"); 385 + 386 + // Create the schema 387 + crate::sqlite_schema::create_schema(&pool) 388 + .await 389 + .expect("Failed to create schema"); 390 + 391 + // Create mock resolver that fails 392 + let mock_resolver = Arc::new(MockHandleResolver { 393 + should_fail: true, 394 + expected_did: String::new(), 395 + }); 396 + 397 + // Create SQLite-backed resolver 398 + let sqlite_resolver = SqliteHandleResolver::with_ttl(mock_resolver, pool.clone(), 3600); 399 + 400 + let test_handle = "error.bsky.social"; 401 + let expected_key = sqlite_resolver.make_key(test_handle) as i64; 402 + 403 + // Verify database is empty initially 404 + let initial_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM handle_resolution_cache") 405 + .fetch_one(&pool) 406 + .await 407 + .expect("Failed to query initial count"); 408 + assert_eq!(initial_count, 0); 409 + 410 + // First resolution - should fail and cache the failure 411 + let result1 = sqlite_resolver.resolve(test_handle).await; 412 + assert!(result1.is_err()); 413 + 414 + // Match the specific error type we expect 415 + match result1 { 416 + Err(HandleResolverError::MockResolutionFailure) => {}, 417 + other => panic!("Expected MockResolutionFailure, got {:?}", other), 418 + } 419 + 420 + // Verify the failure was cached 421 + let count_after_first: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM handle_resolution_cache") 422 + .fetch_one(&pool) 423 + .await 424 + .expect("Failed to query count after first resolution"); 425 + assert_eq!(count_after_first, 1); 426 + 427 + // Verify the cached error record 428 + let cached_record = sqlx::query("SELECT key, result, created, updated FROM handle_resolution_cache WHERE key = ?1") 429 + .bind(expected_key) 430 + .fetch_one(&pool) 431 + .await 432 + .expect("Failed to fetch cached error record"); 433 + 434 + let cached_key: i64 = cached_record.get("key"); 435 + let cached_result: Vec<u8> = cached_record.get("result"); 436 + let cached_created: i64 = cached_record.get("created"); 437 + let cached_updated: i64 = cached_record.get("updated"); 438 + 439 + assert_eq!(cached_key, expected_key); 440 + assert!(!cached_result.is_empty(), "Cached error result should not be empty"); 441 + assert!(cached_created > 0, "Created timestamp should be positive"); 442 + assert!(cached_updated > 0, "Updated timestamp should be positive"); 443 + assert_eq!(cached_created, cached_updated, "Created and updated should be equal on first insert"); 444 + 445 + // Verify we can deserialize the cached error result 446 + let resolution_result = crate::handle_resolution_result::HandleResolutionResult::from_bytes(&cached_result) 447 + .expect("Failed to deserialize cached error result"); 448 + let cached_did = resolution_result.to_did(); 449 + assert!(cached_did.is_none(), "Error result should have no DID"); 450 + 451 + // Second resolution - should hit cache with error (no additional database operations) 452 + let result2 = sqlite_resolver.resolve(test_handle).await; 453 + assert!(result2.is_err()); 454 + 455 + // Match the specific error type we expect from cache 456 + match result2 { 457 + Err(HandleResolverError::HandleNotFound) => {}, // Cache returns HandleNotFound for "not resolved" 458 + other => panic!("Expected HandleNotFound from cache, got {:?}", other), 459 + } 460 + 461 + // Verify count hasn't changed (cache hit, no new operations) 462 + let count_after_second: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM handle_resolution_cache") 463 + .fetch_one(&pool) 464 + .await 465 + .expect("Failed to query count after second resolution"); 466 + assert_eq!(count_after_second, 1); 467 + } 468 + }
+1
src/lib.rs
··· 7 7 pub mod cache; // Only create_redis_pool exposed 8 8 pub mod handle_resolver_task; // Factory functions and TaskConfig exposed 9 9 pub mod queue_adapter; // Trait and factory functions exposed 10 + pub mod sqlite_schema; // SQLite schema management functions exposed 10 11 pub mod task_manager; // Only spawn_cancellable_task exposed 11 12 12 13 // Internal modules - crate visibility only
+285
src/sqlite_schema.rs
··· 1 + //! SQLite schema management for QuickDID. 2 + //! 3 + //! This module provides functionality to create and manage the SQLite database 4 + //! schema used by the SQLite-backed handle resolver cache. 5 + 6 + use anyhow::Result; 7 + use sqlx::{SqlitePool, migrate::MigrateDatabase, Sqlite}; 8 + use std::path::Path; 9 + 10 + /// SQL schema for the handle resolution cache table. 11 + const CREATE_HANDLE_RESOLUTION_CACHE_TABLE: &str = r#" 12 + CREATE TABLE IF NOT EXISTS handle_resolution_cache ( 13 + key INTEGER PRIMARY KEY, 14 + result BLOB NOT NULL, 15 + created INTEGER NOT NULL, 16 + updated INTEGER NOT NULL 17 + ); 18 + 19 + CREATE INDEX IF NOT EXISTS idx_handle_resolution_cache_updated 20 + ON handle_resolution_cache(updated); 21 + "#; 22 + 23 + /// Create or connect to a SQLite database and ensure schema is initialized. 24 + /// 25 + /// # Arguments 26 + /// 27 + /// * `database_url` - SQLite database URL (e.g., "sqlite:./quickdid.db" or "sqlite::memory:") 28 + /// 29 + /// # Returns 30 + /// 31 + /// Returns a SqlitePool connected to the database with schema initialized. 32 + /// 33 + /// # Example 34 + /// 35 + /// ```no_run 36 + /// use quickdid::sqlite_schema::create_sqlite_pool; 37 + /// 38 + /// # async fn example() -> anyhow::Result<()> { 39 + /// // File-based database 40 + /// let pool = create_sqlite_pool("sqlite:./quickdid.db").await?; 41 + /// 42 + /// // In-memory database (for testing) 43 + /// let pool = create_sqlite_pool("sqlite::memory:").await?; 44 + /// # Ok(()) 45 + /// # } 46 + /// ``` 47 + pub async fn create_sqlite_pool(database_url: &str) -> Result<SqlitePool> { 48 + tracing::info!("Initializing SQLite database: {}", database_url); 49 + 50 + // Extract the database path from the URL for file-based databases 51 + if let Some(path) = database_url.strip_prefix("sqlite:") { 52 + if path != ":memory:" && !path.is_empty() { 53 + // Create the database file if it doesn't exist 54 + if !Sqlite::database_exists(database_url).await? { 55 + tracing::info!("Creating SQLite database file: {}", path); 56 + Sqlite::create_database(database_url).await?; 57 + } 58 + 59 + // Ensure the parent directory exists 60 + if let Some(parent) = Path::new(path).parent() { 61 + if !parent.exists() { 62 + tracing::info!("Creating directory: {}", parent.display()); 63 + std::fs::create_dir_all(parent)?; 64 + } 65 + } 66 + } 67 + } 68 + 69 + // Connect to the database 70 + let pool = SqlitePool::connect(database_url).await?; 71 + tracing::info!("Connected to SQLite database"); 72 + 73 + // Create the schema 74 + create_schema(&pool).await?; 75 + 76 + Ok(pool) 77 + } 78 + 79 + /// Create the database schema if it doesn't exist. 80 + /// 81 + /// # Arguments 82 + /// 83 + /// * `pool` - SQLite connection pool 84 + /// 85 + /// # Example 86 + /// 87 + /// ```no_run 88 + /// use quickdid::sqlite_schema::create_schema; 89 + /// use sqlx::SqlitePool; 90 + /// 91 + /// # async fn example() -> anyhow::Result<()> { 92 + /// let pool = SqlitePool::connect("sqlite::memory:").await?; 93 + /// create_schema(&pool).await?; 94 + /// # Ok(()) 95 + /// # } 96 + /// ``` 97 + pub async fn create_schema(pool: &SqlitePool) -> Result<()> { 98 + tracing::debug!("Creating SQLite schema if not exists"); 99 + 100 + // Execute the schema creation SQL 101 + sqlx::query(CREATE_HANDLE_RESOLUTION_CACHE_TABLE) 102 + .execute(pool) 103 + .await?; 104 + 105 + tracing::info!("SQLite schema initialized"); 106 + 107 + Ok(()) 108 + } 109 + 110 + /// Clean up expired entries from the handle resolution cache. 111 + /// 112 + /// This function removes entries that are older than the specified TTL. 113 + /// It should be called periodically to prevent the database from growing indefinitely. 114 + /// 115 + /// # Arguments 116 + /// 117 + /// * `pool` - SQLite connection pool 118 + /// * `ttl_seconds` - TTL in seconds for cache entries 119 + /// 120 + /// # Returns 121 + /// 122 + /// Returns the number of entries deleted. 123 + /// 124 + /// # Example 125 + /// 126 + /// ```no_run 127 + /// use quickdid::sqlite_schema::cleanup_expired_entries; 128 + /// use sqlx::SqlitePool; 129 + /// 130 + /// # async fn example() -> anyhow::Result<()> { 131 + /// let pool = SqlitePool::connect("sqlite:./quickdid.db").await?; 132 + /// let deleted_count = cleanup_expired_entries(&pool, 7776000).await?; // 90 days 133 + /// println!("Deleted {} expired entries", deleted_count); 134 + /// # Ok(()) 135 + /// # } 136 + /// ``` 137 + pub async fn cleanup_expired_entries(pool: &SqlitePool, ttl_seconds: u64) -> Result<u64> { 138 + let current_timestamp = std::time::SystemTime::now() 139 + .duration_since(std::time::UNIX_EPOCH) 140 + .unwrap_or_default() 141 + .as_secs() as i64; 142 + 143 + let cutoff_timestamp = current_timestamp - (ttl_seconds as i64); 144 + 145 + let result = sqlx::query("DELETE FROM handle_resolution_cache WHERE updated < ?1") 146 + .bind(cutoff_timestamp) 147 + .execute(pool) 148 + .await?; 149 + 150 + let deleted_count = result.rows_affected(); 151 + if deleted_count > 0 { 152 + tracing::info!("Cleaned up {} expired cache entries", deleted_count); 153 + } 154 + 155 + Ok(deleted_count) 156 + } 157 + 158 + /// Get statistics about the handle resolution cache. 159 + /// 160 + /// # Arguments 161 + /// 162 + /// * `pool` - SQLite connection pool 163 + /// 164 + /// # Returns 165 + /// 166 + /// Returns a tuple of (total_entries, database_size_bytes). 167 + /// 168 + /// # Example 169 + /// 170 + /// ```no_run 171 + /// use quickdid::sqlite_schema::get_cache_stats; 172 + /// use sqlx::SqlitePool; 173 + /// 174 + /// # async fn example() -> anyhow::Result<()> { 175 + /// let pool = SqlitePool::connect("sqlite:./quickdid.db").await?; 176 + /// let (total_entries, size_bytes) = get_cache_stats(&pool).await?; 177 + /// println!("Cache has {} entries, {} bytes", total_entries, size_bytes); 178 + /// # Ok(()) 179 + /// # } 180 + /// ``` 181 + pub async fn get_cache_stats(pool: &SqlitePool) -> Result<(i64, i64)> { 182 + // Get total entries 183 + let total_entries: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM handle_resolution_cache") 184 + .fetch_one(pool) 185 + .await?; 186 + 187 + // Get database page size and page count to calculate total size 188 + let page_size: i64 = sqlx::query_scalar("PRAGMA page_size") 189 + .fetch_one(pool) 190 + .await?; 191 + 192 + let page_count: i64 = sqlx::query_scalar("PRAGMA page_count") 193 + .fetch_one(pool) 194 + .await?; 195 + 196 + let size_bytes = page_size * page_count; 197 + 198 + Ok((total_entries, size_bytes)) 199 + } 200 + 201 + #[cfg(test)] 202 + mod tests { 203 + use super::*; 204 + 205 + #[tokio::test] 206 + async fn test_create_sqlite_pool_memory() { 207 + let pool = create_sqlite_pool("sqlite::memory:") 208 + .await 209 + .expect("Failed to create in-memory SQLite pool"); 210 + 211 + // Verify the table was created 212 + let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM handle_resolution_cache") 213 + .fetch_one(&pool) 214 + .await 215 + .expect("Failed to query table"); 216 + 217 + assert_eq!(count, 0); 218 + } 219 + 220 + #[tokio::test] 221 + async fn test_cleanup_expired_entries() { 222 + let pool = create_sqlite_pool("sqlite::memory:") 223 + .await 224 + .expect("Failed to create in-memory SQLite pool"); 225 + 226 + // Insert a test entry that's already expired 227 + let old_timestamp = std::time::SystemTime::now() 228 + .duration_since(std::time::UNIX_EPOCH) 229 + .unwrap() 230 + .as_secs() as i64 - 3600; // 1 hour ago 231 + 232 + sqlx::query( 233 + "INSERT INTO handle_resolution_cache (key, result, created, updated) VALUES (1, ?1, ?2, ?2)" 234 + ) 235 + .bind(&b"test_data"[..]) 236 + .bind(old_timestamp) 237 + .execute(&pool) 238 + .await 239 + .expect("Failed to insert test data"); 240 + 241 + // Clean up entries older than 30 minutes (1800 seconds) 242 + let deleted = cleanup_expired_entries(&pool, 1800) 243 + .await 244 + .expect("Failed to cleanup expired entries"); 245 + 246 + assert_eq!(deleted, 1); 247 + 248 + // Verify the entry was deleted 249 + let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM handle_resolution_cache") 250 + .fetch_one(&pool) 251 + .await 252 + .expect("Failed to query table"); 253 + 254 + assert_eq!(count, 0); 255 + } 256 + 257 + #[tokio::test] 258 + async fn test_get_cache_stats() { 259 + let pool = create_sqlite_pool("sqlite::memory:") 260 + .await 261 + .expect("Failed to create in-memory SQLite pool"); 262 + 263 + // Insert a test entry 264 + let current_timestamp = std::time::SystemTime::now() 265 + .duration_since(std::time::UNIX_EPOCH) 266 + .unwrap() 267 + .as_secs() as i64; 268 + 269 + sqlx::query( 270 + "INSERT INTO handle_resolution_cache (key, result, created, updated) VALUES (1, ?1, ?2, ?2)" 271 + ) 272 + .bind(&b"test_data"[..]) 273 + .bind(current_timestamp) 274 + .execute(&pool) 275 + .await 276 + .expect("Failed to insert test data"); 277 + 278 + let (total_entries, size_bytes) = get_cache_stats(&pool) 279 + .await 280 + .expect("Failed to get cache stats"); 281 + 282 + assert_eq!(total_entries, 1); 283 + assert!(size_bytes > 0); 284 + } 285 + }