Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
145e067454 | |||
82de5b260f
|
|||
eb360e565c | |||
c18c8a885c
|
|||
762da1e672
|
|||
3bc512ab48 | |||
7d8071d41b
|
|||
1cc4138ec7
|
|||
00517daaaa
|
|||
b941dc9a76 | |||
5da3c83c12
|
|||
a16bee8e37 | |||
a61f00a79d
|
|||
2bd9bd7b8e | |||
c79ff2fde0 | |||
c29a84d15e | |||
01274c1364 | |||
9c3f2cb7f7 | |||
9489f1a5a8 | |||
f6aba7fac6 | |||
772366e267 | |||
1e08ee3dbb | |||
78f0c4057a | |||
cf5d5268f6 | |||
ce2479f6ca | |||
82d4699bca | |||
5ab7cae1fe | |||
f049750e4c | |||
0b5f19fc77 | |||
14eabdbe82 | |||
ea568449fe | |||
6ec3a6031e | |||
0f8fd2343e | |||
12c00941b5 |
61
CHANGELOG.md
61
CHANGELOG.md
@@ -6,6 +6,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.8.1] - 2025-08-09
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- error logging
|
||||||
|
|
||||||
|
## [0.8.0] - 2025-08-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- add docs
|
||||||
|
- update readme
|
||||||
|
|
||||||
|
## [0.7.5] - 2025-07-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- print big inner
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- more error correction
|
||||||
|
- correct error test to not be as verbose
|
||||||
|
|
||||||
|
## [0.7.4] - 2025-07-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- cleanup aggregate error for single error
|
||||||
|
|
||||||
|
## [0.7.3] - 2025-07-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- automatic conversion from anyhow::Error and access to aggregate errors
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- *(deps)* update all dependencies (#30)
|
||||||
|
|
||||||
|
## [0.7.2] - 2025-06-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- add wait
|
||||||
|
- add conditional, allows adding or waiting for close
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- *(deps)* update rust crate async-trait to v0.1.86 (#28)
|
||||||
|
- *(deps)* update rust crate rand to 0.9.0 (#27)
|
||||||
|
- *(deps)* update rust crate thiserror to v2.0.11 (#26)
|
||||||
|
- *(deps)* update all dependencies (#25)
|
||||||
|
- *(deps)* update rust crate async-trait to v0.1.84 (#24)
|
||||||
|
- *(deps)* update rust crate thiserror to v2.0.9 (#22)
|
||||||
|
- *(deps)* update rust crate thiserror to v2.0.8 (#21)
|
||||||
|
- *(deps)* update rust crate thiserror to v2.0.7 (#20)
|
||||||
|
- *(deps)* update rust crate thiserror to v2.0.6 (#19)
|
||||||
|
- *(deps)* update rust crate thiserror to v2.0.5 (#18)
|
||||||
|
- *(deps)* update rust crate tokio-util to v0.7.13 (#17)
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- chore
|
||||||
|
|
||||||
|
- *(deps)* update all dependencies (#29)
|
||||||
|
- *(deps)* update rust crate anyhow to v1.0.95 (#23)
|
||||||
|
- *(deps)* update all dependencies (#16)
|
||||||
|
- *(deps)* update rust crate tracing-subscriber to v0.3.19 (#15)
|
||||||
|
- *(deps)* update rust crate tracing to v0.1.41 (#13)
|
||||||
|
|
||||||
## [0.7.1] - 2024-11-24
|
## [0.7.1] - 2024-11-24
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
128
Cargo.lock
generated
128
Cargo.lock
generated
@@ -28,15 +28,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.93"
|
version = "1.0.98"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
|
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.83"
|
version = "0.1.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
|
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -179,13 +179,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.15"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.13.3+wasi-0.2.2",
|
||||||
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -200,6 +201,17 @@ 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"
|
||||||
@@ -208,9 +220,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.164"
|
version = "0.2.169"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
|
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
@@ -260,13 +272,13 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi",
|
"hermit-abi",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notmad"
|
name = "notmad"
|
||||||
version = "0.7.0"
|
version = "0.7.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -353,7 +365,7 @@ version = "0.2.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy",
|
"zerocopy 0.7.35",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -376,20 +388,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_chacha"
|
name = "rand_chacha"
|
||||||
version = "0.3.1"
|
version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
@@ -397,11 +408,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.6.4"
|
version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
|
"zerocopy 0.8.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -525,18 +537,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.3"
|
version = "2.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa"
|
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.3"
|
version = "2.0.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
|
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -555,17 +567,19 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.41.1"
|
version = "1.46.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
|
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"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",
|
||||||
@@ -573,9 +587,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -584,9 +598,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.12"
|
version = "0.7.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
|
checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -597,9 +611,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.40"
|
version = "0.1.41"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -609,9 +623,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.27"
|
version = "0.1.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -620,9 +634,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.32"
|
version = "0.1.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"valuable",
|
"valuable",
|
||||||
@@ -641,9 +655,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-subscriber"
|
name = "tracing-subscriber"
|
||||||
version = "0.3.18"
|
version = "0.3.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"matchers",
|
"matchers",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
@@ -696,6 +710,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.13.3+wasi-0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -791,6 +814,15 @@ 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 = "wit-bindgen-rt"
|
||||||
|
version = "0.33.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.7.35"
|
version = "0.7.35"
|
||||||
@@ -798,7 +830,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"zerocopy-derive",
|
"zerocopy-derive 0.7.35",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive 0.8.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -811,3 +852,14 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
|||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.7.1"
|
version = "0.8.1"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
mad = { path = "crates/mad" }
|
mad = { path = "crates/mad" }
|
||||||
|
159
README.md
159
README.md
@@ -1,39 +1,92 @@
|
|||||||
# MAD
|
# MAD - Lifecycle Manager for Rust Applications
|
||||||
|
|
||||||
Mad is a life-cycle manager for long running rust operations.
|
[](https://crates.io/crates/notmad)
|
||||||
|
[](https://docs.rs/notmad)
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
- Webservers
|
## Overview
|
||||||
- Queue bindings
|
|
||||||
- gRPC servers etc
|
|
||||||
- Cron runners
|
|
||||||
|
|
||||||
It is supposed to be the main thing the application runs, and everything from it is spawned and managed by it.
|
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
|
||||||
|
|
||||||
|
Add MAD to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
notmad = "0.7.5"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
async-trait = "0.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Here's a simple example of a component that simulates a long-running server:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
struct WaitServer {}
|
use mad::{Component, Mad};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
// Define your component
|
||||||
|
struct WebServer {
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Component for WaitServer {
|
impl Component for WebServer {
|
||||||
fn name(&self) -> Option<String> {
|
fn name(&self) -> Option<String> {
|
||||||
Some("NeverEndingRun".into())
|
Some(format!("WebServer on port {}", self.port))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation: CancellationToken) -> Result<(), mad::MadError> {
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), mad::MadError> {
|
||||||
let millis_wait = rand::thread_rng().gen_range(50..1000);
|
println!("Starting web server on port {}", self.port);
|
||||||
|
|
||||||
// Simulates a server running for some time. Is normally supposed to be futures blocking indefinitely
|
// Your server logic here
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(millis_wait)).await;
|
// The cancellation token will be triggered on shutdown
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancellation.cancelled() => {
|
||||||
|
println!("Shutting down web server");
|
||||||
|
}
|
||||||
|
_ = self.serve() => {
|
||||||
|
println!("Server 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(WaitServer {})
|
.add(WebServer { port: 8080 })
|
||||||
.add(WaitServer {})
|
.add(WebServer { port: 8081 }) // You can add multiple instances
|
||||||
.add(WaitServer {})
|
|
||||||
.run()
|
.run()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -41,11 +94,75 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Custom Components
|
||||||
|
|
||||||
|
Components can be anything that implements the `Component` trait:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mad::{Component, Mad};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
struct QueueProcessor {
|
||||||
|
queue_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for QueueProcessor {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some(format!("QueueProcessor-{}", self.queue_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), mad::MadError> {
|
||||||
|
while !cancellation.is_cancelled() {
|
||||||
|
// Process messages from queue
|
||||||
|
self.process_next_message().await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
MAD provides comprehensive error handling through the `MadError` type with automatic conversion from `anyhow::Error`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), mad::MadError> {
|
||||||
|
// Errors automatically convert from anyhow::Error to MadError
|
||||||
|
database_operation().await?;
|
||||||
|
|
||||||
|
// Or return explicit errors
|
||||||
|
if some_condition {
|
||||||
|
return Err(anyhow::anyhow!("Something went wrong").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Can be found (here)[crates/mad/examples]
|
Check out the [examples directory](crates/mad/examples) for more detailed examples:
|
||||||
|
|
||||||
- basic
|
- **basic** - Simple component lifecycle
|
||||||
- fn
|
- **fn** - Using functions as components
|
||||||
- signals
|
- **signals** - Handling system signals
|
||||||
- error_log
|
- **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
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Created and maintained by [kjuulh](https://github.com/kjuulh)
|
||||||
|
|
||||||
|
## Repository
|
||||||
|
|
||||||
|
Find the source code at [https://github.com/kjuulh/mad](https://github.com/kjuulh/mad)
|
||||||
|
@@ -4,15 +4,15 @@ version.workspace = true
|
|||||||
description = "notmad is a life-cycle manager for long running rust operations"
|
description = "notmad is a life-cycle manager for long running rust operations"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/kjuulh/mad"
|
repository = "https://github.com/kjuulh/mad"
|
||||||
author = "kjuulh"
|
authors = ["kjuulh"]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-trait = "0.1.81"
|
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.8.5"
|
rand = "0.9.0"
|
||||||
thiserror = "2.0.0"
|
thiserror = "2.0.0"
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tokio-util = "0.7.11"
|
tokio-util = "0.7.11"
|
||||||
|
18
crates/mad/examples/comprehensive/Cargo.toml
Normal file
18
crates/mad/examples/comprehensive/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "mad-comprehensive-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "comprehensive"
|
||||||
|
path = "main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
notmad = { path = "../.." }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-util = "0.7"
|
||||||
|
async-trait = "0.1"
|
||||||
|
anyhow = "1"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
rand = "0.8"
|
332
crates/mad/examples/comprehensive/main.rs
Normal file
332
crates/mad/examples/comprehensive/main.rs
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
//! Comprehensive example demonstrating MAD's full capabilities.
|
||||||
|
//!
|
||||||
|
//! This example shows:
|
||||||
|
//! - Multiple component types (struct, closure, conditional)
|
||||||
|
//! - Component lifecycle (setup, run, close)
|
||||||
|
//! - Error handling and propagation
|
||||||
|
//! - Graceful shutdown with cancellation tokens
|
||||||
|
//! - Concurrent component execution
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use notmad::{Component, Mad, MadError};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use tokio::time::{Duration, interval};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
|
/// A web server component that simulates handling HTTP requests.
|
||||||
|
struct WebServer {
|
||||||
|
port: u16,
|
||||||
|
request_count: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for WebServer {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some(format!("web-server-{}", self.port))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup(&self) -> Result<(), MadError> {
|
||||||
|
info!("Setting up web server on port {}", self.port);
|
||||||
|
// In a real application, you might:
|
||||||
|
// - Bind to the port
|
||||||
|
// - Set up TLS certificates
|
||||||
|
// - Initialize middleware
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
info!("Web server on port {} is ready", self.port);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
||||||
|
info!("Web server on port {} started", self.port);
|
||||||
|
let mut interval = interval(Duration::from_secs(1));
|
||||||
|
|
||||||
|
while !cancellation.is_cancelled() {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancellation.cancelled() => {
|
||||||
|
info!("Web server on port {} received shutdown signal", self.port);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ = interval.tick() => {
|
||||||
|
// Simulate handling requests
|
||||||
|
let count = self.request_count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
info!("Server on port {} handled request #{}", self.port, count + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&self) -> Result<(), MadError> {
|
||||||
|
info!("Shutting down web server on port {}", self.port);
|
||||||
|
// In a real application, you might:
|
||||||
|
// - Drain existing connections
|
||||||
|
// - Save server state
|
||||||
|
// - Close database connections
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
let total = self.request_count.load(Ordering::Relaxed);
|
||||||
|
info!(
|
||||||
|
"Web server on port {} shut down. Total requests handled: {}",
|
||||||
|
self.port, total
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A background job processor that simulates processing tasks from a queue.
|
||||||
|
struct JobProcessor {
|
||||||
|
queue_name: String,
|
||||||
|
processing_interval: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for JobProcessor {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some(format!("job-processor-{}", self.queue_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup(&self) -> Result<(), MadError> {
|
||||||
|
info!("Connecting to queue: {}", self.queue_name);
|
||||||
|
// Simulate connecting to a message queue
|
||||||
|
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
||||||
|
info!("Job processor for {} started", self.queue_name);
|
||||||
|
let mut interval = interval(self.processing_interval);
|
||||||
|
let mut job_count = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancellation.cancelled() => {
|
||||||
|
info!("Job processor for {} stopping...", self.queue_name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ = interval.tick() => {
|
||||||
|
job_count += 1;
|
||||||
|
info!("Processing job #{} from {}", job_count, self.queue_name);
|
||||||
|
|
||||||
|
// Simulate job processing
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
// Simulate occasional errors (but don't fail the component)
|
||||||
|
if job_count % 10 == 0 {
|
||||||
|
warn!("Job #{} from {} required retry", job_count, self.queue_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Job processor for {} processed {} jobs",
|
||||||
|
self.queue_name, job_count
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&self) -> Result<(), MadError> {
|
||||||
|
info!("Disconnecting from queue: {}", self.queue_name);
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A health check component that monitors system health.
|
||||||
|
struct HealthChecker {
|
||||||
|
check_interval: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for HealthChecker {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some("health-checker".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
||||||
|
info!("Health checker started");
|
||||||
|
let mut interval = interval(self.check_interval);
|
||||||
|
|
||||||
|
while !cancellation.is_cancelled() {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancellation.cancelled() => {
|
||||||
|
info!("Health checker stopping...");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ = interval.tick() => {
|
||||||
|
// Simulate health checks
|
||||||
|
let cpu_usage = rand::random::<f32>() * 100.0;
|
||||||
|
let memory_usage = rand::random::<f32>() * 100.0;
|
||||||
|
|
||||||
|
info!("System health: CPU={:.1}%, Memory={:.1}%", cpu_usage, memory_usage);
|
||||||
|
|
||||||
|
if cpu_usage > 90.0 {
|
||||||
|
warn!("High CPU usage detected: {:.1}%", cpu_usage);
|
||||||
|
}
|
||||||
|
if memory_usage > 90.0 {
|
||||||
|
warn!("High memory usage detected: {:.1}%", memory_usage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A component that will fail after some time to demonstrate error handling.
|
||||||
|
struct FailingComponent {
|
||||||
|
fail_after: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for FailingComponent {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some("failing-component".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
||||||
|
info!(
|
||||||
|
"Failing component started (will fail after {:?})",
|
||||||
|
self.fail_after
|
||||||
|
);
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancellation.cancelled() => {
|
||||||
|
info!("Failing component cancelled before failure");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ = tokio::time::sleep(self.fail_after) => {
|
||||||
|
error!("Failing component encountered an error!");
|
||||||
|
Err(anyhow::anyhow!("Simulated component failure").into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Debug component that logs system status periodically.
|
||||||
|
struct DebugComponent;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for DebugComponent {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some("debug-component".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, cancel: CancellationToken) -> Result<(), MadError> {
|
||||||
|
info!("Debug mode enabled - verbose logging active");
|
||||||
|
let mut interval = interval(Duration::from_secs(5));
|
||||||
|
|
||||||
|
while !cancel.is_cancelled() {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel.cancelled() => break,
|
||||||
|
_ = interval.tick() => {
|
||||||
|
info!("DEBUG: System running normally");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Debug component shutting down");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
// Initialize tracing for logging
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_target(false)
|
||||||
|
.without_time()
|
||||||
|
.init();
|
||||||
|
|
||||||
|
info!("Starting comprehensive MAD example application");
|
||||||
|
|
||||||
|
// Check if we should enable the failing component
|
||||||
|
let enable_failure_demo = std::env::var("ENABLE_FAILURE").is_ok();
|
||||||
|
|
||||||
|
// Check if we should enable debug mode
|
||||||
|
let debug_mode = std::env::var("DEBUG").is_ok();
|
||||||
|
|
||||||
|
// Shared state for demonstration
|
||||||
|
let request_count = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
|
// Build and run the application
|
||||||
|
let result = Mad::builder()
|
||||||
|
// Add web servers
|
||||||
|
.add(WebServer {
|
||||||
|
port: 8080,
|
||||||
|
request_count: request_count.clone(),
|
||||||
|
})
|
||||||
|
.add(WebServer {
|
||||||
|
port: 8081,
|
||||||
|
request_count: request_count.clone(),
|
||||||
|
})
|
||||||
|
// Add job processors
|
||||||
|
.add(JobProcessor {
|
||||||
|
queue_name: "high-priority".to_string(),
|
||||||
|
processing_interval: Duration::from_secs(2),
|
||||||
|
})
|
||||||
|
.add(JobProcessor {
|
||||||
|
queue_name: "low-priority".to_string(),
|
||||||
|
processing_interval: Duration::from_secs(5),
|
||||||
|
})
|
||||||
|
// Add health checker
|
||||||
|
.add(HealthChecker {
|
||||||
|
check_interval: Duration::from_secs(3),
|
||||||
|
})
|
||||||
|
// Conditionally add a debug component using add_fn
|
||||||
|
.add_conditional(debug_mode, DebugComponent)
|
||||||
|
// Conditionally add failing component to demonstrate error handling
|
||||||
|
.add_conditional(
|
||||||
|
enable_failure_demo,
|
||||||
|
FailingComponent {
|
||||||
|
fail_after: Duration::from_secs(10),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// Add a simple metrics reporter using add_fn
|
||||||
|
.add_fn(|cancel: CancellationToken| async move {
|
||||||
|
info!("Metrics reporter started");
|
||||||
|
let mut interval = interval(Duration::from_secs(10));
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
while !cancel.is_cancelled() {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel.cancelled() => break,
|
||||||
|
_ = interval.tick() => {
|
||||||
|
let uptime = start.elapsed();
|
||||||
|
info!("Application uptime: {:?}", uptime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Metrics reporter stopped");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
// Configure graceful shutdown timeout
|
||||||
|
.cancellation(Some(Duration::from_secs(5)))
|
||||||
|
// Run the application
|
||||||
|
.run()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("Application shut down successfully");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Application failed: {}", e);
|
||||||
|
|
||||||
|
// Check if it's an aggregate error with multiple failures
|
||||||
|
if let MadError::AggregateError(ref agg) = e {
|
||||||
|
error!("Multiple component failures detected:");
|
||||||
|
for (i, err) in agg.get_errors().iter().enumerate() {
|
||||||
|
error!(" {}. {}", i + 1, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
crates/mad/examples/multi_service/Cargo.toml
Normal file
15
crates/mad/examples/multi_service/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "mad-multi-service-example"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "multi_service"
|
||||||
|
path = "main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
notmad = { path = "../.." }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-util = "0.7"
|
||||||
|
async-trait = "0.1"
|
||||||
|
anyhow = "1"
|
214
crates/mad/examples/multi_service/main.rs
Normal file
214
crates/mad/examples/multi_service/main.rs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
//! Example demonstrating running multiple services with MAD.
|
||||||
|
//!
|
||||||
|
//! This example shows how to run a web server, queue processor, and
|
||||||
|
//! scheduled task together, with graceful shutdown on Ctrl+C.
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use notmad::{Component, Mad, MadError};
|
||||||
|
use tokio::time::{Duration, interval};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
/// A simple web server component.
|
||||||
|
///
|
||||||
|
/// In a real application, this would bind to a port and handle HTTP requests.
|
||||||
|
/// Here we simulate it by periodically logging that we're "handling" requests.
|
||||||
|
struct WebServer {
|
||||||
|
port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for WebServer {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some(format!("WebServer:{}", self.port))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup(&self) -> Result<(), MadError> {
|
||||||
|
println!("[{}] Binding to port...", self.name().unwrap());
|
||||||
|
// Simulate server setup time
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
println!("[{}] Ready to accept connections", self.name().unwrap());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
||||||
|
println!("[{}] Server started", self.name().unwrap());
|
||||||
|
|
||||||
|
// Simulate handling requests until shutdown
|
||||||
|
let mut request_id = 0;
|
||||||
|
let mut interval = interval(Duration::from_secs(2));
|
||||||
|
|
||||||
|
while !cancellation.is_cancelled() {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancellation.cancelled() => {
|
||||||
|
println!("[{}] Shutdown signal received", self.name().unwrap());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ = interval.tick() => {
|
||||||
|
request_id += 1;
|
||||||
|
println!("[{}] Handling request #{}", self.name().unwrap(), request_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&self) -> Result<(), MadError> {
|
||||||
|
println!("[{}] Closing connections...", self.name().unwrap());
|
||||||
|
// Simulate graceful connection drain
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
println!("[{}] Server stopped", self.name().unwrap());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A queue processor that consumes messages from a queue.
|
||||||
|
///
|
||||||
|
/// This simulates processing messages from a message queue like
|
||||||
|
/// RabbitMQ, Kafka, or AWS SQS.
|
||||||
|
struct QueueProcessor {
|
||||||
|
queue_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for QueueProcessor {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some(format!("QueueProcessor:{}", self.queue_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
||||||
|
println!("[{}] Started processing", self.name().unwrap());
|
||||||
|
|
||||||
|
let mut message_count = 0;
|
||||||
|
|
||||||
|
// Process messages until shutdown
|
||||||
|
while !cancellation.is_cancelled() {
|
||||||
|
// Simulate waiting for and processing a message
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancellation.cancelled() => {
|
||||||
|
println!("[{}] Stopping message processing", self.name().unwrap());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ = tokio::time::sleep(Duration::from_secs(1)) => {
|
||||||
|
message_count += 1;
|
||||||
|
println!("[{}] Processed message #{}", self.name().unwrap(), message_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"[{}] Processed {} messages total",
|
||||||
|
self.name().unwrap(),
|
||||||
|
message_count
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A scheduled task that runs periodically.
|
||||||
|
///
|
||||||
|
/// This could be used for tasks like:
|
||||||
|
/// - Cleaning up old data
|
||||||
|
/// - Generating reports
|
||||||
|
/// - Syncing with external systems
|
||||||
|
struct ScheduledTask {
|
||||||
|
task_name: String,
|
||||||
|
interval_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for ScheduledTask {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some(format!("ScheduledTask:{}", self.task_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(&self, cancellation: CancellationToken) -> Result<(), MadError> {
|
||||||
|
println!(
|
||||||
|
"[{}] Scheduled to run every {} seconds",
|
||||||
|
self.name().unwrap(),
|
||||||
|
self.interval_secs
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut interval = interval(Duration::from_secs(self.interval_secs));
|
||||||
|
let mut run_count = 0;
|
||||||
|
|
||||||
|
while !cancellation.is_cancelled() {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancellation.cancelled() => {
|
||||||
|
println!("[{}] Scheduler stopping", self.name().unwrap());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ = interval.tick() => {
|
||||||
|
run_count += 1;
|
||||||
|
println!("[{}] Executing run #{}", self.name().unwrap(), run_count);
|
||||||
|
|
||||||
|
// Simulate task execution
|
||||||
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
println!("[{}] Run #{} completed", self.name().unwrap(), run_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
println!("Starting multi-service application");
|
||||||
|
println!("Press Ctrl+C to trigger graceful shutdown");
|
||||||
|
println!("----------------------------------------");
|
||||||
|
|
||||||
|
// Build the application with multiple services
|
||||||
|
Mad::builder()
|
||||||
|
// Add a web server on port 8080
|
||||||
|
.add(WebServer { port: 8080 })
|
||||||
|
// Add another web server on port 8081 (e.g., admin interface)
|
||||||
|
.add(WebServer { port: 8081 })
|
||||||
|
// Add queue processors for different queues
|
||||||
|
.add(QueueProcessor {
|
||||||
|
queue_name: "orders".to_string(),
|
||||||
|
})
|
||||||
|
.add(QueueProcessor {
|
||||||
|
queue_name: "notifications".to_string(),
|
||||||
|
})
|
||||||
|
// Add scheduled tasks
|
||||||
|
.add(ScheduledTask {
|
||||||
|
task_name: "cleanup".to_string(),
|
||||||
|
interval_secs: 5,
|
||||||
|
})
|
||||||
|
.add(ScheduledTask {
|
||||||
|
task_name: "report_generator".to_string(),
|
||||||
|
interval_secs: 10,
|
||||||
|
})
|
||||||
|
// Add a monitoring component using a closure
|
||||||
|
.add_fn(|cancel| async move {
|
||||||
|
println!("[Monitor] Starting system monitor");
|
||||||
|
let mut interval = interval(Duration::from_secs(15));
|
||||||
|
|
||||||
|
while !cancel.is_cancelled() {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel.cancelled() => {
|
||||||
|
println!("[Monitor] Stopping monitor");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ = interval.tick() => {
|
||||||
|
println!("[Monitor] All systems operational");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
// Set graceful shutdown timeout to 3 seconds
|
||||||
|
.cancellation(Some(Duration::from_secs(3)))
|
||||||
|
// Run all components
|
||||||
|
.run()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("----------------------------------------");
|
||||||
|
println!("All services shut down successfully");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
86
crates/mad/examples/nested_errors/main.rs
Normal file
86
crates/mad/examples/nested_errors/main.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
struct NestedErrorComponent {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl notmad::Component for NestedErrorComponent {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some(self.name.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl notmad::Component for AnotherFailingComponent {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some("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,49 +1,228 @@
|
|||||||
|
//! # MAD - Lifecycle Manager for Rust Applications
|
||||||
|
//!
|
||||||
|
//! 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.
|
||||||
|
//!
|
||||||
|
//! ## 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
|
||||||
|
//!
|
||||||
|
//! ```rust,no_run
|
||||||
|
//! use notmad::{Component, Mad};
|
||||||
|
//! use async_trait::async_trait;
|
||||||
|
//! use tokio_util::sync::CancellationToken;
|
||||||
|
//!
|
||||||
|
//! struct MyService {
|
||||||
|
//! name: String,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[async_trait]
|
||||||
|
//! impl Component for MyService {
|
||||||
|
//! fn name(&self) -> Option<String> {
|
||||||
|
//! Some(self.name.clone())
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! 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(())
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! async fn main() -> anyhow::Result<()> {
|
||||||
|
//! Mad::builder()
|
||||||
|
//! .add(MyService { name: "service-1".into() })
|
||||||
|
//! .add(MyService { name: "service-2".into() })
|
||||||
|
//! .run()
|
||||||
|
//! .await?;
|
||||||
|
//! Ok(())
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Component Lifecycle
|
||||||
|
//!
|
||||||
|
//! Components go through three phases:
|
||||||
|
//!
|
||||||
|
//! 1. **Setup**: Optional initialization phase before components start running
|
||||||
|
//! 2. **Run**: Main execution phase where components perform their work
|
||||||
|
//! 3. **Close**: Optional cleanup phase after components stop
|
||||||
|
//!
|
||||||
|
//! ## 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::{fmt::Display, sync::Arc, error::Error};
|
||||||
use tokio::signal::unix::{signal, SignalKind};
|
use tokio::signal::unix::{SignalKind, signal};
|
||||||
|
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
use crate::waiter::Waiter;
|
||||||
|
|
||||||
|
mod waiter;
|
||||||
|
|
||||||
|
/// Error type for MAD operations.
|
||||||
|
///
|
||||||
|
/// This enum represents all possible errors that can occur during
|
||||||
|
/// the lifecycle of MAD components.
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum MadError {
|
pub enum MadError {
|
||||||
#[error("component failed: {0}")]
|
/// Generic error wrapper for anyhow errors.
|
||||||
Inner(#[source] anyhow::Error),
|
///
|
||||||
|
/// This variant is used when components return errors via the `?` operator
|
||||||
|
/// or when converting from `anyhow::Error`.
|
||||||
|
#[error(transparent)]
|
||||||
|
Inner(anyhow::Error),
|
||||||
|
|
||||||
#[error("component(s) failed: {run}")]
|
/// Error that occurred during the run phase of a component.
|
||||||
RunError { run: anyhow::Error },
|
#[error(transparent)]
|
||||||
|
RunError {
|
||||||
|
run: anyhow::Error
|
||||||
|
},
|
||||||
|
|
||||||
#[error("component(s) failed: {close}")]
|
/// Error that occurred during the close phase of a component.
|
||||||
CloseError { close: anyhow::Error },
|
#[error("component(s) failed during close")]
|
||||||
|
CloseError {
|
||||||
|
#[source]
|
||||||
|
close: anyhow::Error
|
||||||
|
},
|
||||||
|
|
||||||
#[error("component(s) failed: {0}")]
|
/// Multiple errors from different components.
|
||||||
|
///
|
||||||
|
/// This is used when multiple components fail simultaneously,
|
||||||
|
/// allowing all errors to be reported rather than just the first one.
|
||||||
|
#[error("{0}")]
|
||||||
AggregateError(AggregateError),
|
AggregateError(AggregateError),
|
||||||
|
|
||||||
|
/// Returned when a component doesn't implement the optional setup method.
|
||||||
|
///
|
||||||
|
/// This is not typically an error condition as setup is optional.
|
||||||
#[error("setup not defined")]
|
#[error("setup not defined")]
|
||||||
SetupNotDefined,
|
SetupNotDefined,
|
||||||
|
|
||||||
|
/// Returned when a component doesn't implement the optional close method.
|
||||||
|
///
|
||||||
|
/// This is not typically an error condition as close is optional.
|
||||||
#[error("close not defined")]
|
#[error("close not defined")]
|
||||||
CloseNotDefined,
|
CloseNotDefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
impl From<anyhow::Error> for MadError {
|
||||||
|
fn from(value: anyhow::Error) -> Self {
|
||||||
|
Self::Inner(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Container for multiple errors from different components.
|
||||||
|
///
|
||||||
|
/// When multiple components fail, their errors are collected
|
||||||
|
/// into this struct to provide complete error reporting.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub struct AggregateError {
|
pub struct AggregateError {
|
||||||
errors: Vec<MadError>,
|
errors: Vec<MadError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AggregateError {
|
||||||
|
/// Returns a slice of all contained errors.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// match result {
|
||||||
|
/// Err(notmad::MadError::AggregateError(agg)) => {
|
||||||
|
/// for error in agg.get_errors() {
|
||||||
|
/// eprintln!("Component error: {}", error);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// _ => {}
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn get_errors(&self) -> &[MadError] {
|
||||||
|
&self.errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for AggregateError {
|
impl Display for AggregateError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_str("MadError::AggregateError: (")?;
|
if self.errors.is_empty() {
|
||||||
|
return Ok(());
|
||||||
for error in &self.errors {
|
|
||||||
f.write_str(&error.to_string())?;
|
|
||||||
f.write_str(", ")?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f.write_str(")")
|
if self.errors.len() == 1 {
|
||||||
|
return write!(f, "{}", self.errors[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(f, "{} component errors occurred:", self.errors.len())?;
|
||||||
|
for (i, error) in self.errors.iter().enumerate() {
|
||||||
|
write!(f, "\n[Component {}] {}", i + 1, error)?;
|
||||||
|
|
||||||
|
// Print the error chain for each component error
|
||||||
|
let mut source = error.source();
|
||||||
|
let mut level = 1;
|
||||||
|
while let Some(err) = source {
|
||||||
|
write!(f, "\n {}. {}", level, err)?;
|
||||||
|
source = err.source();
|
||||||
|
level += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The main lifecycle manager for running multiple components.
|
||||||
|
///
|
||||||
|
/// `Mad` orchestrates the lifecycle of multiple components, ensuring they
|
||||||
|
/// start up in order, run concurrently, and shut down gracefully.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use notmad::{Component, Mad};
|
||||||
|
/// use async_trait::async_trait;
|
||||||
|
/// use tokio_util::sync::CancellationToken;
|
||||||
|
///
|
||||||
|
/// struct MyComponent;
|
||||||
|
///
|
||||||
|
/// #[async_trait]
|
||||||
|
/// impl Component for MyComponent {
|
||||||
|
/// async fn run(&self, _cancel: CancellationToken) -> Result<(), notmad::MadError> {
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// # async fn example() -> Result<(), notmad::MadError> {
|
||||||
|
/// Mad::builder()
|
||||||
|
/// .add(MyComponent)
|
||||||
|
/// .run()
|
||||||
|
/// .await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
pub struct Mad {
|
pub struct Mad {
|
||||||
components: Vec<Arc<dyn Component + Send + Sync + 'static>>,
|
components: Vec<Arc<dyn Component + Send + Sync + 'static>>,
|
||||||
|
|
||||||
@@ -56,6 +235,18 @@ struct CompletionResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Mad {
|
impl Mad {
|
||||||
|
/// Creates a new `Mad` builder.
|
||||||
|
///
|
||||||
|
/// This is the entry point for constructing a MAD application.
|
||||||
|
/// Components are added using the builder pattern before calling `run()`.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use notmad::Mad;
|
||||||
|
///
|
||||||
|
/// let mut app = Mad::builder();
|
||||||
|
/// ```
|
||||||
pub fn builder() -> Self {
|
pub fn builder() -> Self {
|
||||||
Self {
|
Self {
|
||||||
components: Vec::default(),
|
components: Vec::default(),
|
||||||
@@ -64,12 +255,123 @@ impl Mad {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a component to the MAD application.
|
||||||
|
///
|
||||||
|
/// Components will be set up in the order they are added,
|
||||||
|
/// run concurrently, and closed in the order they were added.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `component` - Any type that implements `Component` or `IntoComponent`
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use notmad::{Component, Mad};
|
||||||
|
/// # use async_trait::async_trait;
|
||||||
|
/// # use tokio_util::sync::CancellationToken;
|
||||||
|
/// # struct MyService;
|
||||||
|
/// # #[async_trait]
|
||||||
|
/// # impl Component for MyService {
|
||||||
|
/// # async fn run(&self, _: CancellationToken) -> Result<(), notmad::MadError> { Ok(()) }
|
||||||
|
/// # }
|
||||||
|
///
|
||||||
|
/// Mad::builder()
|
||||||
|
/// .add(MyService)
|
||||||
|
/// .add(MyService);
|
||||||
|
/// ```
|
||||||
pub fn add(&mut self, component: impl IntoComponent) -> &mut Self {
|
pub fn add(&mut self, component: impl IntoComponent) -> &mut Self {
|
||||||
self.components.push(component.into_component());
|
self.components.push(component.into_component());
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Conditionally adds a component based on a boolean condition.
|
||||||
|
///
|
||||||
|
/// If the condition is false, a waiter component is added instead,
|
||||||
|
/// which simply waits for cancellation without doing any work.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `condition` - If true, adds the component; if false, adds a waiter
|
||||||
|
/// * `component` - The component to add if condition is true
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use notmad::Mad;
|
||||||
|
/// # use notmad::Component;
|
||||||
|
/// # use async_trait::async_trait;
|
||||||
|
/// # use tokio_util::sync::CancellationToken;
|
||||||
|
/// # struct DebugService;
|
||||||
|
/// # #[async_trait]
|
||||||
|
/// # impl Component for DebugService {
|
||||||
|
/// # async fn run(&self, _: CancellationToken) -> Result<(), notmad::MadError> { Ok(()) }
|
||||||
|
/// # }
|
||||||
|
///
|
||||||
|
/// let enable_debug = std::env::var("DEBUG").is_ok();
|
||||||
|
///
|
||||||
|
/// Mad::builder()
|
||||||
|
/// .add_conditional(enable_debug, DebugService);
|
||||||
|
/// ```
|
||||||
|
pub fn add_conditional(&mut self, condition: bool, component: impl IntoComponent) -> &mut Self {
|
||||||
|
if condition {
|
||||||
|
self.components.push(component.into_component());
|
||||||
|
} else {
|
||||||
|
self.components
|
||||||
|
.push(Waiter::new(component.into_component()).into_component())
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a waiter component that does nothing but wait for cancellation.
|
||||||
|
///
|
||||||
|
/// This is useful when you need a placeholder component or want
|
||||||
|
/// the application to keep running without any specific work.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # async fn example() {
|
||||||
|
/// use notmad::Mad;
|
||||||
|
///
|
||||||
|
/// Mad::builder()
|
||||||
|
/// .add_wait() // Keeps the app running until shutdown signal
|
||||||
|
/// .run()
|
||||||
|
/// .await;
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn add_wait(&mut self) -> &mut Self {
|
||||||
|
self.components.push(Waiter::default().into_component());
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a closure or function as a component.
|
||||||
|
///
|
||||||
|
/// This is a convenient way to add simple components without
|
||||||
|
/// creating a full struct that implements `Component`.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `f` - A closure that takes a `CancellationToken` and returns a future
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use notmad::Mad;
|
||||||
|
/// use tokio_util::sync::CancellationToken;
|
||||||
|
///
|
||||||
|
/// Mad::builder()
|
||||||
|
/// .add_fn(|cancel: CancellationToken| async move {
|
||||||
|
/// while !cancel.is_cancelled() {
|
||||||
|
/// println!("Working...");
|
||||||
|
/// tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
/// }
|
||||||
|
/// Ok(())
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
pub fn add_fn<F, Fut>(&mut self, f: F) -> &mut Self
|
pub fn add_fn<F, Fut>(&mut self, f: F) -> &mut Self
|
||||||
where
|
where
|
||||||
F: Fn(CancellationToken) -> Fut + Send + Sync + 'static,
|
F: Fn(CancellationToken) -> Fut + Send + Sync + 'static,
|
||||||
@@ -80,12 +382,63 @@ impl Mad {
|
|||||||
self.add(comp)
|
self.add(comp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configures the cancellation timeout behavior.
|
||||||
|
///
|
||||||
|
/// When a shutdown signal is received, MAD will:
|
||||||
|
/// 1. Send cancellation tokens to all components
|
||||||
|
/// 2. Wait for the specified duration
|
||||||
|
/// 3. Force shutdown if components haven't stopped
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `should_cancel` - Duration to wait after cancellation before forcing shutdown.
|
||||||
|
/// Pass `None` to wait indefinitely.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # async fn example() {
|
||||||
|
/// use notmad::Mad;
|
||||||
|
/// use std::time::Duration;
|
||||||
|
///
|
||||||
|
/// Mad::builder()
|
||||||
|
/// .cancellation(Some(Duration::from_secs(30))) // 30 second grace period
|
||||||
|
/// .run()
|
||||||
|
/// .await;
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
pub fn cancellation(&mut self, should_cancel: Option<std::time::Duration>) -> &mut Self {
|
pub fn cancellation(&mut self, should_cancel: Option<std::time::Duration>) -> &mut Self {
|
||||||
self.should_cancel = should_cancel;
|
self.should_cancel = should_cancel;
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs all components until completion or shutdown.
|
||||||
|
///
|
||||||
|
/// This method:
|
||||||
|
/// 1. Calls `setup()` on all components (in order)
|
||||||
|
/// 2. Starts all components concurrently
|
||||||
|
/// 3. Waits for shutdown signal (SIGTERM, Ctrl+C) or component failure
|
||||||
|
/// 4. Sends cancellation to all components
|
||||||
|
/// 5. Calls `close()` on all components (in order)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Ok(())` if all components shut down cleanly
|
||||||
|
/// * `Err(MadError)` if any component fails
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # use notmad::Mad;
|
||||||
|
/// # async fn example() -> Result<(), notmad::MadError> {
|
||||||
|
/// Mad::builder()
|
||||||
|
/// .add_wait()
|
||||||
|
/// .run()
|
||||||
|
/// .await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
pub async fn run(&mut self) -> Result<(), MadError> {
|
pub async fn run(&mut self) -> Result<(), MadError> {
|
||||||
tracing::info!("running mad setup");
|
tracing::info!("running mad setup");
|
||||||
|
|
||||||
@@ -100,7 +453,7 @@ impl Mad {
|
|||||||
(Err(run), Err(close)) => {
|
(Err(run), Err(close)) => {
|
||||||
return Err(MadError::AggregateError(AggregateError {
|
return Err(MadError::AggregateError(AggregateError {
|
||||||
errors: vec![run, close],
|
errors: vec![run, close],
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
(Ok(_), Ok(_)) => {}
|
(Ok(_), Ok(_)) => {}
|
||||||
(Ok(_), Err(close)) => return Err(close),
|
(Ok(_), Err(close)) => return Err(close),
|
||||||
@@ -248,24 +601,148 @@ async fn signal_unix_terminate() {
|
|||||||
sigterm.recv().await;
|
sigterm.recv().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trait for implementing MAD components.
|
||||||
|
///
|
||||||
|
/// Components represent individual services or tasks that run as part
|
||||||
|
/// of your application. Each component has its own lifecycle with
|
||||||
|
/// optional setup and cleanup phases.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use notmad::{Component, MadError};
|
||||||
|
/// use async_trait::async_trait;
|
||||||
|
/// use tokio_util::sync::CancellationToken;
|
||||||
|
///
|
||||||
|
/// struct DatabaseConnection {
|
||||||
|
/// url: String,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[async_trait]
|
||||||
|
/// impl Component for DatabaseConnection {
|
||||||
|
/// fn name(&self) -> Option<String> {
|
||||||
|
/// Some("database".to_string())
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// async fn setup(&self) -> Result<(), MadError> {
|
||||||
|
/// println!("Connecting to database...");
|
||||||
|
/// // Initialize connection pool
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// async fn run(&self, cancel: CancellationToken) -> Result<(), MadError> {
|
||||||
|
/// // Keep connection alive, handle queries
|
||||||
|
/// cancel.cancelled().await;
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// async fn close(&self) -> Result<(), MadError> {
|
||||||
|
/// println!("Closing database connection...");
|
||||||
|
/// // Clean up resources
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait Component {
|
pub trait Component {
|
||||||
|
/// Returns an optional name for the component.
|
||||||
|
///
|
||||||
|
/// This name is used in logging and error messages to identify
|
||||||
|
/// which component is being processed.
|
||||||
|
///
|
||||||
|
/// # Default
|
||||||
|
///
|
||||||
|
/// Returns `None` if not overridden.
|
||||||
fn name(&self) -> Option<String> {
|
fn name(&self) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Optional setup phase called before the component starts running.
|
||||||
|
///
|
||||||
|
/// Use this for initialization tasks like:
|
||||||
|
/// - Establishing database connections
|
||||||
|
/// - Loading configuration
|
||||||
|
/// - Preparing resources
|
||||||
|
///
|
||||||
|
/// # Default
|
||||||
|
///
|
||||||
|
/// Returns `MadError::SetupNotDefined` which is handled gracefully.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If setup fails with an error other than `SetupNotDefined`,
|
||||||
|
/// the entire application will stop before any components start running.
|
||||||
async fn setup(&self) -> Result<(), MadError> {
|
async fn setup(&self) -> Result<(), MadError> {
|
||||||
Err(MadError::SetupNotDefined)
|
Err(MadError::SetupNotDefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Main execution phase of the component.
|
||||||
|
///
|
||||||
|
/// This method should contain the primary logic of your component.
|
||||||
|
/// It should respect the cancellation token and shut down gracefully
|
||||||
|
/// when cancellation is requested.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `cancellation_token` - Signal for graceful shutdown
|
||||||
|
///
|
||||||
|
/// # Implementation Guidelines
|
||||||
|
///
|
||||||
|
/// - Check `cancellation_token.is_cancelled()` periodically
|
||||||
|
/// - Use `tokio::select!` with `cancellation_token.cancelled()` for async operations
|
||||||
|
/// - Clean up resources before returning
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Any error returned will trigger shutdown of all other components.
|
||||||
async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError>;
|
async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError>;
|
||||||
|
|
||||||
|
/// Optional cleanup phase called after the component stops.
|
||||||
|
///
|
||||||
|
/// Use this for cleanup tasks like:
|
||||||
|
/// - Flushing buffers
|
||||||
|
/// - Closing connections
|
||||||
|
/// - Saving state
|
||||||
|
///
|
||||||
|
/// # Default
|
||||||
|
///
|
||||||
|
/// Returns `MadError::CloseNotDefined` which is handled gracefully.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Errors during close are logged but don't prevent other components
|
||||||
|
/// from closing.
|
||||||
async fn close(&self) -> Result<(), MadError> {
|
async fn close(&self) -> Result<(), MadError> {
|
||||||
Err(MadError::CloseNotDefined)
|
Err(MadError::CloseNotDefined)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trait for converting types into components.
|
||||||
|
///
|
||||||
|
/// This trait is automatically implemented for all types that implement
|
||||||
|
/// `Component + Send + Sync + 'static`, allowing them to be added to MAD
|
||||||
|
/// directly without manual conversion.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use notmad::{Component, IntoComponent, Mad};
|
||||||
|
/// # use async_trait::async_trait;
|
||||||
|
/// # use tokio_util::sync::CancellationToken;
|
||||||
|
///
|
||||||
|
/// struct MyService;
|
||||||
|
///
|
||||||
|
/// # #[async_trait]
|
||||||
|
/// # impl Component for MyService {
|
||||||
|
/// # async fn run(&self, _: CancellationToken) -> Result<(), notmad::MadError> { Ok(()) }
|
||||||
|
/// # }
|
||||||
|
///
|
||||||
|
/// // MyService automatically implements IntoComponent
|
||||||
|
/// Mad::builder()
|
||||||
|
/// .add(MyService); // Works directly
|
||||||
|
/// ```
|
||||||
pub trait IntoComponent {
|
pub trait IntoComponent {
|
||||||
|
/// Converts self into an Arc-wrapped component.
|
||||||
fn into_component(self) -> Arc<dyn Component + Send + Sync + 'static>;
|
fn into_component(self) -> Arc<dyn Component + Send + Sync + 'static>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,3 +782,132 @@ where
|
|||||||
self.execute(cancellation_token).await
|
self.execute(cancellation_token).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use anyhow::Context;
|
||||||
|
|
||||||
|
#[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;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Component for FailingComponent {
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
Some("test-component".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
84
crates/mad/src/waiter.rs
Normal file
84
crates/mad/src/waiter.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
//! Waiter components for MAD.
|
||||||
|
//!
|
||||||
|
//! This module provides waiter components that simply wait for cancellation
|
||||||
|
//! without performing any work. Useful for keeping the application alive
|
||||||
|
//! or as placeholders in conditional component loading.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
use crate::{Component, MadError};
|
||||||
|
|
||||||
|
/// A default waiter component that panics if run.
|
||||||
|
///
|
||||||
|
/// This is used internally as a placeholder that should never
|
||||||
|
/// actually be executed.
|
||||||
|
pub struct DefaultWaiter {}
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for DefaultWaiter {
|
||||||
|
async fn run(&self, _cancellation_token: CancellationToken) -> Result<(), MadError> {
|
||||||
|
panic!("should never be called");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper component that waits for cancellation.
|
||||||
|
///
|
||||||
|
/// Instead of running the wrapped component's logic, this simply
|
||||||
|
/// waits for the cancellation token. This is useful for conditionally
|
||||||
|
/// disabling components while keeping the same structure.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// use mad::Waiter;
|
||||||
|
///
|
||||||
|
/// // Instead of running the service, just wait
|
||||||
|
/// let waiter = Waiter::new(service.into_component());
|
||||||
|
/// ```
|
||||||
|
pub struct Waiter {
|
||||||
|
comp: Arc<dyn Component + Send + Sync + 'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Waiter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
comp: Arc::new(DefaultWaiter {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Waiter {
|
||||||
|
/// Creates a new waiter that wraps the given component.
|
||||||
|
///
|
||||||
|
/// The wrapped component's name will be used (prefixed with "waiter/"),
|
||||||
|
/// but its run method will not be called.
|
||||||
|
pub fn new(c: Arc<dyn Component + Send + Sync + 'static>) -> Self {
|
||||||
|
Self { comp: c }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for Waiter {
|
||||||
|
/// Returns the name of the waiter, prefixed with "waiter/".
|
||||||
|
///
|
||||||
|
/// If the wrapped component has a name, it will be "waiter/{name}".
|
||||||
|
/// Otherwise, returns "waiter".
|
||||||
|
fn name(&self) -> Option<String> {
|
||||||
|
match self.comp.name() {
|
||||||
|
Some(name) => Some(format!("waiter/{name}")),
|
||||||
|
None => Some("waiter".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for cancellation without performing any work.
|
||||||
|
///
|
||||||
|
/// This method simply waits for the cancellation token to be triggered,
|
||||||
|
/// then returns successfully.
|
||||||
|
async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError> {
|
||||||
|
cancellation_token.cancelled().await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use notmad::{Component, Mad};
|
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;
|
||||||
@@ -137,3 +137,20 @@ async fn test_can_shutdown_gracefully() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_can_easily_transform_error() -> anyhow::Result<()> {
|
||||||
|
fn fallible() -> anyhow::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner() -> Result<(), MadError> {
|
||||||
|
fallible()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
inner()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# yaml-language-server: $schema=https://git.front.kjuulh.io/kjuulh/cuddle/raw/branch/main/schemas/base.json
|
# yaml-language-server: $schema=https://git.kjuulh.io/kjuulh/cuddle/raw/branch/main/schemas/base.json
|
||||||
|
|
||||||
base: "git@git.front.kjuulh.io:kjuulh/cuddle-rust-lib-plan.git"
|
base: "git@git.kjuulh.io:kjuulh/cuddle-rust-lib-plan.git"
|
||||||
|
|
||||||
vars:
|
vars:
|
||||||
service: "mad"
|
service: "mad"
|
||||||
@@ -12,6 +12,6 @@ please:
|
|||||||
repository: "mad"
|
repository: "mad"
|
||||||
branch: main
|
branch: main
|
||||||
settings:
|
settings:
|
||||||
api_url: "https://git.front.kjuulh.io"
|
api_url: "https://git.kjuulh.io"
|
||||||
actions:
|
actions:
|
||||||
rust:
|
rust:
|
||||||
|
Reference in New Issue
Block a user