Compare commits
17 Commits
v0.8.0
...
689bfd1325
| Author | SHA1 | Date | |
|---|---|---|---|
|
689bfd1325
|
|||
|
f0c90edce9
|
|||
| 5e60a272f7 | |||
| 34d609937d | |||
| 094b14c945 | |||
| f6d4f846fc | |||
| 7f3139f4f9 | |||
| 494ec05874 | |||
|
8c0128612f
|
|||
| cbe049b6a2 | |||
|
2d6b14ad77
|
|||
| f777ec9b1e | |||
| 2415088792 | |||
| a35d15edc2 | |||
| 613947ac88 | |||
| 145e067454 | |||
|
82de5b260f
|
23
CHANGELOG.md
23
CHANGELOG.md
@@ -6,6 +6,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.10.0] - 2025-11-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- implement take errors
|
||||||
|
|
||||||
|
## [0.9.0] - 2025-11-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- mad not properly surfaces panics
|
||||||
|
- add publish
|
||||||
|
- add readme
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- *(deps)* update all dependencies (#38)
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- *(deps)* update rust crate tracing-subscriber to v0.3.20 (#37)
|
||||||
|
|
||||||
|
## [0.8.1] - 2025-08-09
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- error logging
|
||||||
|
|
||||||
## [0.8.0] - 2025-08-08
|
## [0.8.0] - 2025-08-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
319
Cargo.lock
generated
319
Cargo.lock
generated
@@ -2,21 +2,6 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "addr2line"
|
|
||||||
version = "0.24.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
|
||||||
dependencies = [
|
|
||||||
"gimli",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "adler2"
|
|
||||||
version = "2.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@@ -28,20 +13,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.98"
|
version = "1.0.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "async-trait"
|
|
||||||
version = "0.1.88"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
@@ -49,21 +23,6 @@ version = "1.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "backtrace"
|
|
||||||
version = "0.3.74"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
|
|
||||||
dependencies = [
|
|
||||||
"addr2line",
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"miniz_oxide",
|
|
||||||
"object",
|
|
||||||
"rustc-demangle",
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
@@ -186,32 +145,15 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.13.3+wasi-0.2.2",
|
"wasi 0.13.3+wasi-0.2.2",
|
||||||
"windows-targets",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gimli"
|
|
||||||
version = "0.31.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "io-uring"
|
|
||||||
version = "0.7.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -220,9 +162,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.169"
|
version = "0.2.177"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
@@ -242,11 +184,11 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"regex-automata 0.1.10",
|
"regex-automata",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -255,15 +197,6 @@ version = "2.7.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "miniz_oxide"
|
|
||||||
version = "0.8.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
|
|
||||||
dependencies = [
|
|
||||||
"adler2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -273,15 +206,14 @@ dependencies = [
|
|||||||
"hermit-abi",
|
"hermit-abi",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notmad"
|
name = "notmad"
|
||||||
version = "0.7.5"
|
version = "0.10.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
|
||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"rand",
|
"rand",
|
||||||
@@ -295,21 +227,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.46.0"
|
version = "0.50.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"overload",
|
"windows-sys 0.61.2",
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "object"
|
|
||||||
version = "0.36.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -318,12 +240,6 @@ version = "1.20.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "overload"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -344,7 +260,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows-targets",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -425,27 +341,6 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex"
|
|
||||||
version = "1.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-automata 0.4.9",
|
|
||||||
"regex-syntax 0.8.5",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-automata"
|
|
||||||
version = "0.1.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
|
||||||
dependencies = [
|
|
||||||
"regex-syntax 0.6.29",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@@ -454,27 +349,15 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax 0.8.5",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-syntax"
|
|
||||||
version = "0.6.29"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc-demangle"
|
|
||||||
version = "0.1.24"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -516,12 +399,12 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.7"
|
version = "0.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
|
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -537,18 +420,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.12"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.12"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -567,29 +450,26 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.46.1"
|
version = "1.49.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
|
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"io-uring",
|
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"slab",
|
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.5.0"
|
version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -598,9 +478,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.15"
|
version = "0.7.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
|
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -611,9 +491,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.41"
|
version = "0.1.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -623,9 +503,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.28"
|
version = "0.1.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -634,9 +514,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.33"
|
version = "0.1.36"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"valuable",
|
"valuable",
|
||||||
@@ -655,14 +535,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-subscriber"
|
name = "tracing-subscriber"
|
||||||
version = "0.3.19"
|
version = "0.3.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"matchers",
|
"matchers",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex",
|
"regex-automata",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
@@ -720,26 +600,10 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "windows-link"
|
||||||
version = "0.3.9"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
dependencies = [
|
|
||||||
"winapi-i686-pc-windows-gnu",
|
|
||||||
"winapi-x86_64-pc-windows-gnu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-i686-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
@@ -747,7 +611,25 @@ version = "0.52.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets",
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.53.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -756,14 +638,31 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows_aarch64_gnullvm",
|
"windows_aarch64_gnullvm 0.52.6",
|
||||||
"windows_aarch64_msvc",
|
"windows_aarch64_msvc 0.52.6",
|
||||||
"windows_i686_gnu",
|
"windows_i686_gnu 0.52.6",
|
||||||
"windows_i686_gnullvm",
|
"windows_i686_gnullvm 0.52.6",
|
||||||
"windows_i686_msvc",
|
"windows_i686_msvc 0.52.6",
|
||||||
"windows_x86_64_gnu",
|
"windows_x86_64_gnu 0.52.6",
|
||||||
"windows_x86_64_gnullvm",
|
"windows_x86_64_gnullvm 0.52.6",
|
||||||
"windows_x86_64_msvc",
|
"windows_x86_64_msvc 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.53.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
"windows_aarch64_gnullvm 0.53.1",
|
||||||
|
"windows_aarch64_msvc 0.53.1",
|
||||||
|
"windows_i686_gnu 0.53.1",
|
||||||
|
"windows_i686_gnullvm 0.53.1",
|
||||||
|
"windows_i686_msvc 0.53.1",
|
||||||
|
"windows_x86_64_gnu 0.53.1",
|
||||||
|
"windows_x86_64_gnullvm 0.53.1",
|
||||||
|
"windows_x86_64_msvc 0.53.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -772,48 +671,96 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnullvm"
|
name = "windows_i686_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen-rt"
|
name = "wit-bindgen-rt"
|
||||||
version = "0.33.0"
|
version = "0.33.0"
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ members = ["crates/*"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.8.0"
|
version = "0.10.0"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
mad = { path = "crates/mad" }
|
|
||||||
|
|
||||||
anyhow = { version = "1.0.71" }
|
anyhow = { version = "1.0.71" }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
218
README.md
218
README.md
@@ -4,165 +4,163 @@
|
|||||||
[](https://docs.rs/notmad)
|
[](https://docs.rs/notmad)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
## Overview
|
A simple lifecycle manager for long-running Rust applications. Run multiple services concurrently with graceful shutdown handling.
|
||||||
|
|
||||||
MAD is a robust lifecycle manager designed for long-running Rust operations. It provides a simple, composable way to manage multiple concurrent services within your application, handling graceful startup and shutdown automatically.
|
|
||||||
|
|
||||||
### Perfect for:
|
|
||||||
- 🌐 Web servers
|
|
||||||
- 📨 Queue consumers and message processors
|
|
||||||
- 🔌 gRPC servers
|
|
||||||
- ⏰ Cron job runners
|
|
||||||
- 🔄 Background workers
|
|
||||||
- 📡 Any long-running async operations
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Component-based architecture** - Build your application from composable, reusable components
|
|
||||||
- **Graceful shutdown** - Automatic handling of shutdown signals with proper cleanup
|
|
||||||
- **Concurrent execution** - Run multiple components in parallel with tokio
|
|
||||||
- **Error handling** - Built-in error propagation and logging
|
|
||||||
- **Cancellation tokens** - Coordinate shutdown across all components
|
|
||||||
- **Minimal boilerplate** - Focus on your business logic, not lifecycle management
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Add MAD to your `Cargo.toml`:
|
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
notmad = "0.7.5"
|
notmad = "0.10.0"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
async-trait = "0.1"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
Here's a simple example of a component that simulates a long-running server:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use mad::{Component, Mad};
|
use notmad::{Component, Mad};
|
||||||
use async_trait::async_trait;
|
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
// Define your component
|
struct MyService;
|
||||||
struct WebServer {
|
|
||||||
port: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for WebServer {
|
|
||||||
fn name(&self) -> Option<String> {
|
|
||||||
Some(format!("WebServer on port {}", self.port))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), mad::MadError> {
|
|
||||||
println!("Starting web server on port {}", self.port);
|
|
||||||
|
|
||||||
// Your server logic here
|
|
||||||
// The cancellation token will be triggered on shutdown
|
|
||||||
tokio::select! {
|
|
||||||
_ = cancellation.cancelled() => {
|
|
||||||
println!("Shutting down web server");
|
|
||||||
}
|
|
||||||
_ = self.serve() => {
|
|
||||||
println!("Server stopped");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
impl Component for MyService {
|
||||||
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
|
println!("Service running...");
|
||||||
|
cancellation.cancelled().await;
|
||||||
|
println!("Service stopped");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebServer {
|
|
||||||
async fn serve(&self) {
|
|
||||||
// Simulate a running server
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
// Build and run your application
|
|
||||||
Mad::builder()
|
Mad::builder()
|
||||||
.add(WebServer { port: 8080 })
|
.add(MyService)
|
||||||
.add(WebServer { port: 8081 }) // You can add multiple instances
|
|
||||||
.run()
|
.run()
|
||||||
.await?;
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Axum Web Server with Graceful Shutdown
|
||||||
|
|
||||||
|
Here's how to run an Axum server with MAD's graceful shutdown:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use axum::{Router, routing::get};
|
||||||
|
use notmad::{Component, ComponentInfo};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
struct WebServer {
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for WebServer {
|
||||||
|
fn info(&self) -> ComponentInfo {
|
||||||
|
format!("WebServer:{}", self.port).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, cancel: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
|
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", self.port))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("Listening on http://0.0.0.0:{}", self.port);
|
||||||
|
|
||||||
|
// Run server with graceful shutdown
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.with_graceful_shutdown(async move {
|
||||||
|
cancel.cancelled().await;
|
||||||
|
println!("Shutting down server...");
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Advanced Usage
|
### Run Multiple Services
|
||||||
|
|
||||||
### Custom Components
|
|
||||||
|
|
||||||
Components can be anything that implements the `Component` trait:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use mad::{Component, Mad};
|
Mad::builder()
|
||||||
use async_trait::async_trait;
|
.add(WebServer { port: 8080 })
|
||||||
|
.add(WebServer { port: 8081 })
|
||||||
|
.run()
|
||||||
|
.await?;
|
||||||
|
```
|
||||||
|
|
||||||
struct QueueProcessor {
|
### Use Functions as Components
|
||||||
queue_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
```rust
|
||||||
impl Component for QueueProcessor {
|
Mad::builder()
|
||||||
fn name(&self) -> Option<String> {
|
.add_fn(|cancel| async move {
|
||||||
Some(format!("QueueProcessor-{}", self.queue_name))
|
println!("Running...");
|
||||||
|
cancel.cancelled().await;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle Hooks
|
||||||
|
|
||||||
|
Components support optional setup and cleanup phases:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl Component for DatabaseService {
|
||||||
|
async fn setup(&self) -> Result<(), notmad::MadError> {
|
||||||
|
println!("Connecting to database...");
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), mad::MadError> {
|
async fn run(&self, cancel: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
while !cancellation.is_cancelled() {
|
cancel.cancelled().await;
|
||||||
// Process messages from queue
|
Ok(())
|
||||||
self.process_next_message().await?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn close(&self) -> Result<(), notmad::MadError> {
|
||||||
|
println!("Closing database connection...");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Error Handling
|
## Migration from v0.9
|
||||||
|
|
||||||
MAD provides comprehensive error handling through the `MadError` type with automatic conversion from `anyhow::Error`:
|
### Breaking Changes
|
||||||
|
|
||||||
```rust
|
1. **`name()` → `info()`**: Returns `ComponentInfo` instead of `Option<String>`
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), mad::MadError> {
|
```rust
|
||||||
// Errors automatically convert from anyhow::Error to MadError
|
// Before
|
||||||
database_operation().await?;
|
fn name(&self) -> Option<String> { Some("my-service".into()) }
|
||||||
|
|
||||||
// Or return explicit errors
|
// After
|
||||||
if some_condition {
|
fn info(&self) -> ComponentInfo { "my-service".into() }
|
||||||
return Err(anyhow::anyhow!("Something went wrong").into());
|
```
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
2. **No more `async-trait`**: Remove the dependency and `#[async_trait]` attribute
|
||||||
}
|
```rust
|
||||||
```
|
// Before
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for MyService { }
|
||||||
|
|
||||||
|
// After
|
||||||
|
impl Component for MyService { }
|
||||||
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Check out the [examples directory](crates/mad/examples) for more detailed examples:
|
See [examples directory](crates/mad/examples) for complete working examples.
|
||||||
|
|
||||||
- **basic** - Simple component lifecycle
|
|
||||||
- **fn** - Using functions as components
|
|
||||||
- **signals** - Handling system signals
|
|
||||||
- **error_log** - Error handling and logging
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
MIT - see [LICENSE](LICENSE)
|
||||||
|
|
||||||
## Author
|
## Links
|
||||||
|
|
||||||
Created and maintained by [kjuulh](https://github.com/kjuulh)
|
- [Documentation](https://docs.rs/notmad)
|
||||||
|
- [Repository](https://github.com/kjuulh/mad)
|
||||||
## Repository
|
- [Crates.io](https://crates.io/crates/notmad)
|
||||||
|
|
||||||
Find the source code at [https://github.com/kjuulh/mad](https://github.com/kjuulh/mad)
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ license = "MIT"
|
|||||||
repository = "https://github.com/kjuulh/mad"
|
repository = "https://github.com/kjuulh/mad"
|
||||||
authors = ["kjuulh"]
|
authors = ["kjuulh"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
readme = "../../README.md"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-trait = "0.1.81"
|
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
futures-util = "0.3.30"
|
futures-util = "0.3.30"
|
||||||
rand = "0.9.0"
|
rand = "0.9.0"
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use async_trait::async_trait;
|
use notmad::ComponentInfo;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
|
||||||
struct WaitServer {}
|
struct WaitServer {}
|
||||||
#[async_trait]
|
|
||||||
impl notmad::Component for WaitServer {
|
impl notmad::Component for WaitServer {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some("WaitServer".into())
|
"WaitServer".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
let millis_wait = rand::thread_rng().gen_range(500..3000);
|
let millis_wait = rand::thread_rng().gen_range(500..3000);
|
||||||
|
|
||||||
tracing::debug!("waiting: {}ms", millis_wait);
|
tracing::debug!("waiting: {}ms", millis_wait);
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
//! - Graceful shutdown with cancellation tokens
|
//! - Graceful shutdown with cancellation tokens
|
||||||
//! - Concurrent component execution
|
//! - Concurrent component execution
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use notmad::{Component, ComponentInfo, Mad, MadError};
|
||||||
use notmad::{Component, Mad, MadError};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use tokio::time::{Duration, interval};
|
use tokio::time::{Duration, interval};
|
||||||
@@ -21,10 +20,9 @@ struct WebServer {
|
|||||||
request_count: Arc<AtomicUsize>,
|
request_count: Arc<AtomicUsize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for WebServer {
|
impl Component for WebServer {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some(format!("web-server-{}", self.port))
|
format!("web-server-{}", self.port).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup(&self) -> Result<(), MadError> {
|
async fn setup(&self) -> Result<(), MadError> {
|
||||||
@@ -81,10 +79,9 @@ struct JobProcessor {
|
|||||||
processing_interval: Duration,
|
processing_interval: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for JobProcessor {
|
impl Component for JobProcessor {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some(format!("job-processor-{}", self.queue_name))
|
format!("job-processor-{}", self.queue_name).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup(&self) -> Result<(), MadError> {
|
async fn setup(&self) -> Result<(), MadError> {
|
||||||
@@ -139,10 +136,9 @@ struct HealthChecker {
|
|||||||
check_interval: Duration,
|
check_interval: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for HealthChecker {
|
impl Component for HealthChecker {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some("health-checker".to_string())
|
"health-checker".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
||||||
@@ -181,10 +177,9 @@ struct FailingComponent {
|
|||||||
fail_after: Duration,
|
fail_after: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for FailingComponent {
|
impl Component for FailingComponent {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some("failing-component".to_string())
|
"failing-component".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
||||||
@@ -209,10 +204,9 @@ impl Component for FailingComponent {
|
|||||||
/// Debug component that logs system status periodically.
|
/// Debug component that logs system status periodically.
|
||||||
struct DebugComponent;
|
struct DebugComponent;
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for DebugComponent {
|
impl Component for DebugComponent {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some("debug-component".to_string())
|
"debug-component".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancel: CancellationToken) -> Result<(), MadError> {
|
async fn run(&self, cancel: CancellationToken) -> Result<(), MadError> {
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use async_trait::async_trait;
|
use notmad::ComponentInfo;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
|
||||||
struct ErrorServer {}
|
struct ErrorServer {}
|
||||||
#[async_trait]
|
|
||||||
impl notmad::Component for ErrorServer {
|
impl notmad::Component for ErrorServer {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some("ErrorServer".into())
|
"ErrorServer".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
let millis_wait = rand::thread_rng().gen_range(500..3000);
|
let millis_wait = rand::thread_rng().gen_range(500..3000);
|
||||||
|
|
||||||
tracing::debug!("waiting: {}ms", millis_wait);
|
tracing::debug!("waiting: {}ms", millis_wait);
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
use async_trait::async_trait;
|
use notmad::ComponentInfo;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
|
||||||
struct WaitServer {}
|
struct WaitServer {}
|
||||||
#[async_trait]
|
|
||||||
impl notmad::Component for WaitServer {
|
impl notmad::Component for WaitServer {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some("WaitServer".into())
|
"WaitServer".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
//! This example shows how to run a web server, queue processor, and
|
//! This example shows how to run a web server, queue processor, and
|
||||||
//! scheduled task together, with graceful shutdown on Ctrl+C.
|
//! scheduled task together, with graceful shutdown on Ctrl+C.
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use notmad::{Component, ComponentInfo, Mad, MadError};
|
||||||
use notmad::{Component, Mad, MadError};
|
|
||||||
use tokio::time::{Duration, interval};
|
use tokio::time::{Duration, interval};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
@@ -16,22 +15,21 @@ struct WebServer {
|
|||||||
port: u16,
|
port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for WebServer {
|
impl Component for WebServer {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some(format!("WebServer:{}", self.port))
|
format!("WebServer:{}", self.port).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup(&self) -> Result<(), MadError> {
|
async fn setup(&self) -> Result<(), MadError> {
|
||||||
println!("[{}] Binding to port...", self.name().unwrap());
|
println!("[{}] Binding to port...", self.info());
|
||||||
// Simulate server setup time
|
// Simulate server setup time
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
println!("[{}] Ready to accept connections", self.name().unwrap());
|
println!("[{}] Ready to accept connections", self.info());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
||||||
println!("[{}] Server started", self.name().unwrap());
|
println!("[{}] Server started", self.info());
|
||||||
|
|
||||||
// Simulate handling requests until shutdown
|
// Simulate handling requests until shutdown
|
||||||
let mut request_id = 0;
|
let mut request_id = 0;
|
||||||
@@ -40,12 +38,12 @@ impl Component for WebServer {
|
|||||||
while !cancellation.is_cancelled() {
|
while !cancellation.is_cancelled() {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = cancellation.cancelled() => {
|
_ = cancellation.cancelled() => {
|
||||||
println!("[{}] Shutdown signal received", self.name().unwrap());
|
println!("[{}] Shutdown signal received", self.info());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ = interval.tick() => {
|
_ = interval.tick() => {
|
||||||
request_id += 1;
|
request_id += 1;
|
||||||
println!("[{}] Handling request #{}", self.name().unwrap(), request_id);
|
println!("[{}] Handling request #{}", self.info(), request_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,10 +52,10 @@ impl Component for WebServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn close(&self) -> Result<(), MadError> {
|
async fn close(&self) -> Result<(), MadError> {
|
||||||
println!("[{}] Closing connections...", self.name().unwrap());
|
println!("[{}] Closing connections...", self.info());
|
||||||
// Simulate graceful connection drain
|
// Simulate graceful connection drain
|
||||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
println!("[{}] Server stopped", self.name().unwrap());
|
println!("[{}] Server stopped", self.info());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,14 +68,13 @@ struct QueueProcessor {
|
|||||||
queue_name: String,
|
queue_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for QueueProcessor {
|
impl Component for QueueProcessor {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some(format!("QueueProcessor:{}", self.queue_name))
|
format!("QueueProcessor:{}", self.queue_name).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
||||||
println!("[{}] Started processing", self.name().unwrap());
|
println!("[{}] Started processing", self.info());
|
||||||
|
|
||||||
let mut message_count = 0;
|
let mut message_count = 0;
|
||||||
|
|
||||||
@@ -86,19 +83,19 @@ impl Component for QueueProcessor {
|
|||||||
// Simulate waiting for and processing a message
|
// Simulate waiting for and processing a message
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = cancellation.cancelled() => {
|
_ = cancellation.cancelled() => {
|
||||||
println!("[{}] Stopping message processing", self.name().unwrap());
|
println!("[{}] Stopping message processing", self.info());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ = tokio::time::sleep(Duration::from_secs(1)) => {
|
_ = tokio::time::sleep(Duration::from_secs(1)) => {
|
||||||
message_count += 1;
|
message_count += 1;
|
||||||
println!("[{}] Processed message #{}", self.name().unwrap(), message_count);
|
println!("[{}] Processed message #{}", self.info(), message_count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"[{}] Processed {} messages total",
|
"[{}] Processed {} messages total",
|
||||||
self.name().unwrap(),
|
self.info(),
|
||||||
message_count
|
message_count
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -116,16 +113,15 @@ struct ScheduledTask {
|
|||||||
interval_secs: u64,
|
interval_secs: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for ScheduledTask {
|
impl Component for ScheduledTask {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some(format!("ScheduledTask:{}", self.task_name))
|
format!("ScheduledTask:{}", self.task_name).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
||||||
println!(
|
println!(
|
||||||
"[{}] Scheduled to run every {} seconds",
|
"[{}] Scheduled to run every {} seconds",
|
||||||
self.name().unwrap(),
|
self.info(),
|
||||||
self.interval_secs
|
self.interval_secs
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -135,17 +131,17 @@ impl Component for ScheduledTask {
|
|||||||
while !cancellation.is_cancelled() {
|
while !cancellation.is_cancelled() {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = cancellation.cancelled() => {
|
_ = cancellation.cancelled() => {
|
||||||
println!("[{}] Scheduler stopping", self.name().unwrap());
|
println!("[{}] Scheduler stopping", self.info());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ = interval.tick() => {
|
_ = interval.tick() => {
|
||||||
run_count += 1;
|
run_count += 1;
|
||||||
println!("[{}] Executing run #{}", self.name().unwrap(), run_count);
|
println!("[{}] Executing run #{}", self.info(), run_count);
|
||||||
|
|
||||||
// Simulate task execution
|
// Simulate task execution
|
||||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
|
||||||
println!("[{}] Run #{} completed", self.name().unwrap(), run_count);
|
println!("[{}] Run #{} completed", self.info(), run_count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
84
crates/mad/examples/nested_errors/main.rs
Normal file
84
crates/mad/examples/nested_errors/main.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use notmad::ComponentInfo;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
struct NestedErrorComponent {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl notmad::Component for NestedErrorComponent {
|
||||||
|
fn info(&self) -> ComponentInfo {
|
||||||
|
self.name.clone().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
|
// Simulate a deeply nested error
|
||||||
|
let io_error = std::io::Error::new(
|
||||||
|
std::io::ErrorKind::PermissionDenied,
|
||||||
|
"access denied to /etc/secret",
|
||||||
|
);
|
||||||
|
|
||||||
|
Err(anyhow::Error::from(io_error)
|
||||||
|
.context("failed to read configuration file")
|
||||||
|
.context("unable to initialize database connection pool")
|
||||||
|
.context(format!("component '{}' startup failed", self.name))
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AnotherFailingComponent;
|
||||||
|
|
||||||
|
impl notmad::Component for AnotherFailingComponent {
|
||||||
|
fn info(&self) -> ComponentInfo {
|
||||||
|
"another-component".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
Err(anyhow::anyhow!("network timeout after 30s")
|
||||||
|
.context("failed to connect to external API")
|
||||||
|
.context("service health check failed")
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter("mad=debug")
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let result = notmad::Mad::builder()
|
||||||
|
.add(NestedErrorComponent {
|
||||||
|
name: "database-service".into(),
|
||||||
|
})
|
||||||
|
.add(AnotherFailingComponent)
|
||||||
|
.run()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(()) => println!("Success!"),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("\n=== Error occurred ===");
|
||||||
|
eprintln!("{}", e);
|
||||||
|
|
||||||
|
// Also demonstrate how to walk the error chain manually
|
||||||
|
if let notmad::MadError::AggregateError(ref agg) = e {
|
||||||
|
eprintln!("\n=== Detailed error chains ===");
|
||||||
|
for (i, error) in agg.get_errors().iter().enumerate() {
|
||||||
|
eprintln!("\nComponent {} error chain:", i + 1);
|
||||||
|
if let notmad::MadError::Inner(inner) = error {
|
||||||
|
for (j, cause) in inner.chain().enumerate() {
|
||||||
|
eprintln!(" {}. {}", j + 1, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let notmad::MadError::Inner(ref inner) = e {
|
||||||
|
eprintln!("\n=== Error chain ===");
|
||||||
|
for (i, cause) in inner.chain().enumerate() {
|
||||||
|
eprintln!(" {}. {}", i + 1, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
use async_trait::async_trait;
|
use notmad::ComponentInfo;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
|
||||||
struct WaitServer {}
|
struct WaitServer {}
|
||||||
#[async_trait]
|
|
||||||
impl notmad::Component for WaitServer {
|
impl notmad::Component for WaitServer {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some("WaitServer".into())
|
"WaitServer".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
let millis_wait = rand::thread_rng().gen_range(500..3000);
|
let millis_wait = rand::thread_rng().gen_range(500..3000);
|
||||||
|
|
||||||
tracing::debug!("waiting: {}ms", millis_wait);
|
tracing::debug!("waiting: {}ms", millis_wait);
|
||||||
@@ -23,10 +22,9 @@ impl notmad::Component for WaitServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct RespectCancel {}
|
struct RespectCancel {}
|
||||||
#[async_trait]
|
|
||||||
impl notmad::Component for RespectCancel {
|
impl notmad::Component for RespectCancel {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some("RespectCancel".into())
|
"RespectCancel".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
@@ -38,13 +36,12 @@ impl notmad::Component for RespectCancel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct NeverStopServer {}
|
struct NeverStopServer {}
|
||||||
#[async_trait]
|
|
||||||
impl notmad::Component for NeverStopServer {
|
impl notmad::Component for NeverStopServer {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some("NeverStopServer".into())
|
"NeverStopServer".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
// Simulates a server running for some time. Is normally supposed to be futures blocking indefinitely
|
// Simulates a server running for some time. Is normally supposed to be futures blocking indefinitely
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(999999999)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(999999999)).await;
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,21 @@
|
|||||||
//! # MAD - Lifecycle Manager for Rust Applications
|
//! # MAD - Lifecycle Manager for Rust Applications
|
||||||
//!
|
//!
|
||||||
//! MAD is a robust lifecycle manager designed for long-running Rust operations. It provides
|
//! A simple lifecycle manager for long-running Rust applications. Run multiple services
|
||||||
//! a simple, composable way to manage multiple concurrent services within your application,
|
//! concurrently with graceful shutdown handling.
|
||||||
//! handling graceful startup and shutdown automatically.
|
|
||||||
//!
|
|
||||||
//! ## Overview
|
|
||||||
//!
|
|
||||||
//! MAD helps you build applications composed of multiple long-running components that need
|
|
||||||
//! to be orchestrated together. It handles:
|
|
||||||
//!
|
|
||||||
//! - **Concurrent execution** of multiple components
|
|
||||||
//! - **Graceful shutdown** with cancellation tokens
|
|
||||||
//! - **Error aggregation** from multiple components
|
|
||||||
//! - **Lifecycle management** with setup, run, and close phases
|
|
||||||
//!
|
//!
|
||||||
//! ## Quick Start
|
//! ## Quick Start
|
||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! use notmad::{Component, Mad};
|
//! use notmad::{Component, Mad};
|
||||||
//! use async_trait::async_trait;
|
|
||||||
//! use tokio_util::sync::CancellationToken;
|
//! use tokio_util::sync::CancellationToken;
|
||||||
//!
|
//!
|
||||||
//! struct MyService {
|
//! struct MyService;
|
||||||
//! name: String,
|
|
||||||
//! }
|
|
||||||
//!
|
//!
|
||||||
//! #[async_trait]
|
|
||||||
//! impl Component for MyService {
|
//! impl Component for MyService {
|
||||||
//! fn name(&self) -> Option<String> {
|
//! async fn run(&self, cancel: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
//! Some(self.name.clone())
|
//! println!("Running...");
|
||||||
//! }
|
//! cancel.cancelled().await;
|
||||||
//!
|
//! println!("Stopped");
|
||||||
//! async fn run(&self, cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
|
||||||
//! // Your service logic here
|
|
||||||
//! while !cancellation.is_cancelled() {
|
|
||||||
//! // Do work...
|
|
||||||
//! tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
|
||||||
//! }
|
|
||||||
//! Ok(())
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
@@ -44,40 +23,24 @@
|
|||||||
//! #[tokio::main]
|
//! #[tokio::main]
|
||||||
//! async fn main() -> anyhow::Result<()> {
|
//! async fn main() -> anyhow::Result<()> {
|
||||||
//! Mad::builder()
|
//! Mad::builder()
|
||||||
//! .add(MyService { name: "service-1".into() })
|
//! .add(MyService)
|
||||||
//! .add(MyService { name: "service-2".into() })
|
|
||||||
//! .run()
|
//! .run()
|
||||||
//! .await?;
|
//! .await?;
|
||||||
//! Ok(())
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ## Component Lifecycle
|
//! ## Features
|
||||||
//!
|
//!
|
||||||
//! Components go through three phases:
|
//! - Run multiple components concurrently
|
||||||
//!
|
//! - Graceful shutdown with cancellation tokens
|
||||||
//! 1. **Setup**: Optional initialization phase before components start running
|
//! - Optional lifecycle hooks: `setup()`, `run()`, `close()`
|
||||||
//! 2. **Run**: Main execution phase where components perform their work
|
//! - Automatic error aggregation
|
||||||
//! 3. **Close**: Optional cleanup phase after components stop
|
//! - SIGTERM and Ctrl+C signal handling
|
||||||
//!
|
|
||||||
//! ## Error Handling
|
|
||||||
//!
|
|
||||||
//! MAD provides comprehensive error handling through [`MadError`], which can:
|
|
||||||
//! - Wrap errors from individual components
|
|
||||||
//! - Aggregate multiple errors when several components fail
|
|
||||||
//! - Automatically convert from `anyhow::Error`
|
|
||||||
//!
|
|
||||||
//! ## Shutdown Behavior
|
|
||||||
//!
|
|
||||||
//! MAD handles shutdown gracefully:
|
|
||||||
//! - Responds to SIGTERM and Ctrl+C signals
|
|
||||||
//! - Propagates cancellation tokens to all components
|
|
||||||
//! - Waits for components to finish cleanup
|
|
||||||
//! - Configurable cancellation timeout
|
|
||||||
|
|
||||||
use futures::stream::FuturesUnordered;
|
use futures::stream::FuturesUnordered;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use std::{fmt::Display, sync::Arc};
|
use std::{error::Error, fmt::Display, pin::Pin, sync::Arc};
|
||||||
use tokio::signal::unix::{SignalKind, signal};
|
use tokio::signal::unix::{SignalKind, signal};
|
||||||
|
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
@@ -96,22 +59,25 @@ pub enum MadError {
|
|||||||
///
|
///
|
||||||
/// This variant is used when components return errors via the `?` operator
|
/// This variant is used when components return errors via the `?` operator
|
||||||
/// or when converting from `anyhow::Error`.
|
/// or when converting from `anyhow::Error`.
|
||||||
#[error("component: {0:#?}")]
|
#[error(transparent)]
|
||||||
Inner(#[source] anyhow::Error),
|
Inner(anyhow::Error),
|
||||||
|
|
||||||
/// Error that occurred during the run phase of a component.
|
/// Error that occurred during the run phase of a component.
|
||||||
#[error("component: {run:#?}")]
|
#[error(transparent)]
|
||||||
RunError { run: anyhow::Error },
|
RunError { run: anyhow::Error },
|
||||||
|
|
||||||
/// Error that occurred during the close phase of a component.
|
/// Error that occurred during the close phase of a component.
|
||||||
#[error("component(s) failed: {close}")]
|
#[error("component(s) failed during close")]
|
||||||
CloseError { close: anyhow::Error },
|
CloseError {
|
||||||
|
#[source]
|
||||||
|
close: anyhow::Error,
|
||||||
|
},
|
||||||
|
|
||||||
/// Multiple errors from different components.
|
/// Multiple errors from different components.
|
||||||
///
|
///
|
||||||
/// This is used when multiple components fail simultaneously,
|
/// This is used when multiple components fail simultaneously,
|
||||||
/// allowing all errors to be reported rather than just the first one.
|
/// allowing all errors to be reported rather than just the first one.
|
||||||
#[error("component(s): {0}")]
|
#[error("{0}")]
|
||||||
AggregateError(AggregateError),
|
AggregateError(AggregateError),
|
||||||
|
|
||||||
/// Returned when a component doesn't implement the optional setup method.
|
/// Returned when a component doesn't implement the optional setup method.
|
||||||
@@ -137,7 +103,7 @@ impl From<anyhow::Error> for MadError {
|
|||||||
///
|
///
|
||||||
/// When multiple components fail, their errors are collected
|
/// When multiple components fail, their errors are collected
|
||||||
/// into this struct to provide complete error reporting.
|
/// into this struct to provide complete error reporting.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub struct AggregateError {
|
pub struct AggregateError {
|
||||||
errors: Vec<MadError>,
|
errors: Vec<MadError>,
|
||||||
}
|
}
|
||||||
@@ -160,6 +126,10 @@ impl AggregateError {
|
|||||||
pub fn get_errors(&self) -> &[MadError] {
|
pub fn get_errors(&self) -> &[MadError] {
|
||||||
&self.errors
|
&self.errors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn take_errors(self) -> Vec<MadError> {
|
||||||
|
self.errors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for AggregateError {
|
impl Display for AggregateError {
|
||||||
@@ -169,17 +139,23 @@ impl Display for AggregateError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.errors.len() == 1 {
|
if self.errors.len() == 1 {
|
||||||
return f.write_str(&self.errors.first().unwrap().to_string());
|
return write!(f, "{}", self.errors[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
f.write_str("MadError::AggregateError: (")?;
|
writeln!(f, "{} component errors occurred:", self.errors.len())?;
|
||||||
|
for (i, error) in self.errors.iter().enumerate() {
|
||||||
|
write!(f, "\n[Component {}] {}", i + 1, error)?;
|
||||||
|
|
||||||
for error in &self.errors {
|
// Print the error chain for each component error
|
||||||
f.write_str(&error.to_string())?;
|
let mut source = error.source();
|
||||||
f.write_str(", ")?;
|
let mut level = 1;
|
||||||
|
while let Some(err) = source {
|
||||||
|
write!(f, "\n {}. {}", level, err)?;
|
||||||
|
source = err.source();
|
||||||
|
level += 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
f.write_str(")")
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,12 +168,10 @@ impl Display for AggregateError {
|
|||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use notmad::{Component, Mad};
|
/// use notmad::{Component, Mad};
|
||||||
/// use async_trait::async_trait;
|
|
||||||
/// use tokio_util::sync::CancellationToken;
|
/// use tokio_util::sync::CancellationToken;
|
||||||
///
|
///
|
||||||
/// struct MyComponent;
|
/// struct MyComponent;
|
||||||
///
|
///
|
||||||
/// #[async_trait]
|
|
||||||
/// impl Component for MyComponent {
|
/// impl Component for MyComponent {
|
||||||
/// async fn run(&self, _cancel: CancellationToken) -> Result<(), notmad::MadError> {
|
/// async fn run(&self, _cancel: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
/// Ok(())
|
/// Ok(())
|
||||||
@@ -213,7 +187,7 @@ impl Display for AggregateError {
|
|||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Mad {
|
pub struct Mad {
|
||||||
components: Vec<Arc<dyn Component + Send + Sync + 'static>>,
|
components: Vec<SharedComponent>,
|
||||||
|
|
||||||
should_cancel: Option<std::time::Duration>,
|
should_cancel: Option<std::time::Duration>,
|
||||||
}
|
}
|
||||||
@@ -257,10 +231,8 @@ impl Mad {
|
|||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use notmad::{Component, Mad};
|
/// use notmad::{Component, Mad};
|
||||||
/// # use async_trait::async_trait;
|
|
||||||
/// # use tokio_util::sync::CancellationToken;
|
/// # use tokio_util::sync::CancellationToken;
|
||||||
/// # struct MyService;
|
/// # struct MyService;
|
||||||
/// # #[async_trait]
|
|
||||||
/// # impl Component for MyService {
|
/// # impl Component for MyService {
|
||||||
/// # async fn run(&self, _: CancellationToken) -> Result<(), notmad::MadError> { Ok(()) }
|
/// # async fn run(&self, _: CancellationToken) -> Result<(), notmad::MadError> { Ok(()) }
|
||||||
/// # }
|
/// # }
|
||||||
@@ -290,10 +262,8 @@ impl Mad {
|
|||||||
/// ```rust
|
/// ```rust
|
||||||
/// use notmad::Mad;
|
/// use notmad::Mad;
|
||||||
/// # use notmad::Component;
|
/// # use notmad::Component;
|
||||||
/// # use async_trait::async_trait;
|
|
||||||
/// # use tokio_util::sync::CancellationToken;
|
/// # use tokio_util::sync::CancellationToken;
|
||||||
/// # struct DebugService;
|
/// # struct DebugService;
|
||||||
/// # #[async_trait]
|
|
||||||
/// # impl Component for DebugService {
|
/// # impl Component for DebugService {
|
||||||
/// # async fn run(&self, _: CancellationToken) -> Result<(), notmad::MadError> { Ok(()) }
|
/// # async fn run(&self, _: CancellationToken) -> Result<(), notmad::MadError> { Ok(()) }
|
||||||
/// # }
|
/// # }
|
||||||
@@ -456,7 +426,7 @@ impl Mad {
|
|||||||
tracing::debug!("setting up components");
|
tracing::debug!("setting up components");
|
||||||
|
|
||||||
for comp in &self.components {
|
for comp in &self.components {
|
||||||
tracing::trace!(component = &comp.name(), "mad setting up");
|
tracing::trace!(component = %comp.info(), "mad setting up");
|
||||||
|
|
||||||
match comp.setup().await {
|
match comp.setup().await {
|
||||||
Ok(_) | Err(MadError::SetupNotDefined) => {}
|
Ok(_) | Err(MadError::SetupNotDefined) => {}
|
||||||
@@ -486,16 +456,37 @@ impl Mad {
|
|||||||
channels.push(error_rx);
|
channels.push(error_rx);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let name = comp.name().clone();
|
let info = comp.info().clone();
|
||||||
|
|
||||||
tracing::debug!(component = name, "mad running");
|
tracing::debug!(component = %info, "mad running");
|
||||||
|
|
||||||
|
let handle = tokio::spawn(async move { comp.run(job_cancellation).await });
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = cancellation_token.cancelled() => {
|
_ = cancellation_token.cancelled() => {
|
||||||
error_tx.send(CompletionResult { res: Ok(()) , name }).await
|
error_tx.send(CompletionResult { res: Ok(()) , name: info.name }).await
|
||||||
}
|
}
|
||||||
res = comp.run(job_cancellation) => {
|
res = handle => {
|
||||||
error_tx.send(CompletionResult { res , name }).await
|
let res = match res {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(join) => {
|
||||||
|
match join.source() {
|
||||||
|
Some(error) => {
|
||||||
|
Err(MadError::RunError{run: anyhow::anyhow!("component aborted: {:?}", error)})
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
if join.is_panic(){
|
||||||
|
Err(MadError::RunError { run: anyhow::anyhow!("component panicked: {}", join) })
|
||||||
|
} else {
|
||||||
|
Err(MadError::RunError { run: anyhow::anyhow!("component faced unknown error: {}", join) })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
error_tx.send(CompletionResult { res , name: info.name }).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -572,7 +563,7 @@ impl Mad {
|
|||||||
tracing::debug!("closing components");
|
tracing::debug!("closing components");
|
||||||
|
|
||||||
for comp in &self.components {
|
for comp in &self.components {
|
||||||
tracing::trace!(component = &comp.name(), "mad closing");
|
tracing::trace!(component = %comp.info(), "mad closing");
|
||||||
match comp.close().await {
|
match comp.close().await {
|
||||||
Ok(_) | Err(MadError::CloseNotDefined) => {}
|
Ok(_) | Err(MadError::CloseNotDefined) => {}
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
@@ -590,6 +581,46 @@ async fn signal_unix_terminate() {
|
|||||||
sigterm.recv().await;
|
sigterm.recv().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct ComponentInfo {
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ComponentInfo {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(
|
||||||
|
self.name
|
||||||
|
.as_ref()
|
||||||
|
.map(|n| n.as_str())
|
||||||
|
.unwrap_or_else(|| "unknown"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ComponentInfo {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_name(&mut self, name: impl Into<String>) -> &mut Self {
|
||||||
|
self.name = Some(name.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for ComponentInfo {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self { name: Some(value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<&str> for ComponentInfo {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
name: Some(value.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Trait for implementing MAD components.
|
/// Trait for implementing MAD components.
|
||||||
///
|
///
|
||||||
/// Components represent individual services or tasks that run as part
|
/// Components represent individual services or tasks that run as part
|
||||||
@@ -599,18 +630,16 @@ async fn signal_unix_terminate() {
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use notmad::{Component, MadError};
|
/// use notmad::{Component, ComponentInfo, MadError};
|
||||||
/// use async_trait::async_trait;
|
|
||||||
/// use tokio_util::sync::CancellationToken;
|
/// use tokio_util::sync::CancellationToken;
|
||||||
///
|
///
|
||||||
/// struct DatabaseConnection {
|
/// struct DatabaseConnection {
|
||||||
/// url: String,
|
/// url: String,
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// #[async_trait]
|
|
||||||
/// impl Component for DatabaseConnection {
|
/// impl Component for DatabaseConnection {
|
||||||
/// fn name(&self) -> Option<String> {
|
/// fn info(&self) -> ComponentInfo {
|
||||||
/// Some("database".to_string())
|
/// "database".into()
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// async fn setup(&self) -> Result<(), MadError> {
|
/// async fn setup(&self) -> Result<(), MadError> {
|
||||||
@@ -632,8 +661,7 @@ async fn signal_unix_terminate() {
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[async_trait::async_trait]
|
pub trait Component: Send + Sync + 'static {
|
||||||
pub trait Component {
|
|
||||||
/// Returns an optional name for the component.
|
/// Returns an optional name for the component.
|
||||||
///
|
///
|
||||||
/// This name is used in logging and error messages to identify
|
/// This name is used in logging and error messages to identify
|
||||||
@@ -642,8 +670,8 @@ pub trait Component {
|
|||||||
/// # Default
|
/// # Default
|
||||||
///
|
///
|
||||||
/// Returns `None` if not overridden.
|
/// Returns `None` if not overridden.
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
None
|
ComponentInfo::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Optional setup phase called before the component starts running.
|
/// Optional setup phase called before the component starts running.
|
||||||
@@ -661,8 +689,8 @@ pub trait Component {
|
|||||||
///
|
///
|
||||||
/// If setup fails with an error other than `SetupNotDefined`,
|
/// If setup fails with an error other than `SetupNotDefined`,
|
||||||
/// the entire application will stop before any components start running.
|
/// the entire application will stop before any components start running.
|
||||||
async fn setup(&self) -> Result<(), MadError> {
|
fn setup(&self) -> impl Future<Output = Result<(), MadError>> + Send + '_ {
|
||||||
Err(MadError::SetupNotDefined)
|
async { Err(MadError::SetupNotDefined) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Main execution phase of the component.
|
/// Main execution phase of the component.
|
||||||
@@ -684,7 +712,10 @@ pub trait Component {
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Any error returned will trigger shutdown of all other components.
|
/// Any error returned will trigger shutdown of all other components.
|
||||||
async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError>;
|
fn run(
|
||||||
|
&self,
|
||||||
|
cancellation_token: CancellationToken,
|
||||||
|
) -> impl Future<Output = Result<(), MadError>> + Send + '_;
|
||||||
|
|
||||||
/// Optional cleanup phase called after the component stops.
|
/// Optional cleanup phase called after the component stops.
|
||||||
///
|
///
|
||||||
@@ -701,8 +732,73 @@ pub trait Component {
|
|||||||
///
|
///
|
||||||
/// Errors during close are logged but don't prevent other components
|
/// Errors during close are logged but don't prevent other components
|
||||||
/// from closing.
|
/// from closing.
|
||||||
|
fn close(&self) -> impl Future<Output = Result<(), MadError>> + Send + '_ {
|
||||||
|
async { Err(MadError::CloseNotDefined) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait AsyncComponent: Send + Sync + 'static {
|
||||||
|
fn info_async(&self) -> ComponentInfo;
|
||||||
|
|
||||||
|
fn setup_async(&self) -> Pin<Box<dyn Future<Output = Result<(), MadError>> + Send + '_>>;
|
||||||
|
|
||||||
|
fn run_async(
|
||||||
|
&self,
|
||||||
|
cancellation_token: CancellationToken,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<(), MadError>> + Send + '_>>;
|
||||||
|
|
||||||
|
fn close_async(&self) -> Pin<Box<dyn Future<Output = Result<(), MadError>> + Send + '_>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Component> AsyncComponent for E {
|
||||||
|
#[inline(always)]
|
||||||
|
fn info_async(&self) -> ComponentInfo {
|
||||||
|
self.info()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn setup_async(&self) -> Pin<Box<dyn Future<Output = Result<(), MadError>> + Send + '_>> {
|
||||||
|
Box::pin(self.setup())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn run_async(
|
||||||
|
&self,
|
||||||
|
cancellation_token: CancellationToken,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<(), MadError>> + Send + '_>> {
|
||||||
|
Box::pin(self.run(cancellation_token))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn close_async(&self) -> Pin<Box<dyn Future<Output = Result<(), MadError>> + Send + '_>> {
|
||||||
|
Box::pin(self.close())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SharedComponent {
|
||||||
|
component: Arc<dyn AsyncComponent + Send + Sync + 'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharedComponent {
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn info(&self) -> ComponentInfo {
|
||||||
|
self.component.info_async()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
async fn setup(&self) -> Result<(), MadError> {
|
||||||
|
self.component.setup_async().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError> {
|
||||||
|
self.component.run_async(cancellation_token).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
async fn close(&self) -> Result<(), MadError> {
|
async fn close(&self) -> Result<(), MadError> {
|
||||||
Err(MadError::CloseNotDefined)
|
self.component.close_async().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,12 +812,10 @@ pub trait Component {
|
|||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use notmad::{Component, IntoComponent, Mad};
|
/// use notmad::{Component, IntoComponent, Mad};
|
||||||
/// # use async_trait::async_trait;
|
|
||||||
/// # use tokio_util::sync::CancellationToken;
|
/// # use tokio_util::sync::CancellationToken;
|
||||||
///
|
///
|
||||||
/// struct MyService;
|
/// struct MyService;
|
||||||
///
|
///
|
||||||
/// # #[async_trait]
|
|
||||||
/// # impl Component for MyService {
|
/// # impl Component for MyService {
|
||||||
/// # async fn run(&self, _: CancellationToken) -> Result<(), notmad::MadError> { Ok(()) }
|
/// # async fn run(&self, _: CancellationToken) -> Result<(), notmad::MadError> { Ok(()) }
|
||||||
/// # }
|
/// # }
|
||||||
@@ -732,12 +826,14 @@ pub trait Component {
|
|||||||
/// ```
|
/// ```
|
||||||
pub trait IntoComponent {
|
pub trait IntoComponent {
|
||||||
/// Converts self into an Arc-wrapped component.
|
/// Converts self into an Arc-wrapped component.
|
||||||
fn into_component(self) -> Arc<dyn Component + Send + Sync + 'static>;
|
fn into_component(self) -> SharedComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Component + Send + Sync + 'static> IntoComponent for T {
|
impl<T: Component> IntoComponent for T {
|
||||||
fn into_component(self) -> Arc<dyn Component + Send + Sync + 'static> {
|
fn into_component(self) -> SharedComponent {
|
||||||
Arc::new(self)
|
SharedComponent {
|
||||||
|
component: Arc::new(self),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,7 +857,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl<F, Fut> Component for ClosureComponent<F, Fut>
|
impl<F, Fut> Component for ClosureComponent<F, Fut>
|
||||||
where
|
where
|
||||||
F: Fn(CancellationToken) -> Fut + Send + Sync + 'static,
|
F: Fn(CancellationToken) -> Fut + Send + Sync + 'static,
|
||||||
@@ -771,3 +866,130 @@ where
|
|||||||
self.execute(cancellation_token).await
|
self.execute(cancellation_token).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_chaining_display() {
|
||||||
|
// Test single error with context chain
|
||||||
|
let base_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
|
||||||
|
let error = anyhow::Error::from(base_error)
|
||||||
|
.context("failed to read configuration")
|
||||||
|
.context("unable to initialize database")
|
||||||
|
.context("service startup failed");
|
||||||
|
|
||||||
|
let mad_error = MadError::Inner(error);
|
||||||
|
let display = format!("{}", mad_error);
|
||||||
|
|
||||||
|
// Should display the top-level error message
|
||||||
|
assert!(display.contains("service startup failed"));
|
||||||
|
|
||||||
|
// Test error chain iteration
|
||||||
|
if let MadError::Inner(ref e) = mad_error {
|
||||||
|
let chain: Vec<String> = e.chain().map(|c| c.to_string()).collect();
|
||||||
|
assert_eq!(chain.len(), 4);
|
||||||
|
assert_eq!(chain[0], "service startup failed");
|
||||||
|
assert_eq!(chain[1], "unable to initialize database");
|
||||||
|
assert_eq!(chain[2], "failed to read configuration");
|
||||||
|
assert_eq!(chain[3], "file not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_aggregate_error_display() {
|
||||||
|
let error1 = MadError::Inner(
|
||||||
|
anyhow::anyhow!("database connection failed")
|
||||||
|
.context("failed to connect to PostgreSQL"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let error2 = MadError::Inner(
|
||||||
|
anyhow::anyhow!("port already in use")
|
||||||
|
.context("failed to bind to port 8080")
|
||||||
|
.context("web server initialization failed"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let aggregate = MadError::AggregateError(AggregateError {
|
||||||
|
errors: vec![error1, error2],
|
||||||
|
});
|
||||||
|
|
||||||
|
let display = format!("{}", aggregate);
|
||||||
|
|
||||||
|
// Check that it shows multiple errors
|
||||||
|
assert!(display.contains("2 component errors occurred"));
|
||||||
|
assert!(display.contains("[Component 1]"));
|
||||||
|
assert!(display.contains("[Component 2]"));
|
||||||
|
|
||||||
|
// Check that context chains are displayed
|
||||||
|
assert!(display.contains("failed to connect to PostgreSQL"));
|
||||||
|
assert!(display.contains("database connection failed"));
|
||||||
|
assert!(display.contains("web server initialization failed"));
|
||||||
|
assert!(display.contains("failed to bind to port 8080"));
|
||||||
|
assert!(display.contains("port already in use"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_single_error_aggregate() {
|
||||||
|
let error = MadError::Inner(anyhow::anyhow!("single error"));
|
||||||
|
let aggregate = AggregateError {
|
||||||
|
errors: vec![error],
|
||||||
|
};
|
||||||
|
|
||||||
|
let display = format!("{}", aggregate);
|
||||||
|
// Single error should be displayed directly
|
||||||
|
assert!(display.contains("single error"));
|
||||||
|
assert!(!display.contains("component errors occurred"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_source_chain() {
|
||||||
|
let error = MadError::Inner(
|
||||||
|
anyhow::anyhow!("root cause")
|
||||||
|
.context("middle layer")
|
||||||
|
.context("top layer"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test that we can access the error chain
|
||||||
|
if let MadError::Inner(ref e) = error {
|
||||||
|
let chain: Vec<String> = e.chain().map(|c| c.to_string()).collect();
|
||||||
|
assert_eq!(chain.len(), 3);
|
||||||
|
assert_eq!(chain[0], "top layer");
|
||||||
|
assert_eq!(chain[1], "middle layer");
|
||||||
|
assert_eq!(chain[2], "root cause");
|
||||||
|
} else {
|
||||||
|
panic!("Expected MadError::Inner");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_component_error_propagation() {
|
||||||
|
struct FailingComponent;
|
||||||
|
|
||||||
|
impl Component for FailingComponent {
|
||||||
|
fn info(&self) -> ComponentInfo {
|
||||||
|
"test-component".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, _cancel: CancellationToken) -> Result<(), MadError> {
|
||||||
|
Err(anyhow::anyhow!("IO error")
|
||||||
|
.context("failed to open file")
|
||||||
|
.context("component initialization failed")
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = Mad::builder()
|
||||||
|
.add(FailingComponent)
|
||||||
|
.cancellation(Some(std::time::Duration::from_millis(100)))
|
||||||
|
.run()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let error = result.unwrap_err();
|
||||||
|
|
||||||
|
// Check error display
|
||||||
|
let display = format!("{}", error);
|
||||||
|
assert!(display.contains("component initialization failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,19 +4,15 @@
|
|||||||
//! without performing any work. Useful for keeping the application alive
|
//! without performing any work. Useful for keeping the application alive
|
||||||
//! or as placeholders in conditional component loading.
|
//! or as placeholders in conditional component loading.
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::{Component, MadError};
|
use crate::{Component, ComponentInfo, IntoComponent, MadError, SharedComponent};
|
||||||
|
|
||||||
/// A default waiter component that panics if run.
|
/// A default waiter component that panics if run.
|
||||||
///
|
///
|
||||||
/// This is used internally as a placeholder that should never
|
/// This is used internally as a placeholder that should never
|
||||||
/// actually be executed.
|
/// actually be executed.
|
||||||
pub struct DefaultWaiter {}
|
pub struct DefaultWaiter;
|
||||||
#[async_trait]
|
|
||||||
impl Component for DefaultWaiter {
|
impl Component for DefaultWaiter {
|
||||||
async fn run(&self, _cancellation_token: CancellationToken) -> Result<(), MadError> {
|
async fn run(&self, _cancellation_token: CancellationToken) -> Result<(), MadError> {
|
||||||
panic!("should never be called");
|
panic!("should never be called");
|
||||||
@@ -38,13 +34,13 @@ impl Component for DefaultWaiter {
|
|||||||
/// let waiter = Waiter::new(service.into_component());
|
/// let waiter = Waiter::new(service.into_component());
|
||||||
/// ```
|
/// ```
|
||||||
pub struct Waiter {
|
pub struct Waiter {
|
||||||
comp: Arc<dyn Component + Send + Sync + 'static>,
|
comp: SharedComponent,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Waiter {
|
impl Default for Waiter {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
comp: Arc::new(DefaultWaiter {}),
|
comp: DefaultWaiter {}.into_component(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,21 +50,20 @@ impl Waiter {
|
|||||||
///
|
///
|
||||||
/// The wrapped component's name will be used (prefixed with "waiter/"),
|
/// The wrapped component's name will be used (prefixed with "waiter/"),
|
||||||
/// but its run method will not be called.
|
/// but its run method will not be called.
|
||||||
pub fn new(c: Arc<dyn Component + Send + Sync + 'static>) -> Self {
|
pub fn new(c: SharedComponent) -> Self {
|
||||||
Self { comp: c }
|
Self { comp: c }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for Waiter {
|
impl Component for Waiter {
|
||||||
/// Returns the name of the waiter, prefixed with "waiter/".
|
/// Returns the name of the waiter, prefixed with "waiter/".
|
||||||
///
|
///
|
||||||
/// If the wrapped component has a name, it will be "waiter/{name}".
|
/// If the wrapped component has a name, it will be "waiter/{name}".
|
||||||
/// Otherwise, returns "waiter".
|
/// Otherwise, returns "waiter".
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
match self.comp.name() {
|
match &self.comp.info().name {
|
||||||
Some(name) => Some(format!("waiter/{name}")),
|
Some(name) => format!("waiter/{name}").into(),
|
||||||
None => Some("waiter".into()),
|
None => "waiter".into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use notmad::{Component, ComponentInfo, Mad, MadError};
|
||||||
use notmad::{Component, Mad, MadError};
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
@@ -9,13 +8,12 @@ use tracing_test::traced_test;
|
|||||||
|
|
||||||
struct NeverEndingRun {}
|
struct NeverEndingRun {}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for NeverEndingRun {
|
impl Component for NeverEndingRun {
|
||||||
fn name(&self) -> Option<String> {
|
fn info(&self) -> ComponentInfo {
|
||||||
Some("NeverEndingRun".into())
|
"NeverEndingRun".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
let millis_wait = rand::thread_rng().gen_range(50..1000);
|
let millis_wait = rand::thread_rng().gen_range(50..1000);
|
||||||
|
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(millis_wait)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(millis_wait)).await;
|
||||||
@@ -138,6 +136,30 @@ async fn test_can_shutdown_gracefully() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[traced_test]
|
||||||
|
async fn test_component_panics_shutdowns_cleanly() -> anyhow::Result<()> {
|
||||||
|
let res = Mad::builder()
|
||||||
|
.add_fn({
|
||||||
|
move |_cancel| async move {
|
||||||
|
panic!("my inner panic");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.add_fn(|cancel| async move {
|
||||||
|
cancel.cancelled().await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let err_content = res.unwrap_err().to_string();
|
||||||
|
assert!(err_content.contains("component panicked"));
|
||||||
|
assert!(err_content.contains("my inner panic"));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_can_easily_transform_error() -> anyhow::Result<()> {
|
fn test_can_easily_transform_error() -> anyhow::Result<()> {
|
||||||
fn fallible() -> anyhow::Result<()> {
|
fn fallible() -> anyhow::Result<()> {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ base: "git@git.kjuulh.io:kjuulh/cuddle-rust-lib-plan.git"
|
|||||||
vars:
|
vars:
|
||||||
service: "mad"
|
service: "mad"
|
||||||
registry: kasperhermansen
|
registry: kasperhermansen
|
||||||
|
rust:
|
||||||
|
publish: {}
|
||||||
|
|
||||||
please:
|
please:
|
||||||
project:
|
project:
|
||||||
|
|||||||
Reference in New Issue
Block a user