15 Commits

Author SHA1 Message Date
689bfd1325 BREAKING: name() -> info() and removed async_trait
Some checks failed
continuous-integration/drone/push Build encountered an error
Signed-off-by: kjuulh <contact@kjuulh.io>
2026-02-05 23:32:27 +01:00
f0c90edce9 feat: replace async-trait with erased box type
Signed-off-by: kjuulh <contact@kjuulh.io>
2026-02-05 23:32:27 +01:00
5e60a272f7 fix(deps): update rust crate thiserror to v2.0.18 (#46)
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-19 02:39:03 +01:00
34d609937d fix(deps): update rust crate tokio-util to v0.7.18 (#45)
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-05 02:39:14 +01:00
094b14c945 chore(deps): update rust crate tokio to v1.49.0 (#44)
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-04 02:41:05 +01:00
f6d4f846fc chore(deps): update rust crate tracing to v0.1.44 (#43)
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-19 02:47:52 +01:00
7f3139f4f9 chore(deps): update tokio-tracing monorepo (#41)
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-29 02:44:04 +01:00
494ec05874 chore(release): v0.10.0 (#40)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
chore(release): 0.10.0

Co-authored-by: cuddle-please <bot@cuddle.sh>
Reviewed-on: #40
2025-11-15 14:48:14 +01:00
8c0128612f feat: implement take errors
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2025-11-15 14:47:24 +01:00
cbe049b6a2 chore(release): v0.9.0 (#36)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
chore(release): 0.9.0

Co-authored-by: cuddle-please <bot@cuddle.sh>
Reviewed-on: #36
2025-11-15 14:35:36 +01:00
2d6b14ad77 feat: mad not properly surfaces panics
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2025-11-15 14:33:00 +01:00
f777ec9b1e fix(deps): update all dependencies (#38)
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-14 02:38:11 +01:00
2415088792 chore(deps): update rust crate tracing-subscriber to v0.3.20 (#37)
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-13 03:15:23 +01:00
a35d15edc2 feat: add publish
All checks were successful
continuous-integration/drone/push Build is passing
2025-09-03 12:52:24 +02:00
613947ac88 feat: add readme
All checks were successful
continuous-integration/drone/push Build is passing
2025-09-03 12:40:28 +02:00
16 changed files with 553 additions and 508 deletions

View File

@@ -6,6 +6,24 @@ 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 ## [0.8.1] - 2025-08-09
### Other ### Other

319
Cargo.lock generated
View File

@@ -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"

View File

@@ -3,10 +3,9 @@ members = ["crates/*"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.8.1" 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"] }

210
README.md
View File

@@ -4,165 +4,163 @@
[![Documentation](https://docs.rs/notmad/badge.svg)](https://docs.rs/notmad) [![Documentation](https://docs.rs/notmad/badge.svg)](https://docs.rs/notmad)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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(()) Ok(())
} }
``` ```
## Advanced Usage ## Basic Usage
### Custom Components ### Axum Web Server with Graceful Shutdown
Components can be anything that implements the `Component` trait: Here's how to run an Axum server with MAD's graceful shutdown:
```rust ```rust
use mad::{Component, Mad}; use axum::{Router, routing::get};
use async_trait::async_trait; use notmad::{Component, ComponentInfo};
use tokio_util::sync::CancellationToken;
struct QueueProcessor { struct WebServer {
queue_name: String, port: u16,
} }
#[async_trait] impl Component for WebServer {
impl Component for QueueProcessor { fn info(&self) -> ComponentInfo {
fn name(&self) -> Option<String> { format!("WebServer:{}", self.port).into()
Some(format!("QueueProcessor-{}", self.queue_name))
} }
async fn run(&self, cancellation: CancellationToken) -> Result<(), mad::MadError> { async fn run(&self, cancel: CancellationToken) -> Result<(), notmad::MadError> {
while !cancellation.is_cancelled() { let app = Router::new().route("/", get(|| async { "Hello, World!" }));
// Process messages from queue
self.process_next_message().await?; 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(())
} }
} }
``` ```
### Error Handling ### Run Multiple Services
MAD provides comprehensive error handling through the `MadError` type with automatic conversion from `anyhow::Error`:
```rust ```rust
async fn run(&self, cancellation: CancellationToken) -> Result<(), mad::MadError> { Mad::builder()
// Errors automatically convert from anyhow::Error to MadError .add(WebServer { port: 8080 })
database_operation().await?; .add(WebServer { port: 8081 })
.run()
.await?;
```
// Or return explicit errors ### Use Functions as Components
if some_condition {
return Err(anyhow::anyhow!("Something went wrong").into()); ```rust
Mad::builder()
.add_fn(|cancel| async move {
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(())
} }
Ok(()) async fn run(&self, cancel: CancellationToken) -> Result<(), notmad::MadError> {
cancel.cancelled().await;
Ok(())
}
async fn close(&self) -> Result<(), notmad::MadError> {
println!("Closing database connection...");
Ok(())
}
} }
``` ```
## Migration from v0.9
### Breaking Changes
1. **`name()``info()`**: Returns `ComponentInfo` instead of `Option<String>`
```rust
// Before
fn name(&self) -> Option<String> { Some("my-service".into()) }
// After
fn info(&self) -> ComponentInfo { "my-service".into() }
```
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)

View File

@@ -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"

View File

@@ -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);

View File

@@ -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> {

View File

@@ -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);

View File

@@ -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> {

View File

@@ -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);
} }
} }
} }

View File

@@ -1,14 +1,13 @@
use async_trait::async_trait; use notmad::ComponentInfo;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
struct NestedErrorComponent { struct NestedErrorComponent {
name: String, name: String,
} }
#[async_trait]
impl notmad::Component for NestedErrorComponent { impl notmad::Component for NestedErrorComponent {
fn name(&self) -> Option<String> { fn info(&self) -> ComponentInfo {
Some(self.name.clone()) self.name.clone().into()
} }
async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> { async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> {
@@ -28,10 +27,9 @@ impl notmad::Component for NestedErrorComponent {
struct AnotherFailingComponent; struct AnotherFailingComponent;
#[async_trait]
impl notmad::Component for AnotherFailingComponent { impl notmad::Component for AnotherFailingComponent {
fn name(&self) -> Option<String> { fn info(&self) -> ComponentInfo {
Some("another-component".into()) "another-component".into()
} }
async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> { async fn run(&self, _cancellation: CancellationToken) -> Result<(), notmad::MadError> {

View File

@@ -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;

View File

@@ -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, error::Error}; 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;
@@ -101,15 +64,13 @@ pub enum MadError {
/// Error that occurred during the run phase of a component. /// Error that occurred during the run phase of a component.
#[error(transparent)] #[error(transparent)]
RunError { RunError { run: anyhow::Error },
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 during close")] #[error("component(s) failed during close")]
CloseError { CloseError {
#[source] #[source]
close: anyhow::Error close: anyhow::Error,
}, },
/// Multiple errors from different components. /// Multiple errors from different components.
@@ -165,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 {
@@ -203,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(())
@@ -224,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>,
} }
@@ -268,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(()) }
/// # } /// # }
@@ -301,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(()) }
/// # } /// # }
@@ -392,7 +351,7 @@ impl Mad {
/// # Arguments /// # Arguments
/// ///
/// * `should_cancel` - Duration to wait after cancellation before forcing shutdown. /// * `should_cancel` - Duration to wait after cancellation before forcing shutdown.
/// Pass `None` to wait indefinitely. /// Pass `None` to wait indefinitely.
/// ///
/// # Example /// # Example
/// ///
@@ -467,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) => {}
@@ -497,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
} }
} }
}); });
@@ -583,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),
@@ -601,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
@@ -610,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> {
@@ -643,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
@@ -653,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.
@@ -672,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.
@@ -695,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.
/// ///
@@ -712,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
} }
} }
@@ -727,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(()) }
/// # } /// # }
@@ -743,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),
}
} }
} }
@@ -772,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,
@@ -786,7 +870,6 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use anyhow::Context;
#[test] #[test]
fn test_error_chaining_display() { fn test_error_chaining_display() {
@@ -818,13 +901,13 @@ mod tests {
fn test_aggregate_error_display() { fn test_aggregate_error_display() {
let error1 = MadError::Inner( let error1 = MadError::Inner(
anyhow::anyhow!("database connection failed") anyhow::anyhow!("database connection failed")
.context("failed to connect to PostgreSQL") .context("failed to connect to PostgreSQL"),
); );
let error2 = MadError::Inner( let error2 = MadError::Inner(
anyhow::anyhow!("port already in use") anyhow::anyhow!("port already in use")
.context("failed to bind to port 8080") .context("failed to bind to port 8080")
.context("web server initialization failed") .context("web server initialization failed"),
); );
let aggregate = MadError::AggregateError(AggregateError { let aggregate = MadError::AggregateError(AggregateError {
@@ -864,7 +947,7 @@ mod tests {
let error = MadError::Inner( let error = MadError::Inner(
anyhow::anyhow!("root cause") anyhow::anyhow!("root cause")
.context("middle layer") .context("middle layer")
.context("top layer") .context("top layer"),
); );
// Test that we can access the error chain // Test that we can access the error chain
@@ -883,10 +966,9 @@ mod tests {
async fn test_component_error_propagation() { async fn test_component_error_propagation() {
struct FailingComponent; struct FailingComponent;
#[async_trait::async_trait]
impl Component for FailingComponent { impl Component for FailingComponent {
fn name(&self) -> Option<String> { fn info(&self) -> ComponentInfo {
Some("test-component".to_string()) "test-component".into()
} }
async fn run(&self, _cancel: CancellationToken) -> Result<(), MadError> { async fn run(&self, _cancel: CancellationToken) -> Result<(), MadError> {

View File

@@ -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(),
} }
} }

View File

@@ -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<()> {

View File

@@ -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: